#!/usr/bin/env bash set -euo pipefail cd "$(dirname "$0")" # LinuxCNC source basis: this guard ties browser OPFS parameter persistence to # interp_internal.hh defaults and rs274ngc_pre.cc restore_parameters() / # save_parameters() file semantics. It validates bridge policy only; it does not # add CNC behavior, expand smoke parsing, or interpret G-code. linuxcnc_root=${LINUXCNC_ROOT:-../linuxcnc} linuxcnc_root=$(cd "$linuxcnc_root" && pwd) grep -F '#define RS274NGC_PARAMETER_FILE_NAME_DEFAULT "rs274ngc.var"' \ "$linuxcnc_root/src/emc/rs274ngc/interp_internal.hh" >/dev/null grep -F '#define RS274NGC_PARAMETER_FILE_BACKUP_SUFFIX ".bak"' \ "$linuxcnc_root/src/emc/rs274ngc/interp_internal.hh" >/dev/null grep -F 'int Interp::restore_parameters(const char *filename)' \ "$linuxcnc_root/src/emc/rs274ngc/rs274ngc_pre.cc" >/dev/null grep -F 'int Interp::open(const char *filename)' \ "$linuxcnc_root/src/emc/rs274ngc/rs274ngc_pre.cc" >/dev/null grep -F '_setup.file_pointer = fopen(filename, "r");' \ "$linuxcnc_root/src/emc/rs274ngc/rs274ngc_pre.cc" >/dev/null grep -F 'read_text(command, _setup.file_pointer, _setup.linetext,' \ "$linuxcnc_root/src/emc/rs274ngc/rs274ngc_pre.cc" >/dev/null grep -F 'int Interp::save_parameters(const char *filename,' \ "$linuxcnc_root/src/emc/rs274ngc/rs274ngc_pre.cc" >/dev/null grep -F 'std::string tempfile = std::string(filename) + ".new";' \ "$linuxcnc_root/src/emc/rs274ngc/rs274ngc_pre.cc" >/dev/null grep -F '+ RS274NGC_PARAMETER_FILE_BACKUP_SUFFIX;' \ "$linuxcnc_root/src/emc/rs274ngc/rs274ngc_pre.cc" >/dev/null grep -F 'unlink(bakfile.c_str());' \ "$linuxcnc_root/src/emc/rs274ngc/rs274ngc_pre.cc" >/dev/null grep -F 'if(link(filename, bakfile.c_str()) < 0)' \ "$linuxcnc_root/src/emc/rs274ngc/rs274ngc_pre.cc" >/dev/null grep -F 'if(rename(tempfile.c_str(), filename) < 0)' \ "$linuxcnc_root/src/emc/rs274ngc/rs274ngc_pre.cc" >/dev/null grep -F 'RS274NGC_PARAMETER_FILE_NAME_DEFAULT` as `rs274ngc.var`' \ docs/linuxcnc-opfs-source-map.md >/dev/null grep -F 'RS274NGC_PARAMETER_FILE_BACKUP_SUFFIX` as `.bak`' \ docs/linuxcnc-opfs-source-map.md >/dev/null grep -F '`Interp::restore_parameters()` for parameter-file reads' \ docs/linuxcnc-opfs-source-map.md >/dev/null grep -F '`Interp::open()` by opening' \ docs/linuxcnc-opfs-source-map.md >/dev/null grep -F '`fopen(filename, "r")`, then `Interp::_read()`' \ docs/linuxcnc-opfs-source-map.md >/dev/null grep -F '`read_text()`' docs/linuxcnc-opfs-source-map.md >/dev/null grep -F '`Interp::save_parameters()` for `filename + ".new"`' \ docs/linuxcnc-opfs-source-map.md >/dev/null grep -F 'unlinks `filename + ".bak"`, links the' \ docs/linuxcnc-opfs-source-map.md >/dev/null grep -F 'must not add independent filesystem behavior or expand the temporary smoke parser' \ docs/linuxcnc-opfs-source-map.md >/dev/null grep -F 'parseFileWithParameterFile()' docs/linuxcnc-opfs-source-map.md >/dev/null grep -F 'OPFS Coverage' docs/linuxcnc-opfs-source-map.md >/dev/null grep -F '| `web/src/wasm-core.js` |' docs/linuxcnc-opfs-source-map.md >/dev/null grep -F '| `web/src/wasm-core.d.ts` |' docs/linuxcnc-opfs-source-map.md >/dev/null grep -F '| `web/src/app.js` |' docs/linuxcnc-opfs-source-map.md >/dev/null grep -F '| `web/test-browser-wasm-smoke-opfs-basic-sections.js` |' docs/linuxcnc-opfs-source-map.md >/dev/null grep -F '| `web/test-browser-wasm-smoke-opfs-parameter-sections.js` |' docs/linuxcnc-opfs-source-map.md >/dev/null grep -F '| `web/test-browser-wasm-smoke-opfs-workspace-sections.js` |' docs/linuxcnc-opfs-source-map.md >/dev/null grep -F '| `web/test-browser-wasm-smoke-opfs-mirror-sections.js` |' docs/linuxcnc-opfs-source-map.md >/dev/null grep -F '| `web/test-browser-wasm-smoke-opfs-directory-sections.js` |' docs/linuxcnc-opfs-source-map.md >/dev/null grep -F '| `web/test-browser-wasm-smoke-opfs-policy-sections.js` |' docs/linuxcnc-opfs-source-map.md >/dev/null grep -F '| `web/test-browser-wasm-smoke-app-sections.js` |' docs/linuxcnc-opfs-source-map.md >/dev/null grep -F 'const LINUXCNC_DEFAULT_PARAMETER_FILE = "rs274ngc.var";' web/src/wasm-core.js >/dev/null grep -F 'async parseFile(path, dialect = "linuxcnc", options = {})' web/src/wasm-core.js >/dev/null grep -F 'OPFS workspace is not available for program file parsing' web/src/wasm-core.js >/dev/null grep -F 'const program = new TextDecoder().decode(await opfs.readFile(path));' web/src/wasm-core.js >/dev/null grep -F 'async parseFileWithParameterFile(path, parameterPath, dialect = "linuxcnc", options = {})' \ web/src/wasm-core.js >/dev/null grep -F 'OPFS workspace is not available for program and parameter file parsing' web/src/wasm-core.js >/dev/null grep -F '`${LINUXCNC_DEFAULT_PARAMETER_FILE}.new`' web/src/wasm-core.js >/dev/null grep -F '`${LINUXCNC_DEFAULT_PARAMETER_FILE}.bak`' web/src/wasm-core.js >/dev/null grep -F 'LinuxCNC source basis: rs274ngc_pre.cc restore_parameters() reads the' web/src/wasm-core.js >/dev/null grep -F 'before replacing the main file and managing filename + ".bak".' web/src/wasm-core.js >/dev/null grep -F 'app frame did not clean up LinuxCNC temporary parameter file in OPFS' \ web/test-browser-wasm-smoke-app-sections.js >/dev/null grep -F 'OPFS cannot be disabled in browser contexts with OPFS support' web/src/wasm-core.js >/dev/null grep -F 'OPFS path must stay inside the CNC workspace' web/src/wasm-core.js >/dev/null grep -F 'OPFS mount point must stay inside the Emscripten filesystem' web/src/wasm-core.js >/dev/null grep -F 'async persistDirectory(path)' web/src/wasm-core.js >/dev/null grep -F 'async readDirectory(path)' web/src/wasm-core.js >/dev/null grep -F 'export type WasmOpfsWorkspace = {' web/src/wasm-core.d.ts >/dev/null grep -F 'readDirectory(path: string): Promise;' web/src/wasm-core.d.ts >/dev/null grep -F 'persistDirectory(path: string): Promise;' web/src/wasm-core.d.ts >/dev/null grep -F 'loadParameterFile(path: string): Promise;' web/src/wasm-core.d.ts >/dev/null grep -F 'persistParameterFile(path: string): Promise;' web/src/wasm-core.d.ts >/dev/null grep -F 'opfs?: false | {' web/src/wasm-core.d.ts >/dev/null grep -F 'mountPoint?: string;' web/src/wasm-core.d.ts >/dev/null grep -F 'workspacePath?: string;' web/src/wasm-core.d.ts >/dev/null grep -F 'const events = await simulator.parseFileWithParameterFile(programPath, parameterPath, "linuxcnc", parseOptions);' \ web/src/app.js >/dev/null grep -F 'OPFS workspace is required for browser program parsing' web/src/app.js >/dev/null grep -F 'runBrowserOpfsBasicSections(context, simulator)' \ web/test-browser-wasm-smoke-opfs-workspace-sections.js >/dev/null grep -F 'runBrowserOpfsParameterSections(context, simulator)' \ web/test-browser-wasm-smoke-opfs-workspace-sections.js >/dev/null grep -F 'runBrowserOpfsMirrorSections(context, simulator)' \ web/test-browser-wasm-smoke-opfs-workspace-sections.js >/dev/null grep -F 'runBrowserOpfsDirectorySections(context, simulator)' \ web/test-browser-wasm-smoke-opfs-workspace-sections.js >/dev/null grep -F 'runBrowserOpfsPolicySections(context)' web/test-browser-wasm-smoke-sections.js >/dev/null grep -F 'runSection("opfs directory persist and load"' \ web/test-browser-wasm-smoke-opfs-directory-sections.js >/dev/null grep -F 'runSection("opfs metadata queries avoid wasm mirror pollution"' \ web/test-browser-wasm-smoke-opfs-directory-sections.js >/dev/null grep -F 'runSection("opfs copy and move mirror files"' \ web/test-browser-wasm-smoke-opfs-mirror-sections.js >/dev/null grep -F 'runSection("browser opfs policy"' \ web/test-browser-wasm-smoke-opfs-policy-sections.js >/dev/null grep -F 'runSection("opfs parameter restore in new instance"' test-web-wasm-browser-smoke.sh >/dev/null grep -F 'runSection("opfs invalid parameter restore failures"' test-web-wasm-browser-smoke.sh >/dev/null grep -F 'runSection("opfs stale parameter state cleanup"' test-web-wasm-browser-smoke.sh >/dev/null grep -F 'runSection("opfs program parse and restore"' test-web-wasm-browser-smoke.sh >/dev/null grep -F 'runSection("browser app opfs restore"' test-web-wasm-browser-smoke.sh >/dev/null if grep -R -n -E 'localStorage|sessionStorage|indexedDB|showOpenFilePicker|showSaveFilePicker|showDirectoryPicker|webkitRequestFileSystem|FileReader' web/src; then echo "browser app/wasm code must not add filesystem persistence outside OPFS" >&2 exit 1 fi if grep -n -E 'localStorage|sessionStorage|indexedDB|showOpenFilePicker|showSaveFilePicker|showDirectoryPicker|webkitRequestFileSystem|FileReader' \ web/test-browser-wasm-smoke-*.js \ web/test-browser-wasm-smoke.html \ test-web-wasm-browser-smoke.sh; then echo "browser OPFS smoke harness must not add filesystem persistence outside OPFS" >&2 exit 1 fi python3 - <<'PY' from pathlib import Path source_map = Path("docs/linuxcnc-opfs-source-map.md").read_text(encoding="utf-8") coverage_entries = [ "web/src/wasm-core.js", "web/src/wasm-core.d.ts", "web/src/app.js", "web/test-browser-wasm-smoke-opfs-basic-sections.js", "web/test-browser-wasm-smoke-opfs-parameter-sections.js", "web/test-browser-wasm-smoke-opfs-workspace-sections.js", "web/test-browser-wasm-smoke-opfs-mirror-sections.js", "web/test-browser-wasm-smoke-opfs-directory-sections.js", "web/test-browser-wasm-smoke-opfs-policy-sections.js", "web/test-browser-wasm-smoke-app-sections.js", ] for entry in coverage_entries: if f"| `{entry}` |" not in source_map: raise SystemExit(f"docs/linuxcnc-opfs-source-map.md missing OPFS coverage entry: {entry}") required_phrases = [ "programs/current.ngc", "parameters/rs274ngc.var", 'fopen(filename, "r")', "LinuxCNC `read_text()`", "program reads and parameter restore/save bridge files under the OPFS workspace", "`.new`, `.bak`, failure, and stale-state semantics", 'unlinks `filename + ".bak"`', "OPFS/WASM mirror persistence", "OPFS directory persistence and WASM mirror refresh", "OPFS workspace and mount policy", "Browser OPFS smoke harness files must also stay free of Local Storage", "they may only verify OPFS-backed browser persistence", "OPFS-backed workspace, directory, parameter-file, and mount/workspace option types", "app-level OPFS program, parameter, backup, and reload behavior", "./check-linuxcnc-opfs-source-map.sh", "does not interpret G-code", ] for phrase in required_phrases: if phrase not in source_map: raise SystemExit(f"docs/linuxcnc-opfs-source-map.md missing OPFS coverage phrase: {phrase}") for entry in [ "test-native.sh", "test-linuxcnc-source-link.sh", "build-wasm.sh", ]: text = Path(entry).read_text(encoding="utf-8") if "./check-linuxcnc-opfs-source-map.sh" not in text: raise SystemExit(f"{entry} must run ./check-linuxcnc-opfs-source-map.sh") PY