sort message table
This commit is contained in:
		
							parent
							
								
									a087c06552
								
							
						
					
					
						commit
						c76b5be2a9
					
				
							
								
								
									
										14
									
								
								README.md
									
									
									
									
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										14
									
								
								README.md
									
									
									
									
									
								
							@ -4,11 +4,17 @@ source for <https://sheetjs.com/tools/iwa-inspector>
 | 
			
		||||
 | 
			
		||||
`iwa-inspector` is a tool for inspecting iWork archives.
 | 
			
		||||
 | 
			
		||||
When a file is loaded, a table will display the messages in the file.
 | 
			
		||||
## Usage
 | 
			
		||||
 | 
			
		||||
When a file is loaded, a table will display the messages in the file. (The site
 | 
			
		||||
automatically fetches a sample file on load)
 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
 | 
			
		||||
Right-clicking a custom message type will show a context menu with options to
 | 
			
		||||
@ -20,6 +26,8 @@ copy the raw byte representation (array of numbers) or parsed object (JSON).
 | 
			
		||||
 | 
			
		||||
`make build` generates the static site.
 | 
			
		||||
 | 
			
		||||
## Refreshing data
 | 
			
		||||
### Refreshing Protos and Messages
 | 
			
		||||
 | 
			
		||||
`make deps` requires a SIP-disabled Intel Mac. The last run used v13.0 apps.
 | 
			
		||||
`make deps` requires a SIP-disabled Intel Mac with Keynote + Numbers + Pages.
 | 
			
		||||
 | 
			
		||||
The last run was on 2023-06-26 against version 13.1
 | 
			
		||||
							
								
								
									
										10892
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										10892
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -35,7 +35,7 @@
 | 
			
		||||
    "eslint-plugin-react-refresh": "0.3.5",
 | 
			
		||||
    "patch-package": "7.0.0",
 | 
			
		||||
    "typescript": "5.0.4",
 | 
			
		||||
    "vite": "4.3.5",
 | 
			
		||||
    "vite": "4.4.7",
 | 
			
		||||
    "vite-plugin-pwa": "0.14.7"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										138
									
								
								src/App.tsx
									
									
									
									
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										138
									
								
								src/App.tsx
									
									
									
									
									
								
							@ -1,9 +1,9 @@
 | 
			
		||||
/* TODO:
 | 
			
		||||
  - history
 | 
			
		||||
  - find example and correctly handle "merge" messages
 | 
			
		||||
  - sort and filter table
 | 
			
		||||
  - filter table by message type or path
 | 
			
		||||
  - loading icons
 | 
			
		||||
  - expand referenced object in place
 | 
			
		||||
  - expand referenced object in place or accordian
 | 
			
		||||
  - paste bytes -> analyze
 | 
			
		||||
  - edit fields / files?
 | 
			
		||||
  - different menu for message / enum / extend / literal
 | 
			
		||||
@ -24,8 +24,6 @@ import 'react-contexify/dist/ReactContexify.css';
 | 
			
		||||
import { ToastContainer, toast } from 'react-toastify';
 | 
			
		||||
import 'react-toastify/dist/ReactToastify.css';
 | 
			
		||||
 | 
			
		||||
//#region Xxd
 | 
			
		||||
 | 
			
		||||
const uuid2str = (l: bigint, u: bigint): string => [
 | 
			
		||||
  ((l >>  0n) & 0xFFn).toString(16).padStart(2, "0"),
 | 
			
		||||
  ((l >>  8n) & 0xFFn).toString(16).padStart(2, "0"),
 | 
			
		||||
@ -48,8 +46,11 @@ const uuid2str = (l: bigint, u: bigint): string => [
 | 
			
		||||
  ((u >> 48n) & 0xFFn).toString(16).padStart(2, "0"),
 | 
			
		||||
  ((u >> 56n) & 0xFFn).toString(16).padStart(2, "0"),
 | 
			
		||||
].join("").toUpperCase();
 | 
			
		||||
//import { vsprintf } from 'printj';
 | 
			
		||||
/*const X = "%02hhx", Y = X + X + " ";
 | 
			
		||||
 | 
			
		||||
//#region Xxd
 | 
			
		||||
 | 
			
		||||
import { vsprintf } from 'printj';
 | 
			
		||||
const X = "%02hhx", Y = X + X + " ";
 | 
			
		||||
const FMT = [...Array.from({length:16}).map((_,i) =>
 | 
			
		||||
  Y.repeat(i>>1) + (i%2 ? X:"  ") + "   " + "     ".repeat(7 - (i >> 1)) + "|" + "%c".repeat(i) + " ".repeat(16-i) + "|\n"
 | 
			
		||||
), Y.repeat(8) + "|" + "%c".repeat(16) + "|\n"];
 | 
			
		||||
@ -63,7 +64,7 @@ const xxd = (u8: Uint8Array): string => {
 | 
			
		||||
  return out.join("");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type XxdProps = {
 | 
			
		||||
/*type XxdProps = {
 | 
			
		||||
  data?: Uint8Array;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -80,17 +81,20 @@ type TableViewProps = {
 | 
			
		||||
  id?: string;
 | 
			
		||||
  data: any[];
 | 
			
		||||
  cols: string[];
 | 
			
		||||
  sort?: string;
 | 
			
		||||
  desc?: boolean;
 | 
			
		||||
  filter?: (row: any, R: number) => boolean;
 | 
			
		||||
  rowclick?: (row: any, R: number, e: ReactMouseEvent<HTMLTableRowElement, MouseEvent>) => void;
 | 
			
		||||
  cellclick?: (value: any, R: number, C: number, e: ReactMouseEvent<HTMLTableCellElement, MouseEvent>) => void;
 | 
			
		||||
  onsort?: (sort: string) => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function TableView({id, data, cols, filter, rowclick, cellclick}: TableViewProps) {
 | 
			
		||||
function TableView({id, data, cols, filter, rowclick, cellclick, sort, desc, onsort}: TableViewProps) {
 | 
			
		||||
  return (
 | 
			
		||||
    <table>
 | 
			
		||||
      <thead>
 | 
			
		||||
        <tr>{cols.map((c,idx) => (
 | 
			
		||||
          <th key={idx}>{c}</th>
 | 
			
		||||
          <th key={idx} onClick={()=>{ if(onsort) onsort(c); }}>{c} {sort == c ? (desc ? "\u25BC" : "\u25B2") : ""}</th>
 | 
			
		||||
        ))}</tr>
 | 
			
		||||
      </thead>
 | 
			
		||||
      <tbody>{data.filter(filter || (()=>true)).map((row, R) => (
 | 
			
		||||
@ -122,8 +126,9 @@ interface ContextMenuProps {
 | 
			
		||||
  onClickCopyByteArray?: ({props}: any)=>void;
 | 
			
		||||
  onClickCopyJSON: ({props}: any)=>void;
 | 
			
		||||
  showProtoDef: ({props}: any)=>void;
 | 
			
		||||
  showXXD: ({props}: any) => void;
 | 
			
		||||
}
 | 
			
		||||
const ContextMenu = ({ID, menuType, menuField, menuId, onClickId, onClickCopyByteArray, onClickCopyJSON, showProtoDef}: ContextMenuProps) => (
 | 
			
		||||
const ContextMenu = ({ID, menuType, menuField, menuId, onClickId, onClickCopyByteArray, onClickCopyJSON, showProtoDef, showXXD}: ContextMenuProps) => (
 | 
			
		||||
  <Menu id={ID}>
 | 
			
		||||
    {menuField && (<Item disabled><b>{menuField}</b></Item>)}
 | 
			
		||||
    <Item disabled><b>{menuType}</b></Item>
 | 
			
		||||
@ -131,11 +136,14 @@ const ContextMenu = ({ID, menuType, menuField, menuId, onClickId, onClickCopyByt
 | 
			
		||||
    <Separator />
 | 
			
		||||
    <Item onClick={onClickCopyByteArray}>Copy byte array</Item>
 | 
			
		||||
    <Item onClick={onClickCopyJSON}>Copy JSON</Item>
 | 
			
		||||
    <Item onClick={showProtoDef}>Show Definition</Item>
 | 
			
		||||
    <Item hidden={()=>menuType == "bytes"} onClick={showProtoDef}>Show Definition</Item>
 | 
			
		||||
    <Item hidden={()=>menuType != "bytes"} onClick={showXXD}>Dump XXD to console</Item>
 | 
			
		||||
  </Menu> );
 | 
			
		||||
 | 
			
		||||
//#endregion
 | 
			
		||||
 | 
			
		||||
//#region preparse
 | 
			
		||||
 | 
			
		||||
const replacer = (_: string, v: any): any => {
 | 
			
		||||
  switch(true) {
 | 
			
		||||
    case (typeof v == "bigint"): return v.toString();
 | 
			
		||||
@ -145,6 +153,36 @@ const replacer = (_: string, v: any): any => {
 | 
			
		||||
  return v;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/* parse if message has not been parsed */
 | 
			
		||||
const preparse = (id: any, message: string, f: ParsedFile, p: ProtoMap) => {
 | 
			
		||||
  const item = f.space[+id][0];
 | 
			
		||||
  if(!item.parsed) {
 | 
			
		||||
    item.parsed = process(item.data, message, p);
 | 
			
		||||
    item.pre = JSON.stringify(item.parsed, replacer);
 | 
			
		||||
  }
 | 
			
		||||
  if(!item.parsedmeta) {
 | 
			
		||||
    const m: $_TSP_MessageInfo = item.parsedmeta = process(item.rawmeta, ".TSP.MessageInfo", p);
 | 
			
		||||
    /* .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 });
 | 
			
		||||
      Object.defineProperty(o, "PB_TYPE", {value: ".TSP.Reference", enumerable: false});
 | 
			
		||||
      return o;
 | 
			
		||||
    });
 | 
			
		||||
    if(m.field_infos) m.field_infos.forEach((fi) => {
 | 
			
		||||
      /* .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 });
 | 
			
		||||
        Object.defineProperty(o, "PB_TYPE", {value: ".TSP.Reference", enumerable: false});
 | 
			
		||||
        return o;
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
//#endregion
 | 
			
		||||
 | 
			
		||||
function App() {
 | 
			
		||||
  /* selected message ID */
 | 
			
		||||
  const [id, setId] = useState<string>("0");
 | 
			
		||||
@ -179,39 +217,41 @@ function App() {
 | 
			
		||||
  const [menuType, setMenuType] = useState<string>("");
 | 
			
		||||
  const [menuId, setMenuId] = useState<string>("");
 | 
			
		||||
  const tblRef = useRef<HTMLDivElement>(null);
 | 
			
		||||
  /* sorting */
 | 
			
		||||
  const [ sort, setSort ] = useState<string>("");
 | 
			
		||||
  const [ desc, setDesc ] = useState<boolean>(false);
 | 
			
		||||
 | 
			
		||||
  /* parse if message has not been parsed */
 | 
			
		||||
  const preparse = (id: any, message: string, f: ParsedFile = file, p: ProtoMap = protos) => {
 | 
			
		||||
    const item = f.space[+id][0];
 | 
			
		||||
    if(!item.parsed) {
 | 
			
		||||
      item.parsed = process(item.data, message, p);
 | 
			
		||||
      item.pre = JSON.stringify(item.parsed, replacer);
 | 
			
		||||
    }
 | 
			
		||||
    if(!item.parsedmeta) {
 | 
			
		||||
      const m: $_TSP_MessageInfo = item.parsedmeta = process(item.rawmeta, ".TSP.MessageInfo", p);
 | 
			
		||||
      /* .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 });
 | 
			
		||||
        Object.defineProperty(o, "PB_TYPE", {value: ".TSP.Reference", enumerable: false});
 | 
			
		||||
        return o;
 | 
			
		||||
      });
 | 
			
		||||
      if(m.field_infos) m.field_infos.forEach((fi) => {
 | 
			
		||||
        /* .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 });
 | 
			
		||||
          Object.defineProperty(o, "PB_TYPE", {value: ".TSP.Reference", enumerable: false});
 | 
			
		||||
          return o;
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
  /* scroll to selected row */
 | 
			
		||||
  const tblScroll = (R: number) => {
 | 
			
		||||
    if(R == -1) return;
 | 
			
		||||
    var rowelt = document.getElementById(`tr-${R}`);
 | 
			
		||||
    var 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);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const onsort = (s: string) => {
 | 
			
		||||
    let d = false;
 | 
			
		||||
    console.log(sort == s, desc);
 | 
			
		||||
    if(sort == s) setDesc(d = !desc);
 | 
			
		||||
    else {
 | 
			
		||||
      setDesc(d = false);
 | 
			
		||||
      setSort(s);
 | 
			
		||||
    }
 | 
			
		||||
    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]);
 | 
			
		||||
 | 
			
		||||
  /* update selection based on table row */
 | 
			
		||||
  const doitRow = (row: TableItem, R: number, reset?: boolean) => {
 | 
			
		||||
    let obj: any, meta: $_TSP_MessageInfo;
 | 
			
		||||
    try {
 | 
			
		||||
      preparse(row.id, row.message);
 | 
			
		||||
      preparse(row.id, row.message, file, protos);
 | 
			
		||||
      const item = file.space[+row.id][0];
 | 
			
		||||
      obj = item.parsed;
 | 
			
		||||
      meta = item.parsedmeta as $_TSP_MessageInfo;
 | 
			
		||||
@ -229,12 +269,7 @@ function App() {
 | 
			
		||||
      var o = {}; Object.entries(v).forEach(([k,v])=> Object.defineProperty(o, k, { enumerable: false,  value: v })); return o;
 | 
			
		||||
    }));
 | 
			
		||||
    setId(String(row.id));
 | 
			
		||||
    var rowelt = document.getElementById(`tr-${R}`);
 | 
			
		||||
    var 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);
 | 
			
		||||
    }
 | 
			
		||||
    tblScroll(R);
 | 
			
		||||
  };
 | 
			
		||||
  /* click event handler for the messages table */
 | 
			
		||||
  const rowclick = (row: any, R: number) => { doitRow(row, R, true); };
 | 
			
		||||
@ -258,7 +293,7 @@ function App() {
 | 
			
		||||
  /* filter message table */
 | 
			
		||||
  const filter = (row: any) => {
 | 
			
		||||
    if(!search) return true;
 | 
			
		||||
    preparse(row.id, row.message);
 | 
			
		||||
    preparse(row.id, row.message, file, protos);
 | 
			
		||||
    return (file.space[+row.id][0].pre||"").toLowerCase().indexOf(search.toLowerCase()) > -1;
 | 
			
		||||
    //return row.message == ".TN.SheetArchive";
 | 
			
		||||
  }
 | 
			
		||||
@ -331,7 +366,7 @@ function App() {
 | 
			
		||||
    navigator.clipboard.writeText(JSON.stringify(props.data, (_,v) => typeof v == "bigint" ? v.toString() : v instanceof Uint8Array ? [...v]: v));
 | 
			
		||||
  }
 | 
			
		||||
  function showProtoDef({props}: {props: MenuProps}) { selectProto(props.type); }
 | 
			
		||||
 | 
			
		||||
  function showXXD({props}: {props: MenuProps}) { console.log(xxd(props.data)); }
 | 
			
		||||
  type NodeRendererProps = {
 | 
			
		||||
    depth: number;
 | 
			
		||||
    name: string;
 | 
			
		||||
@ -365,6 +400,15 @@ function App() {
 | 
			
		||||
        <ObjectName name={name} />: {frag}
 | 
			
		||||
      </span> );
 | 
			
		||||
    }
 | 
			
		||||
    if(data instanceof Uint8Array) return (
 | 
			
		||||
      <span onContextMenu={(e) => {displayMenu(e, { type: "bytes", id, data, field: (data as any).PB_FIELD })}}>
 | 
			
		||||
        <ObjectLabel name={name} data={data} isNonenumerable={isNonenumerable} />
 | 
			
		||||
      </span>
 | 
			
		||||
    );
 | 
			
		||||
    // Uncomment to show hex representation of unsigned 32-bit ints
 | 
			
		||||
    //if(typeof data == "number" && (data>>>0) == data) return ( <>
 | 
			
		||||
    //  <ObjectName name={name} />: <ObjectValue object={data} /> 0x{data.toString(16)}
 | 
			
		||||
    //</>);
 | 
			
		||||
    return ( <ObjectLabel name={name} data={data} isNonenumerable={isNonenumerable} /> );
 | 
			
		||||
  };
 | 
			
		||||
  const metaRenderer = ({ depth, name, data, isNonenumerable }: NodeRendererProps) => {
 | 
			
		||||
@ -407,7 +451,7 @@ function App() {
 | 
			
		||||
        <Panel defaultSize={25}><div className="overflow" ref={tblRef}>
 | 
			
		||||
 | 
			
		||||
          {/* message table */}
 | 
			
		||||
          <TableView data={file.tbl} cols={["id", "type", "message", "path"]} filter={filter} id={id} rowclick={rowclick} />
 | 
			
		||||
          <TableView data={file.tbl} cols={["id", "type", "message", "path"]} filter={filter} id={id} rowclick={rowclick} sort={sort} desc={desc} onsort={onsort}/>
 | 
			
		||||
 | 
			
		||||
        </div></Panel>
 | 
			
		||||
        <PanelResizeHandle style={{ height: "3px", backgroundColor: "#EEEEEE" }} />
 | 
			
		||||
@ -452,7 +496,7 @@ function App() {
 | 
			
		||||
      </PanelGroup>
 | 
			
		||||
 | 
			
		||||
      {/* Menu */}
 | 
			
		||||
      <ContextMenu ID={MENU_ID} menuField={menuField} menuType={menuType} menuId={menuId} onClickId={onClickId} onClickCopyByteArray={onClickCopyByteArray} onClickCopyJSON={onClickCopyJSON} showProtoDef={showProtoDef} />
 | 
			
		||||
      <ContextMenu ID={MENU_ID} menuField={menuField} menuType={menuType} menuId={menuId} onClickId={onClickId} onClickCopyByteArray={onClickCopyByteArray} onClickCopyJSON={onClickCopyJSON} showProtoDef={showProtoDef} showXXD={showXXD} />
 | 
			
		||||
 | 
			
		||||
      {/* Toast */}
 | 
			
		||||
      <ToastContainer />
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user