From 182fcfdca2c176e12f49704b78ac8f7cca89704c Mon Sep 17 00:00:00 2001 From: Petr Pucil Date: Tue, 20 Feb 2024 14:46:56 +0100 Subject: [PATCH 01/10] Display a partial object tree if a parsing error occurred See https://github.com/kaitai-io/kaitai_struct_webide/issues/156 --- src/entities.ts | 5 +++++ src/v1/ExportToJson.ts | 21 ++++++++++++++------- src/v1/app.ts | 6 +++--- src/v1/app.worker.ts | 24 ++++++++++++++---------- src/v1/kaitaiWorker.ts | 23 +++++++++++++++++++---- src/v1/parsedToTree.ts | 2 +- 6 files changed, 56 insertions(+), 25 deletions(-) diff --git a/src/entities.ts b/src/entities.ts index 3f95806b..077b2095 100644 --- a/src/entities.ts +++ b/src/entities.ts @@ -16,6 +16,11 @@ interface IWorkerMessage { } /* tslint:enable */ +interface IWorkerResponse { + result: any; + error: any; +} + interface IInstance { path: string[]; offset: number; diff --git a/src/v1/ExportToJson.ts b/src/v1/ExportToJson.ts index ffdf3769..45546b71 100644 --- a/src/v1/ExportToJson.ts +++ b/src/v1/ExportToJson.ts @@ -1,4 +1,4 @@ -import { workerMethods } from "./app.worker"; +import { workerMethods } from "./app.worker"; export function exportToJson(useHex: boolean = false) { var indentLen = 2; @@ -44,9 +44,16 @@ export function exportToJson(useHex: boolean = false) { } } - return workerMethods.reparse(true).then(exportedRoot => { - console.log("exported", exportedRoot); - expToNative(exportedRoot); - return result; - }); -} \ No newline at end of file + return workerMethods.reparse(true) + .then(response => { + if (response.error) { + throw response.error; + } + return response.result; + }) + .then(exportedRoot => { + console.log("exported", exportedRoot); + expToNative(exportedRoot); + return result; + }); +} diff --git a/src/v1/app.ts b/src/v1/app.ts index d55cf531..9419f8ef 100644 --- a/src/v1/app.ts +++ b/src/v1/app.ts @@ -1,4 +1,4 @@ -import * as localforage from "localforage"; +import * as localforage from "localforage"; import * as Vue from "vue"; import { UI } from "./app.layout"; @@ -118,7 +118,7 @@ class AppController { let jsClassName = this.compilerService.ksySchema.meta.id.split("_").map((x: string) => x.ucFirst()).join(""); await workerMethods.initCode(debugCode, jsClassName, this.compilerService.ksyTypes); - let exportedRoot = await workerMethods.reparse(this.vm.disableLazyParsing); + const { result: exportedRoot, error: parseError } = await workerMethods.reparse(this.vm.disableLazyParsing); kaitaiIde.root = exportedRoot; //console.log("reparse exportedRoot", exportedRoot); @@ -142,7 +142,7 @@ class AppController { }); }); - this.errors.handle(null); + this.errors.handle(parseError); } catch(error) { this.errors.handle(error); } diff --git a/src/v1/app.worker.ts b/src/v1/app.worker.ts index c97b73be..109373d9 100644 --- a/src/v1/app.worker.ts +++ b/src/v1/app.worker.ts @@ -1,4 +1,4 @@ -var worker = new Worker("js/v1/kaitaiWorker.js"); +var worker = new Worker("js/v1/kaitaiWorker.js"); var msgHandlers: { [msgId: number]: (msg: IWorkerMessage) => void } = {}; @@ -10,16 +10,20 @@ worker.onmessage = (ev: MessageEvent) => { }; var lastMsgId = 0; -function workerCall(request: IWorkerMessage) { - return new Promise((resolve, reject) => { +function workerCall(request: IWorkerMessage): Promise { + return new Promise((resolve, reject) => { request.msgId = ++lastMsgId; msgHandlers[request.msgId] = response => { if (response.error) { console.log("error", response.error); + } + + if (response.error && (response.result === undefined || response.result === null)) { reject(response.error); + } else { + const { result, error } = response; + resolve({ result, error }); } - else - resolve(response.result); //console.info(`[performance] [${(new Date()).format("H:i:s.u")}] Got worker response: ${Date.now()}.`); }; @@ -29,15 +33,15 @@ function workerCall(request: IWorkerMessage) { export var workerMethods = { initCode: (sourceCode: string, mainClassName: string, ksyTypes: IKsyTypes) => { - return >workerCall({ type: "initCode", args: [sourceCode, mainClassName, ksyTypes] }); + return workerCall({ type: "initCode", args: [sourceCode, mainClassName, ksyTypes] }); }, setInput: (inputBuffer: ArrayBuffer) => { - return >workerCall({ type: "setInput", args: [inputBuffer] }); + return workerCall({ type: "setInput", args: [inputBuffer] }); }, reparse: (eagerMode: boolean) => { - return >workerCall({ type: "reparse", args: [eagerMode] }); + return workerCall({ type: "reparse", args: [eagerMode] }); }, get: (path: string[]) => { - return >workerCall({ type: "get", args: [path] }); + return workerCall({ type: "get", args: [path] }); } -}; \ No newline at end of file +}; diff --git a/src/v1/kaitaiWorker.ts b/src/v1/kaitaiWorker.ts index 40800ee7..27008e0c 100644 --- a/src/v1/kaitaiWorker.ts +++ b/src/v1/kaitaiWorker.ts @@ -130,12 +130,20 @@ var apiMethods = { //var start = performance.now(); wi.ioInput = new KaitaiStream(wi.inputBuffer, 0); wi.root = new wi.MainClass(wi.ioInput); - wi.root._read(); + let error; + try { + wi.root._read(); + } catch (e) { + error = e; + } if (hooks.nodeFilter) wi.root = hooks.nodeFilter(wi.root); wi.exported = exportValue(wi.root, { start: 0, end: wi.inputBuffer.byteLength }, [], eagerMode); //console.log("parse before return", performance.now() - start, "date", Date.now()); - return wi.exported; + return { + result: wi.exported, + error, + }; }, get: (path: string[]) => { var obj = wi.root; @@ -144,7 +152,9 @@ var apiMethods = { var debug = parent._debug["_m_" + path[path.length - 1]]; wi.exported = exportValue(obj, debug, path, false); // - return wi.exported; + return { + result: wi.exported, + }; } }; @@ -154,7 +164,12 @@ myself.onmessage = (ev: MessageEvent) => { if (apiMethods.hasOwnProperty(msg.type)) { try { - msg.result = apiMethods[msg.type].apply(self, msg.args); + const ret = apiMethods[msg.type].apply(self, msg.args); + if (ret) { + const { result, error } = ret; + msg.result = result; + msg.error = error; + } } catch (error) { console.log("[Worker] Error", error); msg.error = error; diff --git a/src/v1/parsedToTree.ts b/src/v1/parsedToTree.ts index 59407d1f..a3b4f151 100644 --- a/src/v1/parsedToTree.ts +++ b/src/v1/parsedToTree.ts @@ -297,7 +297,7 @@ export class ParsedTreeHandler { var expNode = isRoot ? this.exportedRoot : nodeData.exported; var isInstance = !expNode; - var valuePromise = isInstance ? this.getProp(nodeData.instance.path).then(exp => nodeData.exported = exp) : Promise.resolve(expNode); + var valuePromise = isInstance ? this.getProp(nodeData.instance.path).then(({ result: exp }) => nodeData.exported = exp) : Promise.resolve(expNode); return valuePromise.then(valueExp => { if (isRoot || isInstance) { this.fillKsyTypes(valueExp); From 797a672d6bd432794879c8b0ce925c3645aaf032 Mon Sep 17 00:00:00 2001 From: Petr Pucil Date: Tue, 20 Feb 2024 20:20:16 +0100 Subject: [PATCH 02/10] Display also primitive fields whose parsing failed --- src/v1/kaitaiWorker.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/v1/kaitaiWorker.ts b/src/v1/kaitaiWorker.ts index 27008e0c..826c4cab 100644 --- a/src/v1/kaitaiWorker.ts +++ b/src/v1/kaitaiWorker.ts @@ -90,9 +90,15 @@ function exportValue(obj: any, debug: IDebugInfo, path: string[], noLazy?: boole } result.object = { class: obj.constructor.name, instances: {}, fields: {} }; - var ksyType = wi.ksyTypes[result.object.class]; + const ksyType = wi.ksyTypes[result.object.class]; - Object.keys(obj).filter(x => x[0] !== "_").forEach(key => result.object.fields[key] = exportValue(obj[key], obj._debug && obj._debug[key], path.concat(key), noLazy)); + const fieldNames = new Set(Object.keys(obj)); + if (obj._debug) { + Object.keys(obj._debug).forEach(k => fieldNames.add(k)); + } + const fieldNamesArr = Array.from(fieldNames).filter(x => x[0] !== "_"); + fieldNamesArr + .forEach(key => result.object.fields[key] = exportValue(obj[key], obj._debug && obj._debug[key], path.concat(key), noLazy)); const propNames = obj.constructor !== Object ? Object.getOwnPropertyNames(obj.constructor.prototype).filter(x => x[0] !== "_" && x !== "constructor") : []; From 20a3ce029e3d9b465effedc19e8b69ed1f071157 Mon Sep 17 00:00:00 2001 From: Petr Pucil Date: Tue, 20 Feb 2024 17:02:44 +0100 Subject: [PATCH 03/10] Show yellow/red icons for incomplete fields in the tree --- css/app.css | 6 ++++-- src/entities.ts | 3 ++- src/v1/kaitaiWorker.ts | 16 +++++++++++++--- src/v1/parsedToTree.ts | 14 +++++++++++++- 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/css/app.css b/css/app.css index 9f105075..9c8e3ace 100644 --- a/css/app.css +++ b/css/app.css @@ -1,4 +1,4 @@ -/*.lm_content>div { height: 100%; width: 100% }*/ +/*.lm_content>div { height: 100%; width: 100% }*/ .lm_splitter.lm_horizontal .lm_drag_handle { left:-5px; width:15px } .lm_splitter.lm_vertical .lm_drag_handle { top:-5px; height:15px } .errorWindow { background: white; font-family: Courier,monospace; font-size: 12px; white-space: pre-wrap; overflow-y: scroll; } @@ -9,6 +9,8 @@ #parsedDataTree.jstree-default>.jstree-container-ul>.jstree-node { margin-left: 0 } #parsedDataTree.jstree-default .jstree-ocl { background-position-y: -8px; width:18px; } #parsedDataTree .missing { font-family: Arial; color: #c00000; } +.alert-color { color: #FFC20A; } +.fail-color { color: #FF4136; } #fileDrop { display:none; font-family: Arial; position: fixed; top: 0; left: 0; bottom: 0; right: 0; background: rgba(0,0,0,0.8); z-index: 9999 } #fileDrop>div { border:2px dashed white; border-radius:25px; width:500px; margin-left:-250px; height:180px; margin-top:-100px; position:fixed; top:50%; left:50%; color:white; text-align:center; font-size:22px; padding-top:65px } #fileTree { font-family:Arial; font-size:12px } @@ -55,4 +57,4 @@ body { color:#333 } #welcomeModalLabel { text-align:center } #welcomeModal .licenses { font-size:12px; margin-bottom:10px } #converterPanel { display:none } -/*.marker_match { background:rgba(80, 255, 80, 0.30); }*/ \ No newline at end of file +/*.marker_match { background:rgba(80, 255, 80, 0.30); }*/ diff --git a/src/entities.ts b/src/entities.ts index 077b2095..fb35be00 100644 --- a/src/entities.ts +++ b/src/entities.ts @@ -1,4 +1,4 @@ -class ObjectType { +class ObjectType { public static Primitive = "Primitive"; public static Array = "Array"; public static TypedArray = "TypedArray"; @@ -39,6 +39,7 @@ interface IExportedValue { ioOffset: number; start: number; end: number; + incomplete: boolean; primitiveValue?: any; arrayItems?: IExportedValue[]; diff --git a/src/v1/kaitaiWorker.ts b/src/v1/kaitaiWorker.ts index 826c4cab..4b8e3aa1 100644 --- a/src/v1/kaitaiWorker.ts +++ b/src/v1/kaitaiWorker.ts @@ -1,4 +1,4 @@ -// issue: https://github.com/Microsoft/TypeScript/issues/582 +// issue: https://github.com/Microsoft/TypeScript/issues/582 var myself = self; var wi = { @@ -18,6 +18,7 @@ interface IDebugInfo { start: number; end: number; ioOffset: number; + incomplete: boolean; arr?: IDebugInfo[]; enumName?: string; } @@ -36,9 +37,11 @@ function getObjectType(obj: any) { } function exportValue(obj: any, debug: IDebugInfo, path: string[], noLazy?: boolean): IExportedValue { - var result = { + adjustDebug(debug); + var result: IExportedValue = { start: debug && debug.start, end: debug && debug.end, + incomplete: debug && debug.incomplete, ioOffset: debug && debug.ioOffset, path: path, type: getObjectType(obj) @@ -120,6 +123,13 @@ function exportValue(obj: any, debug: IDebugInfo, path: string[], noLazy?: boole return result; } +function adjustDebug(debug: IDebugInfo): void { + if (!debug || Object.prototype.hasOwnProperty.call(debug, 'incomplete')) { + return; + } + debug.incomplete = (debug.start != null && debug.end == null); +} + importScripts("../entities.js"); importScripts("../../lib/_npm/kaitai-struct/KaitaiStream.js"); @@ -144,7 +154,7 @@ var apiMethods = { } if (hooks.nodeFilter) wi.root = hooks.nodeFilter(wi.root); - wi.exported = exportValue(wi.root, { start: 0, end: wi.inputBuffer.byteLength }, [], eagerMode); + wi.exported = exportValue(wi.root, { start: 0, end: wi.inputBuffer.byteLength, incomplete: error !== undefined }, [], eagerMode); //console.log("parse before return", performance.now() - start, "date", Date.now()); return { result: wi.exported, diff --git a/src/v1/parsedToTree.ts b/src/v1/parsedToTree.ts index a3b4f151..7a597996 100644 --- a/src/v1/parsedToTree.ts +++ b/src/v1/parsedToTree.ts @@ -1,4 +1,4 @@ -import { IInterval, IntervalHandler } from "../utils/IntervalHelper"; +import { IInterval, IntervalHandler } from "../utils/IntervalHelper"; import { s, htmlescape, asciiEncode, hexEncode, uuidEncode, collectAllObjects } from "../utils"; import { workerMethods } from "./app.worker"; import { app } from "./app"; @@ -222,6 +222,18 @@ export class ParsedTreeHandler { else text = (showProp ? s`${propName} = ` : "") + this.primitiveToText(item); + if (item.incomplete) { + const showAsError = + item.type === ObjectType.Undefined || + (item.type === ObjectType.Object && Object.keys(item.object.fields).length === 0); + + if (showAsError) { + text += ` `; + } else { + text += ` `; + } + } + return { text: text, children: isObject || isArray, data: this.addNodeData({ exported: item }) }; } From 9ba6653eeb428c274b7bcc9e418ed7aeb735b993 Mon Sep 17 00:00:00 2001 From: Petr Pucil Date: Tue, 20 Feb 2024 19:43:00 +0100 Subject: [PATCH 04/10] Remove broken unnecessary code for `ioOffset` adjustment The `if (result.start === childIoOffset) { ... }` code was intentionally removed, because it turned out to be broken. For example, consider the following .ksy snippet: ```ksy meta: id: subtype_in_substream seq: - id: foo type: u1 - id: sub size: 7 type: subtype types: subtype: seq: - id: bar type: u1 - id: body type: body_type body_type: seq: - id: qux size: 3 ``` If you select the `sub` field in the Web IDE's object tree, the highlighted interval in the hex dump is `[1, 8)` (from 1 inclusive to 8 exclusive). The interval of `sub.bar` is `[1, 2)`, as expected. But the highlighted interval for `sub.body` is `[1, 4)`, which is wrong - this would suggest that `sub.bar` and `sub.body` overlap, which is not the case. The correct interval for `sub.body` should the same as for `sub.body.qux`, which is `[2, 5)`. This incorrect highlighting is exactly the result of the `if (result.start === childIoOffset) { ... }` code, which confuses the `start` position of `body` of `1` with the start of a new substream, because `1` also happens to be the `byteOffset` of the current stream. I believe that removing this piece of code has no observable negative effect, because a project search shows that `ioOffset` is always used like `exp.ioOffset + exp.start`, so it doesn't seem to matter whether `ioOffset` and `start` (or `end`) have "correct" values individually, because in the end only their sum is used and interpreted. --- src/v1/kaitaiWorker.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/v1/kaitaiWorker.ts b/src/v1/kaitaiWorker.ts index 4b8e3aa1..bd791838 100644 --- a/src/v1/kaitaiWorker.ts +++ b/src/v1/kaitaiWorker.ts @@ -84,14 +84,6 @@ function exportValue(obj: any, debug: IDebugInfo, path: string[], noLazy?: boole result.arrayItems = (obj).map((item, i) => exportValue(item, debug && debug.arr && debug.arr[i], path.concat(i.toString()), noLazy)); } else if (result.type === ObjectType.Object) { - var childIoOffset = obj._io ? obj._io._byteOffset : 0; - - if (result.start === childIoOffset) { // new KaitaiStream was used, fix start position - result.ioOffset = childIoOffset; - result.start -= childIoOffset; - result.end -= childIoOffset; - } - result.object = { class: obj.constructor.name, instances: {}, fields: {} }; const ksyType = wi.ksyTypes[result.object.class]; @@ -154,7 +146,7 @@ var apiMethods = { } if (hooks.nodeFilter) wi.root = hooks.nodeFilter(wi.root); - wi.exported = exportValue(wi.root, { start: 0, end: wi.inputBuffer.byteLength, incomplete: error !== undefined }, [], eagerMode); + wi.exported = exportValue(wi.root, { start: 0, end: wi.inputBuffer.byteLength, ioOffset: 0, incomplete: error !== undefined }, [], eagerMode); //console.log("parse before return", performance.now() - start, "date", Date.now()); return { result: wi.exported, From 45f066e4ac0cd04018a2797f30c1fa21c64c5518 Mon Sep 17 00:00:00 2001 From: Petr Pucil Date: Tue, 20 Feb 2024 19:39:24 +0100 Subject: [PATCH 05/10] Infer missing `_debug[*].end` of user objects with substreams --- src/v1/kaitaiWorker.ts | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/v1/kaitaiWorker.ts b/src/v1/kaitaiWorker.ts index bd791838..fe728a8d 100644 --- a/src/v1/kaitaiWorker.ts +++ b/src/v1/kaitaiWorker.ts @@ -36,7 +36,7 @@ function getObjectType(obj: any) { return ObjectType.Object; } -function exportValue(obj: any, debug: IDebugInfo, path: string[], noLazy?: boolean): IExportedValue { +function exportValue(obj: any, debug: IDebugInfo, hasRawAttr: boolean, path: string[], noLazy: boolean): IExportedValue { adjustDebug(debug); var result: IExportedValue = { start: debug && debug.start, @@ -81,9 +81,14 @@ function exportValue(obj: any, debug: IDebugInfo, path: string[], noLazy?: boole } } else if (result.type === ObjectType.Array) { - result.arrayItems = (obj).map((item, i) => exportValue(item, debug && debug.arr && debug.arr[i], path.concat(i.toString()), noLazy)); + result.arrayItems = (obj).map((item, i) => exportValue(item, debug && debug.arr && debug.arr[i], hasRawAttr, path.concat(i.toString()), noLazy)); } else if (result.type === ObjectType.Object) { + const hasSubstream = hasRawAttr && obj._io; + if (result.incomplete && hasSubstream) { + debug.end = debug.start + obj._io.size; + result.end = debug.end; + } result.object = { class: obj.constructor.name, instances: {}, fields: {} }; const ksyType = wi.ksyTypes[result.object.class]; @@ -93,7 +98,7 @@ function exportValue(obj: any, debug: IDebugInfo, path: string[], noLazy?: boole } const fieldNamesArr = Array.from(fieldNames).filter(x => x[0] !== "_"); fieldNamesArr - .forEach(key => result.object.fields[key] = exportValue(obj[key], obj._debug && obj._debug[key], path.concat(key), noLazy)); + .forEach(key => result.object.fields[key] = exportValue(obj[key], obj._debug && obj._debug[key], fieldNames.has(`_raw_${key}`), path.concat(key), noLazy)); const propNames = obj.constructor !== Object ? Object.getOwnPropertyNames(obj.constructor.prototype).filter(x => x[0] !== "_" && x !== "constructor") : []; @@ -103,9 +108,10 @@ function exportValue(obj: any, debug: IDebugInfo, path: string[], noLazy?: boole const parseMode = ksyInstanceData["-webide-parse-mode"]; const eagerLoad = parseMode === "eager" || (parseMode !== "lazy" && ksyInstanceData.value); - if (eagerLoad || noLazy) - result.object.fields[propName] = exportValue(obj[propName], obj._debug["_m_" + propName], path.concat(propName), noLazy); - else + if (eagerLoad || noLazy) { + const instHasRawAttr = Object.prototype.hasOwnProperty.call(obj, `_raw__m_${propName}`); + result.object.fields[propName] = exportValue(obj[propName], obj._debug["_m_" + propName], instHasRawAttr, path.concat(propName), noLazy); + } else result.object.instances[propName] = { path: path.concat(propName), offset: 0 }; } } @@ -146,7 +152,7 @@ var apiMethods = { } if (hooks.nodeFilter) wi.root = hooks.nodeFilter(wi.root); - wi.exported = exportValue(wi.root, { start: 0, end: wi.inputBuffer.byteLength, ioOffset: 0, incomplete: error !== undefined }, [], eagerMode); + wi.exported = exportValue(wi.root, { start: 0, end: wi.inputBuffer.byteLength, ioOffset: 0, incomplete: error !== undefined }, false, [], eagerMode); //console.log("parse before return", performance.now() - start, "date", Date.now()); return { result: wi.exported, @@ -158,8 +164,9 @@ var apiMethods = { var parent: any = null; path.forEach(key => { parent = obj; obj = obj[key]; }); + const instHasRawAttr = Object.prototype.hasOwnProperty.call(obj, `_raw__m_${path[path.length - 1]}`); var debug = parent._debug["_m_" + path[path.length - 1]]; - wi.exported = exportValue(obj, debug, path, false); // + wi.exported = exportValue(obj, debug, instHasRawAttr, path, false); return { result: wi.exported, }; From e25812252a9fc30839b09fe291024baf5777a7d9 Mon Sep 17 00:00:00 2001 From: Petr Pucil Date: Tue, 20 Feb 2024 20:23:23 +0100 Subject: [PATCH 06/10] Infer `_debug[*].end` of arrays/user objects without substreams --- src/v1/kaitaiWorker.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/v1/kaitaiWorker.ts b/src/v1/kaitaiWorker.ts index fe728a8d..fe0fc3a7 100644 --- a/src/v1/kaitaiWorker.ts +++ b/src/v1/kaitaiWorker.ts @@ -82,6 +82,10 @@ function exportValue(obj: any, debug: IDebugInfo, hasRawAttr: boolean, path: str } else if (result.type === ObjectType.Array) { result.arrayItems = (obj).map((item, i) => exportValue(item, debug && debug.arr && debug.arr[i], hasRawAttr, path.concat(i.toString()), noLazy)); + if (result.incomplete && debug && debug.arr) { + debug.end = inferDebugEnd(debug.arr); + result.end = debug.end; + } } else if (result.type === ObjectType.Object) { const hasSubstream = hasRawAttr && obj._io; @@ -100,6 +104,11 @@ function exportValue(obj: any, debug: IDebugInfo, hasRawAttr: boolean, path: str fieldNamesArr .forEach(key => result.object.fields[key] = exportValue(obj[key], obj._debug && obj._debug[key], fieldNames.has(`_raw_${key}`), path.concat(key), noLazy)); + if (result.incomplete && !hasSubstream && obj._debug) { + debug.end = inferDebugEnd(fieldNamesArr.map(key => obj._debug[key])); + result.end = debug.end; + } + const propNames = obj.constructor !== Object ? Object.getOwnPropertyNames(obj.constructor.prototype).filter(x => x[0] !== "_" && x !== "constructor") : []; @@ -121,6 +130,15 @@ function exportValue(obj: any, debug: IDebugInfo, hasRawAttr: boolean, path: str return result; } +function inferDebugEnd(debugs: IDebugInfo[]): number { + const inferredEnd = debugs + .reduce((acc, debug) => debug && debug.end > acc ? debug.end : acc, Number.NEGATIVE_INFINITY); + if (inferredEnd === Number.NEGATIVE_INFINITY) { + return; + } + return inferredEnd; +} + function adjustDebug(debug: IDebugInfo): void { if (!debug || Object.prototype.hasOwnProperty.call(debug, 'incomplete')) { return; From d0a3d71a3f2ad0836e236902775827e66980c364 Mon Sep 17 00:00:00 2001 From: Petr Pucil Date: Tue, 20 Feb 2024 20:31:46 +0100 Subject: [PATCH 07/10] Display already parsed instances regardless of lazy/eager mode --- src/v1/kaitaiWorker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/v1/kaitaiWorker.ts b/src/v1/kaitaiWorker.ts index fe0fc3a7..aeef8304 100644 --- a/src/v1/kaitaiWorker.ts +++ b/src/v1/kaitaiWorker.ts @@ -117,7 +117,7 @@ function exportValue(obj: any, debug: IDebugInfo, hasRawAttr: boolean, path: str const parseMode = ksyInstanceData["-webide-parse-mode"]; const eagerLoad = parseMode === "eager" || (parseMode !== "lazy" && ksyInstanceData.value); - if (eagerLoad || noLazy) { + if (Object.prototype.hasOwnProperty.call(obj, `_m_${propName}`) || eagerLoad || noLazy) { const instHasRawAttr = Object.prototype.hasOwnProperty.call(obj, `_raw__m_${propName}`); result.object.fields[propName] = exportValue(obj[propName], obj._debug["_m_" + propName], instHasRawAttr, path.concat(propName), noLazy); } else From 02c885706ff19cecec572535f44e3fd24a8d5276 Mon Sep 17 00:00:00 2001 From: Petr Pucil Date: Tue, 20 Feb 2024 21:53:00 +0100 Subject: [PATCH 08/10] Show validation errors of fields (requires KSC support) Depends on https://github.com/kaitai-io/kaitai_struct_compiler/commit/b6756f75d5fc59b99c0419f9e10aaabe5e55a9f8 --- src/entities.ts | 1 + src/v1/kaitaiWorker.ts | 2 ++ src/v1/parsedToTree.ts | 24 ++++++++++++++++++++---- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/entities.ts b/src/entities.ts index fb35be00..daace1f3 100644 --- a/src/entities.ts +++ b/src/entities.ts @@ -40,6 +40,7 @@ interface IExportedValue { start: number; end: number; incomplete: boolean; + validationError?: Error; primitiveValue?: any; arrayItems?: IExportedValue[]; diff --git a/src/v1/kaitaiWorker.ts b/src/v1/kaitaiWorker.ts index aeef8304..1f3c0539 100644 --- a/src/v1/kaitaiWorker.ts +++ b/src/v1/kaitaiWorker.ts @@ -18,6 +18,7 @@ interface IDebugInfo { start: number; end: number; ioOffset: number; + validationError?: Error; incomplete: boolean; arr?: IDebugInfo[]; enumName?: string; @@ -42,6 +43,7 @@ function exportValue(obj: any, debug: IDebugInfo, hasRawAttr: boolean, path: str start: debug && debug.start, end: debug && debug.end, incomplete: debug && debug.incomplete, + validationError: (debug && debug.validationError) || undefined, ioOffset: debug && debug.ioOffset, path: path, type: getObjectType(obj) diff --git a/src/v1/parsedToTree.ts b/src/v1/parsedToTree.ts index 7a597996..51a80fc2 100644 --- a/src/v1/parsedToTree.ts +++ b/src/v1/parsedToTree.ts @@ -222,16 +222,32 @@ export class ParsedTreeHandler { else text = (showProp ? s`${propName} = ` : "") + this.primitiveToText(item); - if (item.incomplete) { + if (item.incomplete || item.validationError !== undefined) { + const validationError = item.validationError !== undefined ? + `${item.validationError.name}: ${item.validationError.message}` : + undefined; + const showAsError = + validationError !== undefined || item.type === ObjectType.Undefined || (item.type === ObjectType.Object && Object.keys(item.object.fields).length === 0); - if (showAsError) { - text += ` `; + const icon = document.createElement('i'); + icon.classList.add('glyphicon'); + icon.classList.add(showAsError ? 'fail-color' : 'alert-color'); + + if (validationError !== undefined) { + icon.classList.add('glyphicon-remove'); + icon.title = `validation of this field failed with "${validationError}"`; + } else if (showAsError) { + icon.classList.add('glyphicon-exclamation-sign'); + icon.title = `parsing of this field failed`; } else { - text += ` `; + icon.classList.add('glyphicon-alert'); + icon.title = `parsing was interrupted by an error, data may be incomplete`; } + + text += ` ${icon.outerHTML}`; } return { text: text, children: isObject || isArray, data: this.addNodeData({ exported: item }) }; From ba2e7795a054345eb901b75078d9e266a469589b Mon Sep 17 00:00:00 2001 From: Petr Pucil Date: Tue, 20 Feb 2024 23:23:58 +0100 Subject: [PATCH 09/10] Show errors caught from explicitly invoked instances --- css/app.css | 1 + src/entities.ts | 1 + src/v1/kaitaiWorker.ts | 43 +++++++++++++++++++++++++++++++----------- src/v1/parsedToTree.ts | 27 ++++++++++++++++++++------ 4 files changed, 55 insertions(+), 17 deletions(-) diff --git a/css/app.css b/css/app.css index 9c8e3ace..1aeb88a1 100644 --- a/css/app.css +++ b/css/app.css @@ -11,6 +11,7 @@ #parsedDataTree .missing { font-family: Arial; color: #c00000; } .alert-color { color: #FFC20A; } .fail-color { color: #FF4136; } +.instance-fail-color { color: #0074D9; } #fileDrop { display:none; font-family: Arial; position: fixed; top: 0; left: 0; bottom: 0; right: 0; background: rgba(0,0,0,0.8); z-index: 9999 } #fileDrop>div { border:2px dashed white; border-radius:25px; width:500px; margin-left:-250px; height:180px; margin-top:-100px; position:fixed; top:50%; left:50%; color:white; text-align:center; font-size:22px; padding-top:65px } #fileTree { font-family:Arial; font-size:12px } diff --git a/src/entities.ts b/src/entities.ts index daace1f3..4329dbf2 100644 --- a/src/entities.ts +++ b/src/entities.ts @@ -41,6 +41,7 @@ interface IExportedValue { end: number; incomplete: boolean; validationError?: Error; + instanceError?: Error; primitiveValue?: any; arrayItems?: IExportedValue[]; diff --git a/src/v1/kaitaiWorker.ts b/src/v1/kaitaiWorker.ts index 1f3c0539..5cfec151 100644 --- a/src/v1/kaitaiWorker.ts +++ b/src/v1/kaitaiWorker.ts @@ -119,10 +119,9 @@ function exportValue(obj: any, debug: IDebugInfo, hasRawAttr: boolean, path: str const parseMode = ksyInstanceData["-webide-parse-mode"]; const eagerLoad = parseMode === "eager" || (parseMode !== "lazy" && ksyInstanceData.value); - if (Object.prototype.hasOwnProperty.call(obj, `_m_${propName}`) || eagerLoad || noLazy) { - const instHasRawAttr = Object.prototype.hasOwnProperty.call(obj, `_raw__m_${propName}`); - result.object.fields[propName] = exportValue(obj[propName], obj._debug["_m_" + propName], instHasRawAttr, path.concat(propName), noLazy); - } else + if (Object.prototype.hasOwnProperty.call(obj, `_m_${propName}`) || eagerLoad || noLazy) + result.object.fields[propName] = fetchInstance(obj, propName, path, noLazy); + else result.object.instances[propName] = { path: path.concat(propName), offset: 0 }; } } @@ -141,6 +140,30 @@ function inferDebugEnd(debugs: IDebugInfo[]): number { return inferredEnd; } +function fetchInstance(obj: any, propName: string, objPath: string[], noLazy: boolean): IExportedValue { + let value; + let instanceError: Error; + try { + value = obj[propName]; + } catch (e) { + instanceError = e; + } + if (instanceError !== undefined) { + try { + // retry once (important for validation errors) + value = obj[propName]; + } catch (e) {} + } + + const instHasRawAttr = Object.prototype.hasOwnProperty.call(obj, `_raw__m_${propName}`); + const debugInfo = obj._debug[`_m_${propName}`]; + const exported = exportValue(value, debugInfo, instHasRawAttr, objPath.concat(propName), noLazy); + if (instanceError !== undefined) { + exported.instanceError = instanceError; + } + return exported; +} + function adjustDebug(debug: IDebugInfo): void { if (!debug || Object.prototype.hasOwnProperty.call(debug, 'incomplete')) { return; @@ -180,15 +203,13 @@ var apiMethods = { }; }, get: (path: string[]) => { - var obj = wi.root; - var parent: any = null; - path.forEach(key => { parent = obj; obj = obj[key]; }); + let parent = wi.root; + const parentPath = path.slice(0, -1); + parentPath.forEach(key => parent = parent[key]); + const propName = path[path.length - 1]; - const instHasRawAttr = Object.prototype.hasOwnProperty.call(obj, `_raw__m_${path[path.length - 1]}`); - var debug = parent._debug["_m_" + path[path.length - 1]]; - wi.exported = exportValue(obj, debug, instHasRawAttr, path, false); return { - result: wi.exported, + result: fetchInstance(parent, propName, parentPath, false), }; } }; diff --git a/src/v1/parsedToTree.ts b/src/v1/parsedToTree.ts index 51a80fc2..5fcfe70f 100644 --- a/src/v1/parsedToTree.ts +++ b/src/v1/parsedToTree.ts @@ -222,10 +222,13 @@ export class ParsedTreeHandler { else text = (showProp ? s`${propName} = ` : "") + this.primitiveToText(item); - if (item.incomplete || item.validationError !== undefined) { + if (item.incomplete || item.validationError !== undefined || item.instanceError !== undefined) { const validationError = item.validationError !== undefined ? `${item.validationError.name}: ${item.validationError.message}` : undefined; + const instanceError = item.instanceError !== undefined ? + `${item.instanceError.name}: ${item.instanceError.message}` : + undefined; const showAsError = validationError !== undefined || @@ -234,17 +237,29 @@ export class ParsedTreeHandler { const icon = document.createElement('i'); icon.classList.add('glyphicon'); - icon.classList.add(showAsError ? 'fail-color' : 'alert-color'); + if (instanceError !== undefined) { + icon.classList.add('instance-fail-color'); + } else { + icon.classList.add(showAsError ? 'fail-color' : 'alert-color'); + } - if (validationError !== undefined) { + if (validationError !== undefined && (instanceError === undefined || item.instanceError === item.validationError)) { icon.classList.add('glyphicon-remove'); - icon.title = `validation of this field failed with "${validationError}"`; + const action = instanceError !== undefined ? + "validation of this instance parsed on explicit request" : + "validation of this field"; + icon.title = `${action} failed with "${validationError}"`; } else if (showAsError) { icon.classList.add('glyphicon-exclamation-sign'); - icon.title = `parsing of this field failed`; + if (instanceError !== undefined) { + icon.title = `explicit parsing of this instance failed with "${instanceError}"`; + } else { + icon.title = `parsing of this field failed`; + } } else { icon.classList.add('glyphicon-alert'); - icon.title = `parsing was interrupted by an error, data may be incomplete`; + const instanceAppendix = instanceError !== undefined ? "explicit " : ""; + icon.title = `${instanceAppendix}parsing was interrupted by an error, data may be incomplete`; } text += ` ${icon.outerHTML}`; From 7121a7b0313bb5121182407d63bae456a913f655 Mon Sep 17 00:00:00 2001 From: Petr Pucil Date: Tue, 20 Feb 2024 23:29:31 +0100 Subject: [PATCH 10/10] Show full error when selecting an instance or invalid field --- src/v1/app.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/v1/app.ts b/src/v1/app.ts index 9419f8ef..fcc6ac73 100644 --- a/src/v1/app.ts +++ b/src/v1/app.ts @@ -126,13 +126,21 @@ class AppController { this.ui.parsedDataTreeHandler.jstree.on("state_ready.jstree", () => { this.ui.parsedDataTreeHandler.jstree.on("select_node.jstree", (e, selectNodeArgs) => { - var node = selectNodeArgs.node; + const node = selectNodeArgs.node; //console.log("node", node); - var exp = this.ui.parsedDataTreeHandler.getNodeData(node).exported; + const exp = this.ui.parsedDataTreeHandler.getNodeData(node).exported; if (exp && exp.path) $("#parsedPath").text(exp.path.join("/")); + if (exp) { + if (exp.instanceError !== undefined) { + app.errors.handle(exp.instanceError); + } else if (exp.validationError !== undefined) { + app.errors.handle(exp.validationError); + } + } + if (!this.blockRecursive && exp && exp.start < exp.end) { this.selectedInTree = true; //console.log("setSelection", exp.ioOffset, exp.start);