This commit is contained in:
SheetJS 2023-07-25 20:12:12 -04:00
parent c76b5be2a9
commit 27e1e66da5
3 changed files with 97 additions and 17 deletions

@ -4,19 +4,62 @@ source for <https://sheetjs.com/tools/iwa-inspector>
`iwa-inspector` is a tool for inspecting iWork archives.
<https://oss.sheetjs.com/notes/iwa/> 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
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;
}
```

@ -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",

@ -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 ( <a onClick={()=>gotoRef(data.id)}><ObjectLabel name={data.id} data={data.message} isNonenumerable={isNonenumerable} /></a> );
};
const showHelp = () => { toast.info(<a href="https://git.sheetjs.com/sheetjs/iwa-inspector" target="_blank">Click for more info</a>); }
return ( <>
{/* header */}
<div id="header" className="header" style={{display: "grid", gridTemplateColumns: "repeat(3, 1fr)", alignItems:"center"}}>
<div style={{display: "flex", justifyContent: "flex-start"}}><input type="file" id="file" onChange={onChange} disabled={loading} accept=".numbers,.key,.pages"/></div>
<div><b><a href="https://sheetjs.com">SheetJS</a> IWA Inspector</b></div>
<div><b><a href="https://sheetjs.com">SheetJS</a> IWA Inspector</b> <b onClick={showHelp}>(?)</b></div>
<div style={{display: "flex", justifyContent: "flex-end"}}><input type="text" value={search} placeholder="search for text" onChange={(e)=>setSearch(e.target.value)}/></div>
</div>