Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/App.affine
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,37 @@
module App;

// TODO: Complete semantic implementation


/* === ORIGINAL RESCRIPT IMPLEMENTATION ===
// SPDX-License-Identifier: MPL-2.0
// DotMatrix-FilePrinter - App module
// Exports functions for the UI layer
// Uses proven library for formally verified safety operations

// Re-export bindings for JavaScript consumption
let checkGforth = Bindings.checkGforth
let previewStrike = Bindings.previewStrike
let executeStrike = Bindings.executeStrike
let verifySubstrate = Bindings.verifySubstrate

// Utilities (powered by proven library)
let stringToBytes = Bindings.stringToBytes
let bytesToString = Bindings.bytesToString
let parseByteString = Bindings.parseByteString
let isValidByte = Bindings.isValidByte
let isValidPath = Bindings.isValidPath
let bytesToHex = Bindings.bytesToHex
let bytesToHexCompact = Bindings.bytesToHexCompact
let hexToBytes = Bindings.hexToBytes

// For hex input mode
let decodeHex = Bindings.hexToBytes

// Constraint values for UI
let maxByte = Types.Constraints.maxByte
let forbiddenNbsp = Types.Constraints.forbiddenNbsp
let forbiddenUtf8 = Types.Constraints.forbiddenUtf8
let forbiddenBytes = Types.Constraints.forbiddenBytes

======================================== */
131 changes: 131 additions & 0 deletions src/Bindings.affine
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,134 @@
module Bindings;

// TODO: Complete semantic implementation


/* === ORIGINAL RESCRIPT IMPLEMENTATION ===
// SPDX-License-Identifier: MPL-2.0
// Backend FFI Bindings for DotMatrix-FilePrinter
// Uses proven library for formally verified safety operations
// Uses RuntimeBridge for Gossamer/Tauri/browser dispatch

// Core backend API β€” delegates to RuntimeBridge for runtime detection
let invoke = RuntimeBridge.invoke

// Re-export types
type contaminant = Types.contaminant
type previewResult = Types.previewResult
type verifyResult = Types.verifyResult

// Commands - these call the Rust backend

// Check if gforth is available
let checkGforth = (): promise<bool> => {
invoke("check_gforth", ())
}

// Preview strike (dry-run)
let previewStrike = (bytes: array<int>): promise<previewResult> => {
invoke("preview_forth_strike", {"bytes": bytes})
}

// Helper to create a rejected promise with an error message
let rejectWithMessage: string => promise<'a> = %raw(`
function(msg) { return Promise.reject(new Error(msg)); }
`)

// Execute strike via Forth kernel (with path validation using SafePath)
let executeStrike = (bytes: array<int>, path: string): promise<unit> => {
// Validate path doesn't contain traversal attacks
if !Proven_SafePath.isSafe(path) {
rejectWithMessage("Invalid path: contains traversal sequences")
} else {
invoke("execute_forth_strike", {"bytes": bytes, "path": path})
}
}

// Verify substrate file (with path validation using SafePath)
let verifySubstrate = (path: string): promise<verifyResult> => {
if !Proven_SafePath.isSafe(path) {
rejectWithMessage("Invalid path: contains traversal sequences")
} else {
invoke("verify_substrate", {"path": path})
}
}

// Utilities using proven library

// Convert string to byte array using SafeString.toCodePoints
let stringToBytes = (str: string): array<int> => {
switch Proven_SafeString.toCodePoints(str) {
| Ok(bytes) => bytes
| Error(_) => [] // Return empty on non-ASCII input
}
}

// Convert byte array to string using SafeString.fromCodePoints
let bytesToString = (bytes: array<int>): string => {
switch Proven_SafeString.fromCodePoints(bytes) {
| Ok(str) => str
| Error(_) => "" // Return empty on invalid code points
}
}

// Validate a single byte (uses SafeMath via Types.Constraints)
let isValidByte = (byte: int): bool => {
Types.Constraints.isValidByte(byte)
}

// Validate path safety using SafePath
let isValidPath = (path: string): bool => {
Proven_SafePath.isSafe(path)
}

// Parse comma-separated bytes using SafeMath.fromString
let parseByteString = (str: string): result<array<int>, string> => {
let parts = str
->String.trim
->String.split(",")
->Array.map(String.trim)
->Array.filter(s => s->String.length > 0)

// Use SafeMath.fromString for safe integer parsing
let bytes = parts->Array.map(s => Proven_SafeMath.fromString(s))

if bytes->Array.some(Option.isNone) {
Error("Invalid byte value")
} else {
let values = bytes->Array.filterMap(x => x)
let invalid = values->Array.findIndex(b => !isValidByte(b))
if invalid >= 0 {
Error(`Byte at position ${Int.toString(invalid)} is invalid`)
} else {
Ok(values)
}
}
}

// Format bytes as spaced hex using SafeHex.encodeSpaced
let bytesToHex = (bytes: array<int>): string => {
switch Proven_SafeHex.encodeSpaced(bytes) {
| Ok(hex) => hex
| Error(_) => "" // Return empty on invalid bytes
}
}

// Format bytes as compact hex (no spaces) using SafeHex.encode
let bytesToHexCompact = (bytes: array<int>): string => {
switch Proven_SafeHex.encode(bytes) {
| Ok(hex) => hex
| Error(_) => ""
}
}

// Decode hex string to bytes using SafeHex.decode
let hexToBytes = (hexStr: string): result<array<int>, string> => {
switch Proven_SafeHex.decode(hexStr) {
| Ok(bytes) => Ok(bytes)
| Error(Proven_SafeHex.InvalidLength) => Error("Invalid hex length (must be even)")
| Error(Proven_SafeHex.InvalidCharacter) => Error("Invalid hex character")
| Error(Proven_SafeHex.EmptyInput) => Ok([])
}
}

======================================== */
212 changes: 212 additions & 0 deletions src/RuntimeBridge.affine
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,215 @@
module RuntimeBridge;

// TODO: Complete semantic implementation


/* === ORIGINAL RESCRIPT IMPLEMENTATION ===
// SPDX-License-Identifier: MPL-2.0

/// RuntimeBridge β€” Unified IPC bridge for DotMatrix-FilePrinter.
///
/// Detects the available runtime (Gossamer, Tauri, or browser-only) and
/// dispatches `invoke` calls to the appropriate backend. This allows all
/// command modules to use a single import instead of binding directly
/// to `@tauri-apps/api/core`.
///
/// Priority order:
/// 1. Gossamer (`window.__gossamer_invoke`) β€” own stack, preferred
/// 2. Tauri (`window.__TAURI_INTERNALS__`) β€” legacy, transition
/// 3. Browser (direct HTTP fetch) β€” development fallback
///
/// Migration path: Bindings.res replaces
/// `@module("@tauri-apps/api/core") external invoke: ...`
/// with
/// `let invoke = RuntimeBridge.invoke`

// ---------------------------------------------------------------------------
// Raw external bindings β€” exactly one of these will be available at runtime
// ---------------------------------------------------------------------------

/// Gossamer IPC: injected by gossamer_channel_open() into the webview.
%%raw(`
function isGossamerRuntime() {
return typeof window !== 'undefined'
&& typeof window.__gossamer_invoke === 'function';
}
`)
@val external isGossamerRuntime: unit => bool = "isGossamerRuntime"

%%raw(`
function gossamerInvoke(cmd, args) {
return window.__gossamer_invoke(cmd, args);
}
`)
@val external gossamerInvoke: (string, 'a) => promise<'b> = "gossamerInvoke"

/// Tauri IPC: injected by the Tauri runtime into the webview.
%%raw(`
function isTauriRuntime() {
return typeof window !== 'undefined'
&& window.__TAURI_INTERNALS__ != null
&& !window.__TAURI_INTERNALS__.__BROWSER_SHIM__;
}
`)
@val external isTauriRuntime: unit => bool = "isTauriRuntime"

@module("@tauri-apps/api/core")
external tauriInvoke: (string, 'a) => promise<'b> = "invoke"

// ---------------------------------------------------------------------------
// Unified invoke β€” detects runtime and dispatches
// ---------------------------------------------------------------------------

/// The runtime currently in use. Cached after first detection for performance.
type runtime =
| Gossamer
| Tauri
| BrowserOnly

%%raw(`
var _detectedRuntime = null;
function detectRuntime() {
if (_detectedRuntime !== null) return _detectedRuntime;
if (typeof window !== 'undefined' && typeof window.__gossamer_invoke === 'function') {
_detectedRuntime = 'gossamer';
} else if (typeof window !== 'undefined' && window.__TAURI_INTERNALS__ != null && !window.__TAURI_INTERNALS__.__BROWSER_SHIM__) {
_detectedRuntime = 'tauri';
} else {
_detectedRuntime = 'browser';
}
return _detectedRuntime;
}
`)
@val external detectRuntimeRaw: unit => string = "detectRuntime"

/// Detect and return the current runtime.
let detectRuntime = (): runtime => {
switch detectRuntimeRaw() {
| "gossamer" => Gossamer
| "tauri" => Tauri
| _ => BrowserOnly
}
}

/// Invoke a backend command through whatever runtime is available.
///
/// - On Gossamer: calls `window.__gossamer_invoke(cmd, args)`
/// - On Tauri: calls `window.__TAURI_INTERNALS__.invoke(cmd, args)`
/// - On browser: rejects with a descriptive error
///
/// This is the primary function all command modules should use.
let invoke = (cmd: string, args: 'a): promise<'b> => {
if isGossamerRuntime() {
gossamerInvoke(cmd, args)
} else if isTauriRuntime() {
tauriInvoke(cmd, args)
} else {
Promise.reject(
JsError.throwWithMessage(
`No desktop runtime β€” "${cmd}" requires Gossamer or Tauri`,
),
)
}
}

/// Check whether any desktop runtime is available.
let hasDesktopRuntime = (): bool => {
isGossamerRuntime() || isTauriRuntime()
}

/// Get a human-readable name for the current runtime.
let runtimeName = (): string => {
switch detectRuntime() {
| Gossamer => "Gossamer"
| Tauri => "Tauri"
| BrowserOnly => "Browser"
}
}

// ---------------------------------------------------------------------------
// Dialog abstraction β€” Gossamer dialogs vs Tauri plugin-dialog
// ---------------------------------------------------------------------------

module Dialog = {
/// Gossamer file dialog: calls gossamer_dialog_open via IPC.
/// Tauri file dialog: calls @tauri-apps/plugin-dialog.
@module("@tauri-apps/plugin-dialog")
external tauriOpenRaw: JSON.t => promise<Nullable.t<JSON.t>> = "open"

@module("@tauri-apps/plugin-dialog")
external tauriSaveRaw: JSON.t => promise<Nullable.t<JSON.t>> = "save"

/// Open a file picker dialog.
let \"open" = (opts: JSON.t): promise<Nullable.t<JSON.t>> => {
if isGossamerRuntime() {
gossamerInvoke("__gossamer_dialog_open", opts)
} else if isTauriRuntime() {
tauriOpenRaw(opts)
} else {
Promise.reject(
JsError.throwWithMessage(
"No desktop runtime β€” file dialogs require Gossamer or Tauri",
),
)
}
}

/// Open a save dialog.
let save = (opts: JSON.t): promise<Nullable.t<JSON.t>> => {
if isGossamerRuntime() {
gossamerInvoke("__gossamer_dialog_save", opts)
} else if isTauriRuntime() {
tauriSaveRaw(opts)
} else {
Promise.reject(
JsError.throwWithMessage(
"No desktop runtime β€” save dialogs require Gossamer or Tauri",
),
)
}
}
}

// ---------------------------------------------------------------------------
// Filesystem abstraction β€” Gossamer fs vs Tauri plugin-fs
// ---------------------------------------------------------------------------

module Fs = {
@module("@tauri-apps/plugin-fs")
external tauriReadTextFileRaw: string => promise<string> = "readTextFile"

@module("@tauri-apps/plugin-fs")
external tauriWriteTextFileRaw: (string, string) => promise<unit> = "writeTextFile"

/// Read a text file from the local filesystem.
let readTextFile = (path: string): promise<string> => {
if isGossamerRuntime() {
gossamerInvoke("__gossamer_fs_read_text", {"path": path})
} else if isTauriRuntime() {
tauriReadTextFileRaw(path)
} else {
Promise.reject(
JsError.throwWithMessage(
"No desktop runtime β€” filesystem access requires Gossamer or Tauri",
),
)
}
}

/// Write a text file to the local filesystem.
let writeTextFile = (path: string, contents: string): promise<unit> => {
if isGossamerRuntime() {
gossamerInvoke("__gossamer_fs_write_text", {"path": path, "contents": contents})
} else if isTauriRuntime() {
tauriWriteTextFileRaw(path, contents)
} else {
Promise.reject(
JsError.throwWithMessage(
"No desktop runtime β€” filesystem access requires Gossamer or Tauri",
),
)
}
}
}

======================================== */
Loading
Loading