diff --git a/.gitea/workflows/verify.yml b/.gitea/workflows/verify.yml new file mode 100644 index 0000000..b84a7cb --- /dev/null +++ b/.gitea/workflows/verify.yml @@ -0,0 +1,39 @@ +name: Verify + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +concurrency: + group: verify-${{ gitea.repository }}-${{ gitea.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + timeout-minutes: 45 + env: + CI: "true" + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + - name: Select declared npm version + run: npm install --global npm@11.17.0 + - name: Install dependencies + run: npm ci + - name: Audit runtime dependencies + run: npm audit --omit=dev --audit-level=moderate + - name: Check, test, and build + run: npm run check + - name: Install browser engines + run: npx playwright install --with-deps chromium firefox webkit + - name: Browser tests + run: npm run test:browser diff --git a/CHANGELOG.md b/CHANGELOG.md index 44ed196..e29cb1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.2.0 - 2026-09-02 + +- Add bounded byte editing, binary search and reusable structure templates in + an isolated worker, with explicit offset and interpretation evidence. + ## 0.1.0 - 2026-09-01 - Initial local-first binary conversion and byte inspection workbench. diff --git a/README.md b/README.md index f82aaf9..b728c8b 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,12 @@ Convert, inspect and decode byte-oriented formats locally in the browser. Binary - Synchronized UTF-8, hexadecimal, Base64, RFC 4648 Base32 and Bitcoin-alphabet Base58 conversions. - A paged byte/offset/ASCII inspector with clickable highlighted source ranges. +- Checked insert, replace and delete operations over the canonical byte buffer. +- Exact UTF-8 and hex search, including `??` byte wildcards, capped result sets, + and a cancellable hard-deadline worker path for larger buffers. +- Little- and big-endian integer, floating-point, timestamp and UTF-8 + interpretations at any checked offset. +- Named range bookmarks plus audited PNG, RIFF/WAVE and ELF header templates. - CBOR and MessagePack JSON encode/decode. - Strict ASN.1 DER TLV trees, including canonical identifiers, lengths and core primitive encodings. - Protocol Buffers wire inspection with optional JSON field schemas for common scalar and nested message types. @@ -13,7 +19,7 @@ Convert, inspect and decode byte-oriented formats locally in the browser. Binary ## Safety and limits -All input is inert data. The application makes no external requests and never evaluates decoded content. Inputs and encoded outputs are capped at 1 MiB; structured parsing is capped at depth 64 and 10,000 nodes. Base58 is capped at 4 KiB because arbitrary-base conversion is quadratic. DER rejects indefinite/non-minimal lengths and identifiers plus malformed core primitive encodings. Protobuf groups are deliberately unsupported. Schema assistance validates field records and wire compatibility, but is intentionally limited and is not a generated protobuf runtime. +All input is inert data. The application makes no external requests and never evaluates decoded content. Inputs, edits and encoded outputs are capped at 1 MiB; search patterns at 4 KiB, matches at 10,000, bookmarks at 100 and template fields at 128. Larger searches run in a disposable local worker with cooperative cancellation and a five-second hard deadline. Structured parsing is capped at depth 64 and 10,000 nodes. Base58 is capped at 4 KiB because arbitrary-base conversion is quadratic. DER rejects indefinite/non-minimal lengths and identifiers plus malformed core primitive encodings. Protobuf groups are deliberately unsupported. Templates inspect only declared fields and do not claim full format validation. Schema assistance validates field records and wire compatibility, but is intentionally limited and is not a generated protobuf runtime. ## Development @@ -26,7 +32,7 @@ npm run test:browser npm run package:release -- --force ``` -The deterministic release is written to `release/binary-tools-0.1.0.zip` with a SHA-256 sidecar. +The deterministic release is written to `release/binary-tools-0.2.0.zip` with a SHA-256 sidecar. ## Licence diff --git a/SOURCE.md b/SOURCE.md index 8da486e..2b15be0 100644 --- a/SOURCE.md +++ b/SOURCE.md @@ -1,10 +1,10 @@ # Source identity - Project: Binary Tools -- Version: 0.1.0 +- Version: 0.2.0 - Repository: https://git.add-ideas.de/lotobo/binary-tools - Licence: GPL-3.0-or-later - Build: Node.js 22+, npm 11, `npm ci && npm run release:artifact` -- Artifact: `binary-tools-0.1.0.zip` +- Artifact: `binary-tools-0.2.0.zip` The generated `toolbox-app.json` repeats this version and source identity. Dependencies are exactly pinned by `package-lock.json`; bundled runtime licence texts are included in `LICENSES/npm-runtime-licenses.txt`. diff --git a/docs/ACCESSIBILITY.md b/docs/ACCESSIBILITY.md index 9acc07b..7461616 100644 --- a/docs/ACCESSIBILITY.md +++ b/docs/ACCESSIBILITY.md @@ -1,3 +1,3 @@ # Accessibility -Controls use native labels, buttons, selects and dialogs. Inspector formats expose tab semantics, parsing failures use alert regions, byte ranges have textual offsets, and every byte has an accessible label. Layout reflows below 62rem while wide byte rows remain horizontally scrollable. The shared shell provides system/light/dark themes and visible keyboard focus. +Controls use native labels, buttons, selects and dialogs. Inspector formats use a labelled pressed-state button group, parsing failures use alert regions, byte ranges have textual offsets, and every byte has an accessible label. Layout reflows below 62rem while wide byte rows remain horizontally scrollable. The shared shell provides system/light/dark themes and visible keyboard focus. diff --git a/package-lock.json b/package-lock.json index f8fe871..e305340 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,24 +1,24 @@ { "name": "binary-tools", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "binary-tools", - "version": "0.1.0", + "version": "0.2.0", "license": "GPL-3.0-or-later", "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3", - "@add-ideas/toolbox-helpers": "0.1.0", - "@add-ideas/toolbox-shell-react": "0.2.3", + "@add-ideas/toolbox-contract": "0.3.0", + "@add-ideas/toolbox-helpers": "0.2.0", + "@add-ideas/toolbox-shell-react": "0.3.0", "@msgpack/msgpack": "3.1.3", "cbor-x": "1.6.4", "react": "19.2.8", "react-dom": "19.2.8" }, "devDependencies": { - "@add-ideas/toolbox-testkit": "0.2.3", + "@add-ideas/toolbox-testkit": "0.3.0", "@eslint/js": "10.0.1", "@playwright/test": "1.62.1", "@testing-library/jest-dom": "6.9.1", @@ -44,24 +44,25 @@ } }, "node_modules/@add-ideas/toolbox-contract": { - "version": "0.2.3", - "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-contract/-/0.2.3/toolbox-contract-0.2.3.tgz", - "integrity": "sha512-T0PVSuMT40GjTDfQJhEEY3ZawQq8zz1/ry95JdKI6W39CdLacaRXdGnEpDCMHt+jUbf1Jz7Nat/M5dFCgKVM9A==", + "version": "0.3.0", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-contract/-/0.3.0/toolbox-contract-0.3.0.tgz", + "integrity": "sha512-dKrK7BjOFwqJaBfJuhKxZKIld4sH0AKjEn6a0yLnbdMUFY+fFv4VSLGV2tNSBD016gumc2iNqOjUj/ld7x4rtA==", "license": "Apache-2.0" }, "node_modules/@add-ideas/toolbox-helpers": { - "version": "0.1.0", - "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-helpers/-/0.1.0/toolbox-helpers-0.1.0.tgz", - "integrity": "sha512-UKl1Oxekedf8D2df86VrnVA53AcMhrnh6iUPXY+k8frirBXotb0yd8SGT+IF/3hcqYwcYe/v9WVFuSgKtIYVnw==", - "license": "GPL-3.0-or-later" + "version": "0.2.0", + "license": "GPL-3.0-or-later", + "engines": { + "node": ">=22" + } }, "node_modules/@add-ideas/toolbox-shell-react": { - "version": "0.2.3", - "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-shell-react/-/0.2.3/toolbox-shell-react-0.2.3.tgz", - "integrity": "sha512-DT5lQDH48BFkFcmFLZnQh7+Cm73JzBPcmp5WzUXypfkUXpEyDYHzaXgmW4kZ0edSwh4RK4sPmx+JPtK0X4aKCQ==", + "version": "0.3.0", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-shell-react/-/0.3.0/toolbox-shell-react-0.3.0.tgz", + "integrity": "sha512-74p6JzAOG0YCAKdlc1hLofV4ZIko7vb448S75cIiM88PKm93EHl5VD7g8YVyfM56Ui97UY9dmy+Whiq4sGzpsg==", "license": "Apache-2.0", "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3" + "@add-ideas/toolbox-contract": "0.3.0" }, "peerDependencies": { "react": ">=18 <20", @@ -69,13 +70,13 @@ } }, "node_modules/@add-ideas/toolbox-testkit": { - "version": "0.2.3", - "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-testkit/-/0.2.3/toolbox-testkit-0.2.3.tgz", - "integrity": "sha512-sq1MwhKWfFKen+N+124hl74qQimRSvmQ9sOU7jdcI+2qCKZ67+2B8rWyezeV80uTFu4Jv6deHksfYQ/tKNV6XQ==", + "version": "0.3.0", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-testkit/-/0.3.0/toolbox-testkit-0.3.0.tgz", + "integrity": "sha512-4Fk+oSvZFspOMIXr8Xy040nhAaBsIQAzsGyXWSpjn3+k3yBKq7nB1r5zCHhsXzfdLzvPDAx2KcmSNOhM330D9w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3" + "@add-ideas/toolbox-contract": "0.3.0" }, "bin": { "toolbox-check": "dist/cli.js" diff --git a/package.json b/package.json index 15c23d3..7e5ddd0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "binary-tools", - "version": "0.1.0", + "version": "0.2.0", "description": "Convert, inspect and decode binary data locally in the browser.", "license": "GPL-3.0-or-later", "author": "Albrecht Degering", @@ -39,16 +39,16 @@ "release:artifact": "npm run check && npm run test:browser && npm run package:release -- --force" }, "dependencies": { - "@add-ideas/toolbox-helpers": "0.1.0", - "@add-ideas/toolbox-contract": "0.2.3", - "@add-ideas/toolbox-shell-react": "0.2.3", + "@add-ideas/toolbox-helpers": "0.2.0", + "@add-ideas/toolbox-contract": "0.3.0", + "@add-ideas/toolbox-shell-react": "0.3.0", "@msgpack/msgpack": "3.1.3", "cbor-x": "1.6.4", "react": "19.2.8", "react-dom": "19.2.8" }, "devDependencies": { - "@add-ideas/toolbox-testkit": "0.2.3", + "@add-ideas/toolbox-testkit": "0.3.0", "@eslint/js": "10.0.1", "@playwright/test": "1.62.1", "@testing-library/jest-dom": "6.9.1", diff --git a/playwright.config.ts b/playwright.config.ts index 666d16f..c3bb7ce 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -15,7 +15,25 @@ export default defineConfig({ timeout: 180_000, }, projects: [ - { name: "chromium", use: { ...devices["Desktop Chrome"] } }, - { name: "firefox", use: { ...devices["Desktop Firefox"] } }, + { + name: "chromium", + testIgnore: /responsive\.spec\.ts/, + use: { ...devices["Desktop Chrome"] }, + }, + { + name: "firefox", + testIgnore: /responsive\.spec\.ts/, + use: { ...devices["Desktop Firefox"] }, + }, + { + name: "webkit", + testIgnore: /responsive\.spec\.ts/, + use: { ...devices["Desktop Safari"] }, + }, + { + name: "mobile-chromium", + testMatch: /responsive\.spec\.ts/, + use: { ...devices["Pixel 5"] }, + }, ], }); diff --git a/public/CHANGELOG.md b/public/CHANGELOG.md index 44ed196..e29cb1b 100644 --- a/public/CHANGELOG.md +++ b/public/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.2.0 - 2026-09-02 + +- Add bounded byte editing, binary search and reusable structure templates in + an isolated worker, with explicit offset and interpretation evidence. + ## 0.1.0 - 2026-09-01 - Initial local-first binary conversion and byte inspection workbench. diff --git a/public/LICENSES/npm-runtime-licenses.txt b/public/LICENSES/npm-runtime-licenses.txt index 4e034b1..a63506a 100644 --- a/public/LICENSES/npm-runtime-licenses.txt +++ b/public/LICENSES/npm-runtime-licenses.txt @@ -1,5 +1,5 @@ ============================================================================== -@add-ideas/toolbox-contract@0.2.3 +@add-ideas/toolbox-contract@0.3.0 Declared licence: Apache-2.0 ============================================================================== --- LICENSE --- @@ -198,7 +198,7 @@ Declared licence: Apache-2.0 ============================================================================== -@add-ideas/toolbox-helpers@0.1.0 +@add-ideas/toolbox-helpers@0.2.0 Declared licence: GPL-3.0-or-later ============================================================================== --- LICENSE --- @@ -879,7 +879,7 @@ Public License instead of this License. But first, please read ============================================================================== -@add-ideas/toolbox-shell-react@0.2.3 +@add-ideas/toolbox-shell-react@0.3.0 Declared licence: Apache-2.0 ============================================================================== --- LICENSE --- diff --git a/public/README.md b/public/README.md index f82aaf9..b728c8b 100644 --- a/public/README.md +++ b/public/README.md @@ -6,6 +6,12 @@ Convert, inspect and decode byte-oriented formats locally in the browser. Binary - Synchronized UTF-8, hexadecimal, Base64, RFC 4648 Base32 and Bitcoin-alphabet Base58 conversions. - A paged byte/offset/ASCII inspector with clickable highlighted source ranges. +- Checked insert, replace and delete operations over the canonical byte buffer. +- Exact UTF-8 and hex search, including `??` byte wildcards, capped result sets, + and a cancellable hard-deadline worker path for larger buffers. +- Little- and big-endian integer, floating-point, timestamp and UTF-8 + interpretations at any checked offset. +- Named range bookmarks plus audited PNG, RIFF/WAVE and ELF header templates. - CBOR and MessagePack JSON encode/decode. - Strict ASN.1 DER TLV trees, including canonical identifiers, lengths and core primitive encodings. - Protocol Buffers wire inspection with optional JSON field schemas for common scalar and nested message types. @@ -13,7 +19,7 @@ Convert, inspect and decode byte-oriented formats locally in the browser. Binary ## Safety and limits -All input is inert data. The application makes no external requests and never evaluates decoded content. Inputs and encoded outputs are capped at 1 MiB; structured parsing is capped at depth 64 and 10,000 nodes. Base58 is capped at 4 KiB because arbitrary-base conversion is quadratic. DER rejects indefinite/non-minimal lengths and identifiers plus malformed core primitive encodings. Protobuf groups are deliberately unsupported. Schema assistance validates field records and wire compatibility, but is intentionally limited and is not a generated protobuf runtime. +All input is inert data. The application makes no external requests and never evaluates decoded content. Inputs, edits and encoded outputs are capped at 1 MiB; search patterns at 4 KiB, matches at 10,000, bookmarks at 100 and template fields at 128. Larger searches run in a disposable local worker with cooperative cancellation and a five-second hard deadline. Structured parsing is capped at depth 64 and 10,000 nodes. Base58 is capped at 4 KiB because arbitrary-base conversion is quadratic. DER rejects indefinite/non-minimal lengths and identifiers plus malformed core primitive encodings. Protobuf groups are deliberately unsupported. Templates inspect only declared fields and do not claim full format validation. Schema assistance validates field records and wire compatibility, but is intentionally limited and is not a generated protobuf runtime. ## Development @@ -26,7 +32,7 @@ npm run test:browser npm run package:release -- --force ``` -The deterministic release is written to `release/binary-tools-0.1.0.zip` with a SHA-256 sidecar. +The deterministic release is written to `release/binary-tools-0.2.0.zip` with a SHA-256 sidecar. ## Licence diff --git a/public/SOURCE.md b/public/SOURCE.md index 8da486e..2b15be0 100644 --- a/public/SOURCE.md +++ b/public/SOURCE.md @@ -1,10 +1,10 @@ # Source identity - Project: Binary Tools -- Version: 0.1.0 +- Version: 0.2.0 - Repository: https://git.add-ideas.de/lotobo/binary-tools - Licence: GPL-3.0-or-later - Build: Node.js 22+, npm 11, `npm ci && npm run release:artifact` -- Artifact: `binary-tools-0.1.0.zip` +- Artifact: `binary-tools-0.2.0.zip` The generated `toolbox-app.json` repeats this version and source identity. Dependencies are exactly pinned by `package-lock.json`; bundled runtime licence texts are included in `LICENSES/npm-runtime-licenses.txt`. diff --git a/public/docs/ACCESSIBILITY.md b/public/docs/ACCESSIBILITY.md index 9acc07b..7461616 100644 --- a/public/docs/ACCESSIBILITY.md +++ b/public/docs/ACCESSIBILITY.md @@ -1,3 +1,3 @@ # Accessibility -Controls use native labels, buttons, selects and dialogs. Inspector formats expose tab semantics, parsing failures use alert regions, byte ranges have textual offsets, and every byte has an accessible label. Layout reflows below 62rem while wide byte rows remain horizontally scrollable. The shared shell provides system/light/dark themes and visible keyboard focus. +Controls use native labels, buttons, selects and dialogs. Inspector formats use a labelled pressed-state button group, parsing failures use alert regions, byte ranges have textual offsets, and every byte has an accessible label. Layout reflows below 62rem while wide byte rows remain horizontally scrollable. The shared shell provides system/light/dark themes and visible keyboard focus. diff --git a/public/sw.js b/public/sw.js index c128bd1..54b35f3 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,5 +1,5 @@ const CACHE_PREFIX = "binary-tools-shell-"; -const CACHE_NAME = CACHE_PREFIX + "0.1.0"; +const CACHE_NAME = CACHE_PREFIX + "0.2.0"; const CORE = ["./", "./manifest.webmanifest", "./favicon.svg"]; self.addEventListener("install", (event) => { event.waitUntil( diff --git a/public/toolbox-app.json b/public/toolbox-app.json index 4584325..134ca12 100644 --- a/public/toolbox-app.json +++ b/public/toolbox-app.json @@ -3,7 +3,7 @@ "schemaVersion": 1, "id": "de.add-ideas.binary-tools", "name": "Binary Tools", - "version": "0.1.0", + "version": "0.2.0", "description": "Convert, inspect and decode bytes locally.", "entry": "./", "icon": "./favicon.svg", @@ -29,6 +29,19 @@ "crossOriginIsolated": false, "topLevelContext": false }, + "io": { + "accepts": [ + { "mediaType": "*/*", "extensions": [] }, + { "mediaType": "application/octet-stream", "extensions": [".bin"] }, + { "mediaType": "text/plain", "extensions": [".txt", ".hex"] } + ], + "produces": [ + { "mediaType": "application/octet-stream", "extensions": [".bin"] }, + { "mediaType": "text/plain", "extensions": [".txt", ".hex"] }, + { "mediaType": "application/json", "extensions": [".json"] } + ] + }, + "capabilities": { "required": [], "optional": ["workers"] }, "privacy": { "processing": "local", "fileUploads": true, diff --git a/src/components/Workbench.tsx b/src/components/Workbench.tsx index ef00929..9b887ff 100644 --- a/src/components/Workbench.tsx +++ b/src/components/Workbench.tsx @@ -16,6 +16,20 @@ import { parseSchema, type ByteNode, } from "../core/inspect"; +import { searchBytesInWorker } from "../core/binary-jobs"; +import { + applyBinaryTemplate, + BINARY_TEMPLATES, + findBytePattern, + interpretBytes, + parseReplacementHex, + parseSearchPattern, + replaceByteRange, + validateBookmarks, + type ByteBookmark, + type ByteSearchResult, + type Endianness, +} from "../core/workbench"; type Inspector = "bytes" | "cbor" | "msgpack" | "der" | "protobuf"; const EXAMPLE = new TextEncoder().encode("Hello, binary world!\n"); @@ -35,7 +49,23 @@ export function Workbench() { const [selection, setSelection] = useState({ start: 0, end: 0 }); const [bytePage, setBytePage] = useState(0); const [error, setError] = useState(""); + const [editOffset, setEditOffset] = useState("0"); + const [editDeleteLength, setEditDeleteLength] = useState("0"); + const [editHex, setEditHex] = useState(""); + const [interpretOffset, setInterpretOffset] = useState("0"); + const [endianness, setEndianness] = useState("little"); + const [searchEncoding, setSearchEncoding] = useState<"hex" | "utf8">("hex"); + const [searchSource, setSearchSource] = useState("48 65 ?? 6c 6f"); + const [searchResult, setSearchResult] = useState( + null, + ); + const [searchStatus, setSearchStatus] = useState(""); + const [searchBusy, setSearchBusy] = useState(false); + const [bookmarks, setBookmarks] = useState([]); + const [bookmarkName, setBookmarkName] = useState(""); + const [templateId, setTemplateId] = useState(BINARY_TEMPLATES[0]?.id ?? ""); const fileInput = useRef(null); + const searchAbort = useRef(null); const views = useMemo( () => ({ @@ -81,6 +111,29 @@ export function Workbench() { const visibleBytePage = Math.min(bytePage, bytePageCount - 1); const bytePageStart = visibleBytePage * BYTE_PAGE_SIZE; const bytePageEnd = Math.min(bytes.length, bytePageStart + BYTE_PAGE_SIZE); + const interpretations = useMemo(() => { + try { + return { + values: interpretBytes(bytes, parseOffset(interpretOffset), endianness), + error: "", + }; + } catch (caught) { + return { + values: [], + error: caught instanceof Error ? caught.message : String(caught), + }; + } + }, [bytes, endianness, interpretOffset]); + const template = useMemo(() => { + try { + return { result: applyBinaryTemplate(bytes, templateId), error: "" }; + } catch (caught) { + return { + result: null, + error: caught instanceof Error ? caught.message : String(caught), + }; + } + }, [bytes, templateId]); function selectRange(range: { start: number; end: number }): void { setSelection(range); @@ -94,6 +147,7 @@ export function Workbench() { setBytes(next); setSelection({ start: 0, end: 0 }); setBytePage(0); + setSearchResult(null); setError(""); } catch (caught) { setError(caught instanceof Error ? caught.message : String(caught)); @@ -124,6 +178,8 @@ export function Workbench() { setSource(bytesToHex(next)); setSelection({ start: 0, end: 0 }); setBytePage(0); + setBookmarks([]); + setSearchResult(null); setError(""); } catch (caught) { setError(caught instanceof Error ? caught.message : String(caught)); @@ -148,6 +204,97 @@ export function Workbench() { } } + function commitBytes( + next: Uint8Array, + nextSelection: { start: number; end: number }, + ): void { + setBytes(next); + setEncoding("hex"); + setSource(bytesToHex(next)); + setSelection(nextSelection); + setBytePage( + nextSelection.start < next.length + ? Math.floor(nextSelection.start / BYTE_PAGE_SIZE) + : 0, + ); + setBookmarks((current) => + current.filter( + (bookmark) => bookmark.offset + bookmark.length <= next.length, + ), + ); + setSearchResult(null); + } + + function applyEdit(): void { + try { + const offset = parseOffset(editOffset); + const deleteLength = parseOffset(editDeleteLength); + const replacement = parseReplacementHex(editHex); + const next = replaceByteRange(bytes, offset, deleteLength, replacement); + commitBytes(next, { start: offset, end: offset + replacement.length }); + setError(""); + } catch (caught) { + setError(caught instanceof Error ? caught.message : String(caught)); + } + } + + async function search(): Promise { + searchAbort.current?.abort(); + const controller = new AbortController(); + searchAbort.current = controller; + setSearchBusy(true); + try { + const pattern = parseSearchPattern(searchSource, searchEncoding); + setSearchStatus("Searching…"); + const next = + bytes.length >= 64 * 1024 && typeof Worker !== "undefined" + ? await searchBytesInWorker(bytes, pattern, { + signal: controller.signal, + onProgress: ({ scannedBytes, totalBytes }) => + setSearchStatus( + `Searching ${scannedBytes.toLocaleString()} of ${totalBytes.toLocaleString()} bytes…`, + ), + }) + : findBytePattern(bytes, pattern); + setSearchResult(next); + setSearchStatus( + `${next.offsets.length.toLocaleString()} match${next.offsets.length === 1 ? "" : "es"}${next.truncated ? " (result limit reached)" : ""}.`, + ); + setError(""); + } catch (caught) { + if (!(caught instanceof DOMException && caught.name === "AbortError")) + setError(caught instanceof Error ? caught.message : String(caught)); + setSearchStatus("Search stopped."); + } finally { + if (searchAbort.current === controller) searchAbort.current = null; + setSearchBusy(false); + } + } + + function addBookmark(): void { + try { + const offset = selection.start; + const length = Math.max(0, selection.end - selection.start); + const next = validateBookmarks( + [ + ...bookmarks, + { + id: `bookmark-${Date.now()}-${bookmarks.length}`, + name: bookmarkName || `Offset ${offset}`, + offset, + length, + }, + ], + bytes.length, + ); + setBookmarks(next); + setBookmarkName(""); + setError(""); + } catch (caught) { + setError(caught instanceof Error ? caught.message : String(caught)); + } + } + return (
@@ -214,6 +361,7 @@ export function Workbench() { ref={fileInput} className="sr-only" type="file" + aria-label="Open binary file" onChange={(event) => void importFile(event.target.files?.[0])} /> @@ -234,6 +382,286 @@ export function Workbench() { )}
+
+
+
+
+

Immutable bounded operations

+

Hex editor

+
+
+

+ Replace, insert or delete a checked byte range. An empty replacement + deletes; a zero delete length inserts. +

+
+ + +
+