From 27e1e66da52807231c06708ee0b27a500ea8901d Mon Sep 17 00:00:00 2001 From: SheetJS Date: Tue, 25 Jul 2023 20:12:12 -0400 Subject: [PATCH] README --- README.md | 88 ++++++++++++++++++++++++++++++++++++++++++++++++---- package.json | 2 +- src/App.tsx | 24 ++++++++------ 3 files changed, 97 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index b519696..3f86e58 100644 --- a/README.md +++ b/README.md @@ -4,19 +4,62 @@ source for `iwa-inspector` is a tool for inspecting iWork archives. + covers the high-level structure of files. +This inspector performs the top-level extraction of messages and parses using +extracted Protocol Buffer definitions. + ## Usage -When a file is loaded, a table will display the messages in the file. (The site -automatically fetches a sample file on load) +The sections are separated by a light gray horizontal line and a light gray +vertical line. Panels can be resized by click-dragging the line. -When a message is selected, the page will display the Protocol Buffers -definition for the message as well as an inspector for the message and metadata. +### Selecting a File + +The file input element in the top-left corner of the page is limited to the IWA +file types: `.numbers`, `.key`, and `.pages`. + +The site automatically fetches a sample file on load. + +### Message Table + +The message table is shown just below the header bar. The column headers are: + +| name | description | +|:----------|:----------------------| +| `id` | Message ID | +| `type` | Numeric Message Type | +| `message` | Absolute Message Type | +| `path` | Location of Message | + +The table can be sorted by clicking on the column headers. + +### Searching for Messages + +The search text box in the top-right corner of the page is a plaintext search +over the parsed messages. Searches will match field names, string values and +UUID fields of message type `.TSP.UUID`. + +### Selecting a Message + +When a row in the table is selected, the bottom-left panel will display the +Protocol Buffers definition for the message and the bottom-right panel will +display the parsed contents. + +### Message Structure + +The bottom-right panel shows the following information: + +- "Message": parsed information following the message definition +- "Metadata": parsed information from the message metadata +- "Dependents": list of messages that list the current message as a dependency. Clicking on a message name in the inspector will show the message definition in the left pane. A "Return" link returns to the base message definition. Clicking on a `.TSP.Reference` ID will jump to the referenced message. +### Exporting Data + Right-clicking a custom message type will show a context menu with options to copy the raw byte representation (array of numbers) or parsed object (JSON). @@ -26,8 +69,41 @@ copy the raw byte representation (array of numbers) or parsed object (JSON). `make build` generates the static site. -### Refreshing Protos and Messages +## Refreshing Protos and Messages `make deps` requires a SIP-disabled Intel Mac with Keynote + Numbers + Pages. -The last run was on 2023-06-26 against version 13.1 \ No newline at end of file +The last run was on 2023-06-26 against version 13.1 + +## Protocol Buffer Details + +Note that the `message` field is absolute. For example, `TSTArchives.proto` +specifies the `.TST.TileStorage` as follows: + +```proto +package TST; + +message TileStorage { + message Tile { + required uint32 tileid = 1; + required .TSP.Reference tile = 2; + } + repeated .TST.TileStorage.Tile tiles = 1; + optional uint32 tile_size = 2; + optional bool should_use_wide_rows = 3; +} +``` + +The protobuf extractor rewrites the message names using the absolute form: + +```proto +message .TST.TileStorage { + message Tile { + required uint32 tileid = 1; + required .TSP.Reference tile = 2; + } + repeated .TST.TileStorage.Tile tiles = 1; + optional uint32 tile_size = 2; + optional bool should_use_wide_rows = 3; +} +``` diff --git a/package.json b/package.json index 0c3c18f..9709338 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "react-inspector": "6.0.1", "react-resizable-panels": "0.0.45", "react-toastify": "9.1.3", - "xlsx": "https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz" + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.0/xlsx-0.20.0.tgz" }, "devDependencies": { "@types/react": "18.2.6", diff --git a/src/App.tsx b/src/App.tsx index 8903c6b..fe0241a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -165,7 +165,7 @@ const preparse = (id: any, message: string, f: ParsedFile, p: ProtoMap) => { /* .TSP.MessageInfo */ if(m.object_references) m.$object_references = m.object_references.map((n: BigInt) => { /* create a fake reference for the inspector */ - var o: $_TSP_Reference = ({ identifier: n }); + const o: $_TSP_Reference = ({ identifier: n }); Object.defineProperty(o, "PB_TYPE", {value: ".TSP.Reference", enumerable: false}); return o; }); @@ -173,7 +173,7 @@ const preparse = (id: any, message: string, f: ParsedFile, p: ProtoMap) => { /* .TSP.FieldInfo */ if(fi.object_references) fi.$object_references = fi.object_references.map((n: BigInt) => { /* create a fake reference for the inspector */ - var o: $_TSP_Reference = ({ identifier: n }); + const o: $_TSP_Reference = ({ identifier: n }); Object.defineProperty(o, "PB_TYPE", {value: ".TSP.Reference", enumerable: false}); return o; }); @@ -224,14 +224,15 @@ function App() { /* scroll to selected row */ const tblScroll = (R: number) => { if(R == -1) return; - var rowelt = document.getElementById(`tr-${R}`); - var top = rowelt?.offsetTop || 0; + const rowelt = document.getElementById(`tr-${R}`); + const top = rowelt?.offsetTop || 0; if(tblRef.current) { let tbl = tblRef.current; if(top > tbl.scrollTop + tbl.clientHeight - (rowelt?.clientHeight||0) || top < tbl.scrollTop + (rowelt?.clientHeight||0)) tbl.scrollTop = Math.max(0, top - tbl.clientHeight/2 - (rowelt?.clientHeight||0)/2); } }; + /* Sorting machinations */ const onsort = (s: string) => { let d = false; console.log(sort == s, desc); @@ -242,7 +243,6 @@ function App() { } file.tbl.sort((x: any, y: any) => (typeof x[s] == "number" ? x[s] - y[s] : String(x[s]).localeCompare(String(y[s]))) * (d ? -1 : 1)); }; - useEffect(() => { if(id) tblScroll(file.tbl.findIndex(row => row["id"] == +id)); }, [sort, desc]); @@ -266,7 +266,7 @@ function App() { setMeta(meta); setDeps(file.tbl.filter(r => ((file.space[+r.id][0].parsedmeta?.object_references||[])?.indexOf(BigInt(row.id)) > -1)).map(v => { /* this copy uses non-enumerable fields */ - var o = {}; Object.entries(v).forEach(([k,v])=> Object.defineProperty(o, k, { enumerable: false, value: v })); return o; + const o = {}; Object.entries(v).forEach(([k,v])=> Object.defineProperty(o, k, { enumerable: false, value: v })); return o; })); setId(String(row.id)); tblScroll(R); @@ -275,7 +275,7 @@ function App() { const rowclick = (row: any, R: number) => { doitRow(row, R, true); }; /* helper for .TSP.Reference */ const gotoRef = (id: string) => { - var R = file.tbl.findIndex(t => +t.id == +id); + const R = file.tbl.findIndex(t => +t.id == +id); if (R == -1) throw new Error(`Message ${id} not found`); doitRow(file.tbl[R], R, false); }; @@ -285,7 +285,7 @@ function App() { const pop = () => { const oldId = stack.pop()||"0"; setStack([...stack]); - var R = file.tbl.findIndex(t => +t.id == +oldId); + const R = file.tbl.findIndex(t => +t.id == +oldId); if (R == -1) throw new Error(`Message ${oldId} not found`); doitRow(file.tbl[R], R); }; @@ -357,7 +357,7 @@ function App() { } function onClickId(){ gotoRef(menuId); } function onClickCopyByteArray({ props }: {props: MenuProps}){ - var _data = props?.data?.PB_RAW?.data || (+props.id == +id) && file.space[+id][0].data; + const _data = props?.data?.PB_RAW?.data || (+props.id == +id) && file.space[+id][0].data; if(!_data) throw new Error("Could not find raw data"); navigator.clipboard.writeText("[" + [..._data].map(x => "0x" + x.toString(16).toUpperCase().padStart(2,"0")).join(", ") + "]"); } @@ -367,6 +367,8 @@ function App() { } function showProtoDef({props}: {props: MenuProps}) { selectProto(props.type); } function showXXD({props}: {props: MenuProps}) { console.log(xxd(props.data)); } + + /* Inspector machinations */ type NodeRendererProps = { depth: number; name: string; @@ -438,11 +440,13 @@ function App() { return ( gotoRef(data.id)}> ); }; + const showHelp = () => { toast.info(Click for more info); } + return ( <> {/* header */}