From 4b4d1dbd8a989ef962a8d85e46e8d904e78361cf Mon Sep 17 00:00:00 2001 From: SheetJS Date: Fri, 19 May 2023 16:17:28 -0400 Subject: [PATCH] plaintext search --- package.json | 2 +- src/App.css | 8 +-- src/App.tsx | 139 ++++++++++++++++++++++++++++++------------ src/iwa.ts | 5 +- src/messages/index.ts | 1 + 5 files changed, 110 insertions(+), 45 deletions(-) diff --git a/package.json b/package.json index c6465a5..8265ca2 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,6 @@ "patch-package": "7.0.0", "typescript": "5.0.4", "vite": "4.3.5", - "vite-plugin-pwa": "0.14.7" + "vite-plugin-pwa": "0.14.7" } } diff --git a/src/App.css b/src/App.css index 25b95e8..0be0d67 100644 --- a/src/App.css +++ b/src/App.css @@ -2,13 +2,13 @@ box-sizing: border-box; } -html, body { - width: 100vw; +html, body { + width: 100vw; min-height: 100vh; } #root { - width: 100vw; + width: 100vw; height: 100vh; display: grid; grid-template-columns: 1fr; @@ -34,7 +34,7 @@ th { top: 0; width: 100vw; max-width: 100%; - position: sticky; + position: sticky; box-shadow: 0 -5px 15px 0 #ced4da; background: white; z-index: 10; diff --git a/src/App.tsx b/src/App.tsx index 1f4c9a7..a97eb3c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,20 +1,22 @@ /* TODO: - history - find example and correctly handle "merge" messages - - messages referencing selected msg - sort and filter table - loading icons - expand referenced object in place - paste bytes -> analyze - edit fields / files? - different menu for message / enum / extend / literal + - display integers in base16 and base10 + - search bytes? + - highlight results in inspector */ import { useState, useEffect, useRef } from 'react'; import type { MouseEvent as ReactMouseEvent, ChangeEventHandler, ChangeEvent } from 'react'; import './App.css'; import { ObjectInspector, ObjectLabel, ObjectName, ObjectValue } from 'react-inspector'; import { PanelGroup, Panel, PanelResizeHandle } from 'react-resizable-panels'; -import { process, read, ParsedFile } from './iwa'; +import { process, read, ParsedFile, TableItem } from './iwa'; import { parse_protos, ProtoMap } from './messages'; import type { $_TSP_MessageInfo, $_TSP_Reference } from './messages'; import { Menu, Item, Separator, useContextMenu } from 'react-contexify'; @@ -56,11 +58,12 @@ type TableViewProps = { id?: string; data: any[]; cols: string[]; + filter?: (row: any, R: number) => boolean; rowclick?: (row: any, R: number, e: ReactMouseEvent) => void; cellclick?: (value: any, R: number, C: number, e: ReactMouseEvent) => void; }; -function TableView({id, data, cols, rowclick, cellclick}: TableViewProps) { +function TableView({id, data, cols, filter, rowclick, cellclick}: TableViewProps) { return ( @@ -68,7 +71,7 @@ function TableView({id, data, cols, rowclick, cellclick}: TableViewProps) { ))} - {data.map((row, R) => ( + {data.filter(filter || (()=>true)).map((row, R) => ( {e.preventDefault(); e.stopPropagation(); rowclick(row, R, e)}} : {})} {...(row["id"] == id ? {style: {backgroundColor: "#646cff", color: "#FFFFFF" }} : {})} @@ -120,6 +123,8 @@ function App() { const [obj, setObj] = useState({}); /* current meta */ const [meta, setMeta] = useState<$_TSP_MessageInfo>({} as any); + /* current deps */ + const [deps, setDeps] = useState([]); /* protobuf definitions */ const [protos, setProtos] = useState({}); /* selected message type */ @@ -128,6 +133,10 @@ function App() { const [__html, setProto] = useState("Select a Row"); /* "dirty" if inspecting a subfield */ const [dirty, setDirty] = useState(false); + /* loading */ + const [loading, setLoading] = useState(true); + /* search */ + const [search, setSearch] = useState(""); /* history stack */ const [stack, setStack] = useState([]); /* react-contexify */ @@ -138,12 +147,41 @@ function App() { const [menuId, setMenuId] = useState(""); const tblRef = useRef(null); + /* 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, (_,v) => typeof v == "bigint" ? v.toString() : v instanceof Uint8Array ? [...v]: v); + } + 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; + }); + }); + } + }; /* update selection based on table row */ - const doitRow = (row: any, R: number, reset?: boolean) => { + const doitRow = (row: TableItem, R: number, reset?: boolean) => { let obj: any, meta: $_TSP_MessageInfo; try { - obj = process(file.space[+row.id][0].data, row.message, protos); - meta = process(file.space[+row.id][0].rawmeta, ".TSP.MessageInfo", protos); + preparse(row.id, row.message); + const item = file.space[+row.id][0]; + obj = item.parsed; + meta = item.parsedmeta as $_TSP_MessageInfo; } catch(e) { console.error(row, e); toast.error(`Could not parse ${row.id} (${row.type})`, {position: toast.POSITION.TOP_CENTER}); return; } @@ -152,23 +190,11 @@ function App() { setProto(protos[row.message] || ""); setDirty(false); setObj(obj); - /* .TSP.MessageInfo */ - if(meta.object_references) meta.$object_references = meta.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(meta.field_infos) meta.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; - }); - }) 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; + })); setId(String(row.id)); var rowelt = document.getElementById(`tr-${R}`); var top = rowelt?.offsetTop || 0; @@ -196,29 +222,56 @@ function App() { doitRow(file.tbl[R], R); }; - /* on load, get protobuf definitions and process the test file */ + /* filter message table */ + const filter = (row: any) => { + if(!search) return true; + preparse(row.id, row.message); + return (file.space[+row.id][0].pre||"").toLowerCase().indexOf(search.toLowerCase()) > -1; + //return row.message == ".TN.SheetArchive"; + } + + /* load file */ + const process_ab = (ab: ArrayBuffer, p: ProtoMap = protos) => { + const f = read(ab); + f.tbl.forEach(row => preparse(row.id, row.message, f, p)); + setFile(f); + }; + + /* on page load, get protobuf definitions and process the test file */ useEffect(() => { + const id = toast.loading(`Processing test.numbers`); let ignore = false; (async () => { + await new Promise((res) => setTimeout(res, 100)); // "sleep" to give toast a chance to shine const protos = await (await fetch("protos")).text(); const testfile = await (await fetch("test.numbers")).arrayBuffer(); if(ignore) return; - setProtos(parse_protos(protos)); - setFile(read(testfile)); + const p = parse_protos(protos); + setProtos(p); + process_ab(testfile, p); + toast.dismiss(id); + setLoading(false); })(); - return () => { ignore = true; } + return () => { ignore = true; toast.dismiss(id); } }, []); useEffect(() => { if(file.tbl[0]) doitRow(file.tbl[0], 0, true); }, [file]); const onChange: ChangeEventHandler = async (e: ChangeEvent) => { - if(!e.target.files) { toast.error("must select a file!"); throw new Error("No file selected!"); } - let data: ParsedFile; - try { data = read(await e.target.files?.[0].arrayBuffer()); } catch(e) { + setLoading(true); + if(!e.target.files) { toast.error("must select a file!"); throw new Error("No file selected!"); setLoading(false); } + try { + const id = toast.loading(`Processing ${e.target.files[0].name}`); + await new Promise((res) => setTimeout(res, 100)); // "sleep" to give toast a chance to shine + process_ab(await e.target.files?.[0].arrayBuffer()); + toast.dismiss(id); + } catch(e) { if((e as any).message?.includes("Failed to read archive")) toast.error("Please select an iWork file", { position: toast.POSITION.TOP_CENTER }); else toast.error(e && (e as any).message || e, { position: toast.POSITION.TOP_CENTER }); - console.error(e); return; + console.error(e); + } finally { + e.target.value = ""; + setLoading(false); } - setFile(data); } /* Menu machinations */ @@ -297,28 +350,35 @@ function App() { } return ( ); }; + const depsRenderer = ({ depth, data, isNonenumerable }: NodeRendererProps) => { + if(depth === 0) return ( Dependents ); + return ( gotoRef(data.id)}> ); + }; return ( <> {/* header */} - +
{/* message table */} - +
-
- +
{/* selected message bar */}
{sel ? (<> Selected message {(stack.length > 4 ? [...stack.slice(0,2), "...", ...stack.slice(-1)] : stack).map(i => i + " > ").join("")} {id} ({sel}) {file.space[+id]?.[0]?.data?.length || 0} bytes {stack.length && Return to {stack[stack.length - 1]} || ""} ) : "Select a message to see the contents"}
-
+
{/* proto definition */} @@ -333,13 +393,14 @@ function App() {
-
+
{/* inspector */} Message Meta + {deps.length && || void 0}
@@ -356,4 +417,4 @@ function App() { ); } -export default App +export default App; diff --git a/src/iwa.ts b/src/iwa.ts index a7727ac..cb3e0c5 100644 --- a/src/iwa.ts +++ b/src/iwa.ts @@ -1,6 +1,6 @@ import * as CFB from 'cfb'; -import Messages, { MessageTypes } from "./messages/"; +import Messages, { $_TSP_MessageInfo, MessageTypes } from "./messages/"; /* see https://bugs.webkit.org/show_bug.cgi?id=243148 -- affects iOS Safari */ declare var Buffer: any; // Buffer is typeof-guarded but TS still needs this :( @@ -119,6 +119,9 @@ interface IWAMessage { meta: ProtoMessage; rawmeta: Uint8Array; data: Uint8Array; + pre?: string; + parsed?: any; + parsedmeta?: $_TSP_MessageInfo; } interface IWAArchiveInfo { id: number; diff --git a/src/messages/index.ts b/src/messages/index.ts index 5318243..3f4cc41 100644 --- a/src/messages/index.ts +++ b/src/messages/index.ts @@ -29,6 +29,7 @@ const post_process = (buf: string, key: string, out: ProtoMap) => { return $1 + " " + key + "." + $2 }); out[new_key] = payload; + post_process(payload, new_key, out); payload = ""; } else if(nested) payload += row + "\n"; });
{c}