From a4ae7c205a212d6fe7cc8ba95632aef183928064 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 24 Aug 2026 05:50:29 +0100 Subject: [PATCH 1/2] chore: embed original ReScript in .affine skeletons for easier porting --- src/App.affine | 34 +++ src/Bindings.affine | 131 +++++++++ src/RuntimeBridge.affine | 212 ++++++++++++++ src/proven/Proven_SafeHex.affine | 419 ++++++++++++++++++++++++++++ src/proven/Proven_SafeMath.affine | 183 ++++++++++++ src/proven/Proven_SafePath.affine | 67 +++++ src/proven/Proven_SafeString.affine | 140 ++++++++++ 7 files changed, 1186 insertions(+) diff --git a/src/App.affine b/src/App.affine index eb92faa..03ab4d7 100644 --- a/src/App.affine +++ b/src/App.affine @@ -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 + +======================================== */ diff --git a/src/Bindings.affine b/src/Bindings.affine index 4f77b1a..3a5611b 100644 --- a/src/Bindings.affine +++ b/src/Bindings.affine @@ -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 => { + invoke("check_gforth", ()) +} + +// Preview strike (dry-run) +let previewStrike = (bytes: array): promise => { + 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, path: string): promise => { + // 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 => { + 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 => { + 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): 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, 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): 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): string => { + switch Proven_SafeHex.encode(bytes) { + | Ok(hex) => hex + | Error(_) => "" + } +} + +// Decode hex string to bytes using SafeHex.decode +let hexToBytes = (hexStr: string): result, 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([]) + } +} + +======================================== */ diff --git a/src/RuntimeBridge.affine b/src/RuntimeBridge.affine index 585655f..8e3c245 100644 --- a/src/RuntimeBridge.affine +++ b/src/RuntimeBridge.affine @@ -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> = "open" + + @module("@tauri-apps/plugin-dialog") + external tauriSaveRaw: JSON.t => promise> = "save" + + /// Open a file picker dialog. + let \"open" = (opts: JSON.t): promise> => { + 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> => { + 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 = "readTextFile" + + @module("@tauri-apps/plugin-fs") + external tauriWriteTextFileRaw: (string, string) => promise = "writeTextFile" + + /// Read a text file from the local filesystem. + let readTextFile = (path: string): promise => { + 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 => { + 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", + ), + ) + } + } +} + +======================================== */ diff --git a/src/proven/Proven_SafeHex.affine b/src/proven/Proven_SafeHex.affine index 8ae6163..e68ea5c 100644 --- a/src/proven/Proven_SafeHex.affine +++ b/src/proven/Proven_SafeHex.affine @@ -5,3 +5,422 @@ module Proven_SafeHex; // TODO: Complete semantic implementation + + +/* === ORIGINAL RESCRIPT IMPLEMENTATION === +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2025 Hyperpolymath + +/** + * SafeHex - Hexadecimal encoding/decoding that cannot crash. + * + * Provides safe hex operations with constant-time comparison for security-sensitive use. + */ + +/** Error types for hex operations */ +type hexError = + | InvalidLength + | InvalidCharacter + | EmptyInput + +/** Hex character set (lowercase) */ +let hexChars = "0123456789abcdef" + +/** Hex character set (uppercase) */ +let hexCharsUpper = "0123456789ABCDEF" + +/** Convert a single hex character to its integer value */ +let hexCharToInt = (char: string): option => { + let code = String.charCodeAt(char, 0)->Float.toInt + if code >= 48 && code <= 57 { + // 0-9 + Some(code - 48) + } else if code >= 65 && code <= 70 { + // A-F + Some(code - 55) + } else if code >= 97 && code <= 102 { + // a-f + Some(code - 87) + } else { + None + } +} + +/** Convert an integer (0-15) to a hex character (lowercase) */ +let intToHexChar = (value: int): option => { + if value >= 0 && value <= 15 { + Some(String.charAt(hexChars, value)) + } else { + None + } +} + +/** Encode a byte array to a hex string (lowercase) */ +let encode = (bytes: array): result => { + if Array.length(bytes) == 0 { + Ok("") + } else { + let result = ref("") + let valid = ref(true) + + Array.forEach(bytes, byte => { + if valid.contents && byte >= 0 && byte <= 255 { + let high = Int.Bitwise.lsr(byte, 4) + let low = Int.Bitwise.land(byte, 0x0f) + result := + result.contents ++ + String.charAt(hexChars, high) ++ + String.charAt(hexChars, low) + } else { + valid := false + } + }) + + if valid.contents { + Ok(result.contents) + } else { + Error(InvalidCharacter) + } + } +} + +/** Encode a byte array to a hex string (uppercase) */ +let encodeUppercase = (bytes: array): result => { + if Array.length(bytes) == 0 { + Ok("") + } else { + let result = ref("") + let valid = ref(true) + + Array.forEach(bytes, byte => { + if valid.contents && byte >= 0 && byte <= 255 { + let high = Int.Bitwise.lsr(byte, 4) + let low = Int.Bitwise.land(byte, 0x0f) + result := + result.contents ++ + String.charAt(hexCharsUpper, high) ++ + String.charAt(hexCharsUpper, low) + } else { + valid := false + } + }) + + if valid.contents { + Ok(result.contents) + } else { + Error(InvalidCharacter) + } + } +} + +/** Encode a string to hex (using UTF-8 code points) */ +let encodeString = (input: string): string => { + let result = ref("") + for i in 0 to String.length(input) - 1 { + let code = String.charCodeAt(input, i)->Float.toInt + // Handle basic ASCII range (0-255) + if code <= 255 { + let high = Int.Bitwise.lsr(code, 4) + let low = Int.Bitwise.land(code, 0x0f) + result := + result.contents ++ String.charAt(hexChars, high) ++ String.charAt(hexChars, low) + } else { + // For characters > 255, encode as multi-byte + // High byte + let highByte = Int.Bitwise.lsr(code, 8) + let highHigh = Int.Bitwise.lsr(highByte, 4) + let highLow = Int.Bitwise.land(highByte, 0x0f) + result := + result.contents ++ + String.charAt(hexChars, highHigh) ++ + String.charAt(hexChars, highLow) + // Low byte + let lowByte = Int.Bitwise.land(code, 0xff) + let lowHigh = Int.Bitwise.lsr(lowByte, 4) + let lowLow = Int.Bitwise.land(lowByte, 0x0f) + result := + result.contents ++ + String.charAt(hexChars, lowHigh) ++ + String.charAt(hexChars, lowLow) + } + } + result.contents +} + +/** Decode a hex string to a byte array */ +let decode = (hexStr: string): result, hexError> => { + let normalized = String.toLowerCase(String.trim(hexStr)) + let length = String.length(normalized) + + if length == 0 { + Ok([]) + } else if mod(length, 2) != 0 { + Error(InvalidLength) + } else { + let numBytes = length / 2 + let bytes = Array.make(~length=numBytes, 0) + let valid = ref(true) + let errorType = ref(InvalidCharacter) + + for i in 0 to numBytes - 1 { + if valid.contents { + let highChar = String.charAt(normalized, i * 2) + let lowChar = String.charAt(normalized, i * 2 + 1) + switch (hexCharToInt(highChar), hexCharToInt(lowChar)) { + | (Some(high), Some(low)) => + Array.setUnsafe(bytes, i, Int.Bitwise.lsl(high, 4) + low) + | _ => + valid := false + errorType := InvalidCharacter + } + } + } + + if valid.contents { + Ok(bytes) + } else { + Error(errorType.contents) + } + } +} + +/** Decode a hex string to a string (ASCII range only) */ +let decodeToString = (hexStr: string): result => { + switch decode(hexStr) { + | Error(e) => Error(e) + | Ok(bytes) => + let chars = Array.map(bytes, byte => String.fromCharCode(byte)) + Ok(Array.join(chars, "")) + } +} + +/** Check if a string is valid hex */ +let isValidHex = (hexStr: string): bool => { + let trimmed = String.trim(hexStr) + let length = String.length(trimmed) + + if length == 0 || mod(length, 2) != 0 { + false + } else { + RegExp.test(%re("/^[0-9a-fA-F]+$/"), trimmed) + } +} + +/** Constant-time comparison of two hex strings + * + * SECURITY: This function compares strings in constant time to prevent + * timing attacks. It always examines the full length of both strings + * regardless of where differences occur. + */ +let constantTimeEqual = (hexA: string, hexB: string): bool => { + let normalizedA = String.toLowerCase(String.trim(hexA)) + let normalizedB = String.toLowerCase(String.trim(hexB)) + + let lengthA = String.length(normalizedA) + let lengthB = String.length(normalizedB) + + // Length comparison must not short-circuit + let lengthMatch = lengthA == lengthB + + // Use the longer length to ensure constant time + let maxLength = if lengthA > lengthB { + lengthA + } else { + lengthB + } + + // Accumulate differences using XOR + let diff = ref(0) + + for i in 0 to maxLength - 1 { + let charA = if i < lengthA { + String.charCodeAt(normalizedA, i)->Float.toInt + } else { + 0 + } + let charB = if i < lengthB { + String.charCodeAt(normalizedB, i)->Float.toInt + } else { + 0 + } + diff := Int.Bitwise.lor(diff.contents, Int.Bitwise.lxor(charA, charB)) + } + + lengthMatch && diff.contents == 0 +} + +/** Constant-time comparison of two byte arrays + * + * SECURITY: This function compares byte arrays in constant time. + */ +let constantTimeEqualBytes = (bytesA: array, bytesB: array): bool => { + let lengthA = Array.length(bytesA) + let lengthB = Array.length(bytesB) + + let lengthMatch = lengthA == lengthB + + let maxLength = if lengthA > lengthB { + lengthA + } else { + lengthB + } + + let diff = ref(0) + + for i in 0 to maxLength - 1 { + let byteA = if i < lengthA { + Array.getUnsafe(bytesA, i) + } else { + 0 + } + let byteB = if i < lengthB { + Array.getUnsafe(bytesB, i) + } else { + 0 + } + diff := Int.Bitwise.lor(diff.contents, Int.Bitwise.lxor(byteA, byteB)) + } + + lengthMatch && diff.contents == 0 +} + +/** Convert a hex string to lowercase */ +let toLowercase = (hexStr: string): result => { + if !isValidHex(hexStr) && String.length(String.trim(hexStr)) > 0 { + Error(InvalidCharacter) + } else { + Ok(String.toLowerCase(String.trim(hexStr))) + } +} + +/** Convert a hex string to uppercase */ +let toUppercase = (hexStr: string): result => { + if !isValidHex(hexStr) && String.length(String.trim(hexStr)) > 0 { + Error(InvalidCharacter) + } else { + Ok(String.toUpperCase(String.trim(hexStr))) + } +} + +/** Get the byte length of a hex string (hex length / 2) */ +let byteLength = (hexStr: string): result => { + let trimmed = String.trim(hexStr) + let length = String.length(trimmed) + + if length == 0 { + Ok(0) + } else if mod(length, 2) != 0 { + Error(InvalidLength) + } else if !isValidHex(trimmed) { + Error(InvalidCharacter) + } else { + Ok(length / 2) + } +} + +/** Pad a hex string with leading zeros to a specified byte length */ +let padToByteLength = (hexStr: string, targetByteLength: int): result => { + if targetByteLength < 0 { + Error(InvalidLength) + } else { + switch decode(hexStr) { + | Error(e) => Error(e) + | Ok(bytes) => + let currentLength = Array.length(bytes) + if currentLength > targetByteLength { + Error(InvalidLength) + } else { + let padding = Array.make(~length=targetByteLength - currentLength, 0) + let paddedBytes = Array.concat(padding, bytes) + encode(paddedBytes) + } + } + } +} + +/** XOR two hex strings of equal length */ +let xorHex = (hexA: string, hexB: string): result => { + switch (decode(hexA), decode(hexB)) { + | (Error(e), _) | (_, Error(e)) => Error(e) + | (Ok(bytesA), Ok(bytesB)) => + if Array.length(bytesA) != Array.length(bytesB) { + Error(InvalidLength) + } else { + let result = Array.mapWithIndex(bytesA, (byteA, i) => { + let byteB = Array.getUnsafe(bytesB, i) + Int.Bitwise.lxor(byteA, byteB) + }) + encode(result) + } + } +} + +/** Encode a byte array to a spaced hex string (for display) + * + * Example: [72, 101, 108] -> "48 65 6c" + */ +let encodeSpaced = (bytes: array): result => { + let length = Array.length(bytes) + if length == 0 { + Ok("") + } else { + let parts = Array.make(~length, "") + let valid = ref(true) + + for i in 0 to length - 1 { + if valid.contents { + let byte = Array.getUnsafe(bytes, i) + if byte >= 0 && byte <= 255 { + let high = Int.Bitwise.lsr(byte, 4) + let low = Int.Bitwise.land(byte, 0x0f) + let hex = String.charAt(hexChars, high) ++ String.charAt(hexChars, low) + Array.setUnsafe(parts, i, hex) + } else { + valid := false + } + } + } + + if valid.contents { + Ok(Array.join(parts, " ")) + } else { + Error(InvalidCharacter) + } + } +} + +/** Encode a byte array to a spaced uppercase hex string (for display) + * + * Example: [72, 101, 108] -> "48 65 6C" + */ +let encodeSpacedUppercase = (bytes: array): result => { + let length = Array.length(bytes) + if length == 0 { + Ok("") + } else { + let parts = Array.make(~length, "") + let valid = ref(true) + + for i in 0 to length - 1 { + if valid.contents { + let byte = Array.getUnsafe(bytes, i) + if byte >= 0 && byte <= 255 { + let high = Int.Bitwise.lsr(byte, 4) + let low = Int.Bitwise.land(byte, 0x0f) + let hex = String.charAt(hexCharsUpper, high) ++ String.charAt(hexCharsUpper, low) + Array.setUnsafe(parts, i, hex) + } else { + valid := false + } + } + } + + if valid.contents { + Ok(Array.join(parts, " ")) + } else { + Error(InvalidCharacter) + } + } +} + +======================================== */ diff --git a/src/proven/Proven_SafeMath.affine b/src/proven/Proven_SafeMath.affine index 749200b..123c657 100644 --- a/src/proven/Proven_SafeMath.affine +++ b/src/proven/Proven_SafeMath.affine @@ -5,3 +5,186 @@ module Proven_SafeMath; // TODO: Complete semantic implementation + + +/* === ORIGINAL RESCRIPT IMPLEMENTATION === +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2025 Hyperpolymath + +/** + * SafeMath - Arithmetic operations that cannot crash. + * + * All operations handle edge cases like division by zero, overflow, and underflow + * without throwing exceptions. Operations return None on failure. + */ + +/** Safe division that returns None on division by zero */ +let div = (numerator: int, denominator: int): option => { + if denominator == 0 { + None + } else { + Some(numerator / denominator) + } +} + +/** Safe division with default value */ +let divOr = (default: int, numerator: int, denominator: int): int => { + switch div(numerator, denominator) { + | Some(v) => v + | None => default + } +} + +/** Safe modulo that returns None on division by zero */ +let safeMod = (numerator: int, denominator: int): option => { + if denominator == 0 { + None + } else { + Some(mod(numerator, denominator)) + } +} + +/** Addition with overflow detection */ +let addChecked = (a: int, b: int): option => { + let result = a + b + // Check for overflow + if (a > 0 && b > 0 && result < 0) || (a < 0 && b < 0 && result > 0) { + None + } else { + Some(result) + } +} + +/** Subtraction with underflow detection */ +let subChecked = (a: int, b: int): option => { + let result = a - b + // Check for underflow + if (a > 0 && b < 0 && result < 0) || (a < 0 && b > 0 && result > 0) { + None + } else { + Some(result) + } +} + +/** Multiplication with overflow detection */ +let mulChecked = (a: int, b: int): option => { + if a == 0 || b == 0 { + Some(0) + } else { + let result = a * b + if result / a != b { + None + } else { + Some(result) + } + } +} + +/** Safe absolute value that handles MIN_INT correctly */ +let absSafe = (n: int): option => { + if n == min_int { + None + } else if n < 0 { + Some(-n) + } else { + Some(n) + } +} + +/** Clamp a value to range [lo, hi] */ +let clamp = (lo: int, hi: int, value: int): int => { + if value < lo { + lo + } else if value > hi { + hi + } else { + value + } +} + +/** Integer exponentiation with overflow detection */ +let rec powChecked = (base: int, exp: int): option => { + if exp < 0 { + None + } else if exp == 0 { + Some(1) + } else if exp == 1 { + Some(base) + } else { + switch powChecked(base, exp / 2) { + | None => None + | Some(half) => + switch mulChecked(half, half) { + | None => None + | Some(squared) => + if mod(exp, 2) == 0 { + Some(squared) + } else { + mulChecked(squared, base) + } + } + } + } +} + +/** Calculate percentage safely */ +let percentOf = (percent: int, total: int): option => { + switch mulChecked(percent, total) { + | None => None + | Some(product) => div(product, 100) + } +} + +/** Calculate what percentage part is of whole */ +let asPercent = (part: int, whole: int): option => { + switch mulChecked(part, 100) { + | None => None + | Some(scaled) => div(scaled, whole) + } +} + +/** Check if a value is within a range [lo, hi] (inclusive) */ +let inRange = (value: int, lo: int, hi: int): bool => { + value >= lo && value <= hi +} + +/** Check if a value is within a range, excluding specific values */ +let inRangeExcluding = (value: int, lo: int, hi: int, excluded: array): bool => { + if !inRange(value, lo, hi) { + false + } else { + !Array.some(excluded, e => e == value) + } +} + +/** Safe integer parsing from string, returns None on invalid input */ +let fromString = (str: string): option => { + let trimmed = String.trim(str) + if String.length(trimmed) == 0 { + None + } else { + // Check for valid integer format + let isValid = RegExp.test(%re("/^-?\\d+$/"), trimmed) + if !isValid { + None + } else { + let parsed = Int.fromString(trimmed) + parsed + } + } +} + +/** Safe integer parsing with range validation */ +let fromStringInRange = (str: string, lo: int, hi: int): option => { + switch fromString(str) { + | None => None + | Some(value) => + if inRange(value, lo, hi) { + Some(value) + } else { + None + } + } +} + +======================================== */ diff --git a/src/proven/Proven_SafePath.affine b/src/proven/Proven_SafePath.affine index 66f86b7..7b2ef28 100644 --- a/src/proven/Proven_SafePath.affine +++ b/src/proven/Proven_SafePath.affine @@ -5,3 +5,70 @@ module Proven_SafePath; // TODO: Complete semantic implementation + + +/* === ORIGINAL RESCRIPT IMPLEMENTATION === +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2025 Hyperpolymath + +/** + * SafePath - Filesystem path operations that cannot crash. + */ + +/** Check if a path contains directory traversal sequences */ +let hasTraversal = (path: string): bool => { + String.includes(path, "..") || + String.includes(path, "~") || + String.startsWith(path, "/") && String.includes(path, "..") +} + +/** Check if a path contains a null byte + * + * SECURITY: Null bytes can truncate paths in C-based filesystem APIs, + * allowing an attacker to bypass extension checks (e.g. "safe.txt\0.sh"). + */ +let hasNullByte = (path: string): bool => { + String.includes(path, "\x00") +} + +/** Check if a path is safe + * + * A path is safe if it: + * - contains no parent directory traversal (..) + * - contains no home directory expansion (~) + * - contains no null bytes + */ +let isSafe = (path: string): bool => { + !hasTraversal(path) && !hasNullByte(path) +} + +/** Sanitize a filename by removing dangerous characters */ +let sanitizeFilename = (filename: string): string => { + filename + ->String.replaceRegExp(%re("/\\.\\./g"), "_") + ->String.replaceRegExp(%re("/[\\/\\\\]/g"), "_") + ->String.replaceRegExp(%re("/[\\x00-\\x1f]/g"), "") + ->String.replaceRegExp(%re("/[<>:\"\\|\\?\\*]/g"), "_") +} + +/** Safely join path components, rejecting traversal attempts */ +let safeJoin = (base: string, parts: array): option => { + let hasUnsafe = parts->Array.some(part => hasTraversal(part)) + if hasUnsafe { + None + } else { + let result = ref(base) + parts->Array.forEach(part => { + let sanitized = sanitizeFilename(part) + let base = result.contents + if String.endsWith(base, "/") { + result := base ++ sanitized + } else { + result := base ++ "/" ++ sanitized + } + }) + Some(result.contents) + } +} + +======================================== */ diff --git a/src/proven/Proven_SafeString.affine b/src/proven/Proven_SafeString.affine index e0b1dc2..ce6ccf0 100644 --- a/src/proven/Proven_SafeString.affine +++ b/src/proven/Proven_SafeString.affine @@ -5,3 +5,143 @@ module Proven_SafeString; // TODO: Complete semantic implementation + + +/* === ORIGINAL RESCRIPT IMPLEMENTATION === +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2025 Hyperpolymath + +/** + * SafeString - String operations that cannot crash. + * + * Provides safe escaping for SQL, HTML, and JavaScript. + */ + +/** Escape a string for safe SQL interpolation */ +let escapeSql = (value: string): string => { + String.replaceRegExp(value, %re("/'/g"), "''") +} + +/** Escape a string for safe HTML insertion */ +let escapeHtml = (value: string): string => { + value + ->String.replaceRegExp(%re("/&/g"), "&") + ->String.replaceRegExp(%re("/String.replaceRegExp(%re("/>/g"), ">") + ->String.replaceRegExp(%re("/\"/g"), """) + ->String.replaceRegExp(%re("/'/g"), "'") +} + +/** Escape a string for safe JavaScript string literal insertion */ +let escapeJs = (value: string): string => { + value + ->String.replaceRegExp(%re("/\\\\/g"), "\\\\") + ->String.replaceRegExp(%re("/\"/g"), "\\\"") + ->String.replaceRegExp(%re("/'/g"), "\\'") + ->String.replaceRegExp(%re("/\n/g"), "\\n") + ->String.replaceRegExp(%re("/\r/g"), "\\r") + ->String.replaceRegExp(%re("/\t/g"), "\\t") +} + +/** Convert a string to an array of ASCII code points (0-255 only) + * + * Returns Error if any character is outside ASCII range (> 255). + * For pure ASCII (0-127), this is always safe. + */ +let toCodePoints = (str: string): result, string> => { + let length = String.length(str) + if length == 0 { + Ok([]) + } else { + let codes = Array.make(~length, 0) + let valid = ref(true) + let errorIdx = ref(0) + + for i in 0 to length - 1 { + if valid.contents { + let code = String.charCodeAt(str, i)->Float.toInt + if code > 255 { + valid := false + errorIdx := i + } else { + Array.setUnsafe(codes, i, code) + } + } + } + + if valid.contents { + Ok(codes) + } else { + Error(`Character at position ${Int.toString(errorIdx.contents)} is outside ASCII range`) + } + } +} + +/** Convert an array of code points to a string + * + * All code points must be in range 0-65535 (valid UTF-16 code units). + */ +let fromCodePoints = (codes: array): result => { + let length = Array.length(codes) + if length == 0 { + Ok("") + } else { + let valid = ref(true) + let errorIdx = ref(0) + let chars = Array.make(~length, "") + + for i in 0 to length - 1 { + if valid.contents { + let code = Array.getUnsafe(codes, i) + if code < 0 || code > 65535 { + valid := false + errorIdx := i + } else { + Array.setUnsafe(chars, i, String.fromCharCode(code)) + } + } + } + + if valid.contents { + Ok(Array.join(chars, "")) + } else { + Error(`Invalid code point at position ${Int.toString(errorIdx.contents)}`) + } + } +} + +/** Check if a string contains only ASCII characters (0-127) */ +let isAscii = (str: string): bool => { + let length = String.length(str) + let valid = ref(true) + + for i in 0 to length - 1 { + if valid.contents { + let code = String.charCodeAt(str, i)->Float.toInt + if code > 127 { + valid := false + } + } + } + + valid.contents +} + +/** Check if a string contains only printable ASCII (32-126) */ +let isPrintableAscii = (str: string): bool => { + let length = String.length(str) + let valid = ref(true) + + for i in 0 to length - 1 { + if valid.contents { + let code = String.charCodeAt(str, i)->Float.toInt + if code < 32 || code > 126 { + valid := false + } + } + } + + valid.contents +} + +======================================== */ From 01ed9762d804af546b1f3f6234f288b9a6a986c7 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 24 Aug 2026 05:52:19 +0100 Subject: [PATCH 2/2] refactor: naive regex translation from ReScript to AffineScript --- src/App.affine | 40 ++-- src/Bindings.affine | 121 ++++++------ src/RuntimeBridge.affine | 96 +++++----- src/proven/Proven_SafeHex.affine | 282 ++++++++++++++-------------- src/proven/Proven_SafeMath.affine | 90 +++++---- src/proven/Proven_SafePath.affine | 44 ++--- src/proven/Proven_SafeString.affine | 102 +++++----- 7 files changed, 369 insertions(+), 406 deletions(-) diff --git a/src/App.affine b/src/App.affine index 03ab4d7..7330b4b 100644 --- a/src/App.affine +++ b/src/App.affine @@ -4,38 +4,32 @@ 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 +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 +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 +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 - -======================================== */ +let maxByte = Types::Constraints::maxByte; +let forbiddenNbsp = Types::Constraints::forbiddenNbsp; +let forbiddenUtf8 = Types::Constraints::forbiddenUtf8; +let forbiddenBytes = Types::Constraints::forbiddenBytes; diff --git a/src/Bindings.affine b/src/Bindings.affine index 3a5611b..bb5c0c1 100644 --- a/src/Bindings.affine +++ b/src/Bindings.affine @@ -4,44 +4,41 @@ 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 +let invoke = RuntimeBridge::invoke; // Re-export types -type contaminant = Types.contaminant -type previewResult = Types.previewResult -type verifyResult = Types.verifyResult +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 => { +pub fn checkGforth() -> promise { invoke("check_gforth", ()) } // Preview strike (dry-run) -let previewStrike = (bytes: array): promise => { +pub fn previewStrike(bytes: [Int]) -> promise { 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)); } +// TODO: Translate raw JS block to AffineScript extern or JS FFI +// 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, path: string): promise => { +pub fn executeStrike(bytes: [Int], path: String) -> promise { // Validate path doesn't contain traversal attacks - if !Proven_SafePath.isSafe(path) { + if !Proven_SafePath::isSafe(path) { rejectWithMessage("Invalid path: contains traversal sequences") } else { invoke("execute_forth_strike", {"bytes": bytes, "path": path}) @@ -49,8 +46,8 @@ let executeStrike = (bytes: array, path: string): promise => { } // Verify substrate file (with path validation using SafePath) -let verifySubstrate = (path: string): promise => { - if !Proven_SafePath.isSafe(path) { +pub fn verifySubstrate(path: String) -> promise { + if !Proven_SafePath::isSafe(path) { rejectWithMessage("Invalid path: contains traversal sequences") } else { invoke("verify_substrate", {"path": path}) @@ -59,80 +56,78 @@ let verifySubstrate = (path: string): promise => { // Utilities using proven library -// Convert string to byte array using SafeString.toCodePoints -let stringToBytes = (str: string): array => { - switch Proven_SafeString.toCodePoints(str) { - | Ok(bytes) => bytes - | Error(_) => [] // Return empty on non-ASCII input +// Convert String to byte array using SafeString::toCodePoints +pub fn stringToBytes(str: String) -> [Int] { + match 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): string => { - switch Proven_SafeString.fromCodePoints(bytes) { - | Ok(str) => str - | Error(_) => "" // Return empty on invalid code points +// Convert byte array to String using SafeString::fromCodePoints +pub fn bytesToString(bytes: [Int]) -> String { + match 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 a single byte (uses SafeMath via Types::Constraints) +pub fn isValidByte(byte: Int) -> Bool { + Types::Constraints::isValidByte(byte) } // Validate path safety using SafePath -let isValidPath = (path: string): bool => { - Proven_SafePath.isSafe(path) +pub fn isValidPath(path: String) -> Bool { + Proven_SafePath::isSafe(path) } -// Parse comma-separated bytes using SafeMath.fromString -let parseByteString = (str: string): result, string> => { - let parts = str - ->String.trim - ->String.split(",") - ->Array.map(String.trim) - ->Array.filter(s => s->String.length > 0) +// Parse comma-separated bytes using SafeMath::fromString +let parseByteString = (str: String): result<[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)) + // Use SafeMath::fromString for safe integer parsing + let bytes = parts->Array::map(s => Proven_SafeMath::fromString(s)) - if bytes->Array.some(Option.isNone) { + 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)) + 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`) + Error(`Byte at position ${Int::toString(invalid)} is invalid`) } else { Ok(values) } } } -// Format bytes as spaced hex using SafeHex.encodeSpaced -let bytesToHex = (bytes: array): string => { - switch Proven_SafeHex.encodeSpaced(bytes) { - | Ok(hex) => hex - | Error(_) => "" // Return empty on invalid bytes +// Format bytes as spaced hex using SafeHex::encodeSpaced +pub fn bytesToHex(bytes: [Int]) -> String { + match 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): string => { - switch Proven_SafeHex.encode(bytes) { - | Ok(hex) => hex - | Error(_) => "" +// Format bytes as compact hex (no spaces) using SafeHex::encode +pub fn bytesToHexCompact(bytes: [Int]) -> String { + match Proven_SafeHex::encode(bytes) { + Ok(hex) => hex + Error(_) => "" } } -// Decode hex string to bytes using SafeHex.decode -let hexToBytes = (hexStr: string): result, 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([]) +// Decode hex String to bytes using SafeHex::decode +let hexToBytes = (hexStr: String): result<[Int], String> => { + match 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([]) } } - -======================================== */ diff --git a/src/RuntimeBridge.affine b/src/RuntimeBridge.affine index 8e3c245..b939335 100644 --- a/src/RuntimeBridge.affine +++ b/src/RuntimeBridge.affine @@ -4,13 +4,9 @@ module RuntimeBridge; -// TODO: Complete semantic implementation - - -/* === ORIGINAL RESCRIPT IMPLEMENTATION === // SPDX-License-Identifier: MPL-2.0 -/// RuntimeBridge — Unified IPC bridge for DotMatrix-FilePrinter. +/// 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 @@ -22,43 +18,46 @@ module RuntimeBridge; /// 2. Tauri (`window.__TAURI_INTERNALS__`) — legacy, transition /// 3. Browser (direct HTTP fetch) — development fallback /// -/// Migration path: Bindings.res replaces +/// Migration path: Bindings::res replaces /// `@module("@tauri-apps/api/core") external invoke: ...` /// with -/// `let invoke = RuntimeBridge.invoke` +/// `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(` +// TODO: Translate raw JS block to AffineScript extern or JS FFI +// %%raw(` function isGossamerRuntime() { return typeof window !== 'undefined' && typeof window.__gossamer_invoke === 'function'; } `) -@val external isGossamerRuntime: unit => bool = "isGossamerRuntime" +@val external isGossamerRuntime: unit => Bool = "isGossamerRuntime" -%%raw(` +// TODO: Translate raw JS block to AffineScript extern or JS FFI +// %%raw(` function gossamerInvoke(cmd, args) { return window.__gossamer_invoke(cmd, args); } `) -@val external gossamerInvoke: (string, 'a) => promise<'b> = "gossamerInvoke" +@val external gossamerInvoke: (String, 'a) => promise<'b> = "gossamerInvoke" /// Tauri IPC: injected by the Tauri runtime into the webview. -%%raw(` +// TODO: Translate raw JS block to AffineScript extern or JS FFI +// %%raw(` function isTauriRuntime() { return typeof window !== 'undefined' && window.__TAURI_INTERNALS__ != null && !window.__TAURI_INTERNALS__.__BROWSER_SHIM__; } `) -@val external isTauriRuntime: unit => bool = "isTauriRuntime" +@val external isTauriRuntime: unit => Bool = "isTauriRuntime" @module("@tauri-apps/api/core") -external tauriInvoke: (string, 'a) => promise<'b> = "invoke" +external tauriInvoke: (String, 'a) => promise<'b> = "invoke" // --------------------------------------------------------------------------- // Unified invoke — detects runtime and dispatches @@ -70,7 +69,8 @@ type runtime = | Tauri | BrowserOnly -%%raw(` +// TODO: Translate raw JS block to AffineScript extern or JS FFI +// %%raw(` var _detectedRuntime = null; function detectRuntime() { if (_detectedRuntime !== null) return _detectedRuntime; @@ -84,14 +84,14 @@ function detectRuntime() { return _detectedRuntime; } `) -@val external detectRuntimeRaw: unit => string = "detectRuntime" +@val external detectRuntimeRaw: unit => String = "detectRuntime" /// Detect and return the current runtime. -let detectRuntime = (): runtime => { - switch detectRuntimeRaw() { - | "gossamer" => Gossamer - | "tauri" => Tauri - | _ => BrowserOnly +pub fn detectRuntime() -> runtime { + match detectRuntimeRaw() { + "gossamer" => Gossamer + "tauri" => Tauri + _ => BrowserOnly } } @@ -102,14 +102,14 @@ let detectRuntime = (): runtime => { /// - 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> => { +let invoke = (cmd: String, args: 'a): promise<'b> => { if isGossamerRuntime() { gossamerInvoke(cmd, args) } else if isTauriRuntime() { tauriInvoke(cmd, args) } else { - Promise.reject( - JsError.throwWithMessage( + Promise::reject( + JsError::throwWithMessage( `No desktop runtime — "${cmd}" requires Gossamer or Tauri`, ), ) @@ -117,16 +117,16 @@ let invoke = (cmd: string, args: 'a): promise<'b> => { } /// Check whether any desktop runtime is available. -let hasDesktopRuntime = (): bool => { +pub fn hasDesktopRuntime() -> Bool { isGossamerRuntime() || isTauriRuntime() } /// Get a human-readable name for the current runtime. -let runtimeName = (): string => { - switch detectRuntime() { - | Gossamer => "Gossamer" - | Tauri => "Tauri" - | BrowserOnly => "Browser" +pub fn runtimeName() -> String { + match detectRuntime() { + Gossamer => "Gossamer" + Tauri => "Tauri" + BrowserOnly => "Browser" } } @@ -135,23 +135,23 @@ let runtimeName = (): string => { // --------------------------------------------------------------------------- module Dialog = { - /// Gossamer file dialog: calls gossamer_dialog_open via IPC. + /// 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> = "open" + external tauriOpenRaw: JSON::t => promise> = "open" @module("@tauri-apps/plugin-dialog") - external tauriSaveRaw: JSON.t => promise> = "save" + external tauriSaveRaw: JSON::t => promise> = "save" /// Open a file picker dialog. - let \"open" = (opts: JSON.t): promise> => { + let \"open" = (opts: JSON::t): promise> => { if isGossamerRuntime() { gossamerInvoke("__gossamer_dialog_open", opts) } else if isTauriRuntime() { tauriOpenRaw(opts) } else { - Promise.reject( - JsError.throwWithMessage( + Promise::reject( + JsError::throwWithMessage( "No desktop runtime — file dialogs require Gossamer or Tauri", ), ) @@ -159,14 +159,14 @@ module Dialog = { } /// Open a save dialog. - let save = (opts: JSON.t): promise> => { + let save = (opts: JSON::t): promise> => { if isGossamerRuntime() { gossamerInvoke("__gossamer_dialog_save", opts) } else if isTauriRuntime() { tauriSaveRaw(opts) } else { - Promise.reject( - JsError.throwWithMessage( + Promise::reject( + JsError::throwWithMessage( "No desktop runtime — save dialogs require Gossamer or Tauri", ), ) @@ -180,20 +180,20 @@ module Dialog = { module Fs = { @module("@tauri-apps/plugin-fs") - external tauriReadTextFileRaw: string => promise = "readTextFile" + external tauriReadTextFileRaw: String => promise = "readTextFile" @module("@tauri-apps/plugin-fs") - external tauriWriteTextFileRaw: (string, string) => promise = "writeTextFile" + external tauriWriteTextFileRaw: (String, String) => promise = "writeTextFile" /// Read a text file from the local filesystem. - let readTextFile = (path: string): promise => { + pub fn readTextFile(path: String) -> promise { if isGossamerRuntime() { gossamerInvoke("__gossamer_fs_read_text", {"path": path}) } else if isTauriRuntime() { tauriReadTextFileRaw(path) } else { - Promise.reject( - JsError.throwWithMessage( + Promise::reject( + JsError::throwWithMessage( "No desktop runtime — filesystem access requires Gossamer or Tauri", ), ) @@ -201,19 +201,17 @@ module Fs = { } /// Write a text file to the local filesystem. - let writeTextFile = (path: string, contents: string): promise => { + pub fn writeTextFile(path: String, contents: String) -> promise { if isGossamerRuntime() { gossamerInvoke("__gossamer_fs_write_text", {"path": path, "contents": contents}) } else if isTauriRuntime() { tauriWriteTextFileRaw(path, contents) } else { - Promise.reject( - JsError.throwWithMessage( + Promise::reject( + JsError::throwWithMessage( "No desktop runtime — filesystem access requires Gossamer or Tauri", ), ) } } } - -======================================== */ diff --git a/src/proven/Proven_SafeHex.affine b/src/proven/Proven_SafeHex.affine index e68ea5c..620ce55 100644 --- a/src/proven/Proven_SafeHex.affine +++ b/src/proven/Proven_SafeHex.affine @@ -4,10 +4,6 @@ module Proven_SafeHex; -// TODO: Complete semantic implementation - - -/* === ORIGINAL RESCRIPT IMPLEMENTATION === // SPDX-License-Identifier: MPL-2.0 // SPDX-FileCopyrightText: 2025 Hyperpolymath @@ -24,14 +20,14 @@ type hexError = | EmptyInput /** Hex character set (lowercase) */ -let hexChars = "0123456789abcdef" +let hexChars = "0123456789abcdef"; /** Hex character set (uppercase) */ -let hexCharsUpper = "0123456789ABCDEF" +let hexCharsUpper = "0123456789ABCDEF"; /** Convert a single hex character to its integer value */ -let hexCharToInt = (char: string): option => { - let code = String.charCodeAt(char, 0)->Float.toInt +pub fn hexCharToInt(char: String) -> Option { + let code = String::charCodeAt(char, 0)->Float::toInt; if code >= 48 && code <= 57 { // 0-9 Some(code - 48) @@ -47,30 +43,30 @@ let hexCharToInt = (char: string): option => { } /** Convert an integer (0-15) to a hex character (lowercase) */ -let intToHexChar = (value: int): option => { +pub fn intToHexChar(value: Int) -> Option { if value >= 0 && value <= 15 { - Some(String.charAt(hexChars, value)) + Some(String::charAt(hexChars, value)) } else { None } } -/** Encode a byte array to a hex string (lowercase) */ -let encode = (bytes: array): result => { - if Array.length(bytes) == 0 { +/** Encode a byte array to a hex String (lowercase) */ +let encode = (bytes: [Int]): result => { + if Array::length(bytes) == 0 { Ok("") } else { - let result = ref("") - let valid = ref(true) + let result = ref(""); + let valid = ref(true); - Array.forEach(bytes, byte => { + Array::forEach(bytes, byte => { if valid.contents && byte >= 0 && byte <= 255 { - let high = Int.Bitwise.lsr(byte, 4) - let low = Int.Bitwise.land(byte, 0x0f) + let high = Int::Bitwise::lsr(byte, 4); + let low = Int::Bitwise::land(byte, 0x0f); result := result.contents ++ - String.charAt(hexChars, high) ++ - String.charAt(hexChars, low) + String::charAt(hexChars, high) ++ + String::charAt(hexChars, low) } else { valid := false } @@ -84,22 +80,22 @@ let encode = (bytes: array): result => { } } -/** Encode a byte array to a hex string (uppercase) */ -let encodeUppercase = (bytes: array): result => { - if Array.length(bytes) == 0 { +/** Encode a byte array to a hex String (uppercase) */ +let encodeUppercase = (bytes: [Int]): result => { + if Array::length(bytes) == 0 { Ok("") } else { - let result = ref("") - let valid = ref(true) + let result = ref(""); + let valid = ref(true); - Array.forEach(bytes, byte => { + Array::forEach(bytes, byte => { if valid.contents && byte >= 0 && byte <= 255 { - let high = Int.Bitwise.lsr(byte, 4) - let low = Int.Bitwise.land(byte, 0x0f) + let high = Int::Bitwise::lsr(byte, 4); + let low = Int::Bitwise::land(byte, 0x0f); result := result.contents ++ - String.charAt(hexCharsUpper, high) ++ - String.charAt(hexCharsUpper, low) + String::charAt(hexCharsUpper, high) ++ + String::charAt(hexCharsUpper, low) } else { valid := false } @@ -113,63 +109,63 @@ let encodeUppercase = (bytes: array): result => { } } -/** Encode a string to hex (using UTF-8 code points) */ -let encodeString = (input: string): string => { - let result = ref("") - for i in 0 to String.length(input) - 1 { - let code = String.charCodeAt(input, i)->Float.toInt +/** Encode a String to hex (using UTF-8 code points) */ +pub fn encodeString(input: String) -> String { + let result = ref(""); + for i in 0 to String::length(input) - 1 { + let code = String::charCodeAt(input, i)->Float::toInt; // Handle basic ASCII range (0-255) if code <= 255 { - let high = Int.Bitwise.lsr(code, 4) - let low = Int.Bitwise.land(code, 0x0f) + let high = Int::Bitwise::lsr(code, 4); + let low = Int::Bitwise::land(code, 0x0f); result := - result.contents ++ String.charAt(hexChars, high) ++ String.charAt(hexChars, low) + result.contents ++ String::charAt(hexChars, high) ++ String::charAt(hexChars, low) } else { // For characters > 255, encode as multi-byte // High byte - let highByte = Int.Bitwise.lsr(code, 8) - let highHigh = Int.Bitwise.lsr(highByte, 4) - let highLow = Int.Bitwise.land(highByte, 0x0f) + let highByte = Int::Bitwise::lsr(code, 8); + let highHigh = Int::Bitwise::lsr(highByte, 4); + let highLow = Int::Bitwise::land(highByte, 0x0f); result := result.contents ++ - String.charAt(hexChars, highHigh) ++ - String.charAt(hexChars, highLow) + String::charAt(hexChars, highHigh) ++ + String::charAt(hexChars, highLow) // Low byte - let lowByte = Int.Bitwise.land(code, 0xff) - let lowHigh = Int.Bitwise.lsr(lowByte, 4) - let lowLow = Int.Bitwise.land(lowByte, 0x0f) + let lowByte = Int::Bitwise::land(code, 0xff); + let lowHigh = Int::Bitwise::lsr(lowByte, 4); + let lowLow = Int::Bitwise::land(lowByte, 0x0f); result := result.contents ++ - String.charAt(hexChars, lowHigh) ++ - String.charAt(hexChars, lowLow) + String::charAt(hexChars, lowHigh) ++ + String::charAt(hexChars, lowLow) } } result.contents } -/** Decode a hex string to a byte array */ -let decode = (hexStr: string): result, hexError> => { - let normalized = String.toLowerCase(String.trim(hexStr)) - let length = String.length(normalized) +/** Decode a hex String to a byte array */ +let decode = (hexStr: String): result<[Int], hexError> => { + let normalized = String::toLowerCase(String::trim(hexStr)); + let length = String::length(normalized); if length == 0 { Ok([]) } else if mod(length, 2) != 0 { Error(InvalidLength) } else { - let numBytes = length / 2 - let bytes = Array.make(~length=numBytes, 0) - let valid = ref(true) - let errorType = ref(InvalidCharacter) + let numBytes = length / 2; + let bytes = Array::make(~length=numBytes, 0); + let valid = ref(true); + let errorType = ref(InvalidCharacter); for i in 0 to numBytes - 1 { if valid.contents { - let highChar = String.charAt(normalized, i * 2) - let lowChar = String.charAt(normalized, i * 2 + 1) - switch (hexCharToInt(highChar), hexCharToInt(lowChar)) { - | (Some(high), Some(low)) => - Array.setUnsafe(bytes, i, Int.Bitwise.lsl(high, 4) + low) - | _ => + let highChar = String::charAt(normalized, i * 2); + let lowChar = String::charAt(normalized, i * 2 + 1); + match (hexCharToInt(highChar), hexCharToInt(lowChar)) { + (Some(high), Some(low)) => + Array::setUnsafe(bytes, i, Int::Bitwise::lsl(high, 4) + low) + _ => valid := false errorType := InvalidCharacter } @@ -184,25 +180,25 @@ let decode = (hexStr: string): result, hexError> => { } } -/** Decode a hex string to a string (ASCII range only) */ -let decodeToString = (hexStr: string): result => { - switch decode(hexStr) { - | Error(e) => Error(e) - | Ok(bytes) => - let chars = Array.map(bytes, byte => String.fromCharCode(byte)) - Ok(Array.join(chars, "")) +/** Decode a hex String to a String (ASCII range only) */ +let decodeToString = (hexStr: String): result => { + match decode(hexStr) { + Error(e) => Error(e) + Ok(bytes) => + let chars = Array::map(bytes, byte => String::fromCharCode(byte)) + Ok(Array::join(chars, "")) } } -/** Check if a string is valid hex */ -let isValidHex = (hexStr: string): bool => { - let trimmed = String.trim(hexStr) - let length = String.length(trimmed) +/** Check if a String is valid hex */ +pub fn isValidHex(hexStr: String) -> Bool { + let trimmed = String::trim(hexStr); + let length = String::length(trimmed); if length == 0 || mod(length, 2) != 0 { false } else { - RegExp.test(%re("/^[0-9a-fA-F]+$/"), trimmed) + RegExp::test(%re("/^[0-9a-fA-F]+$/"), trimmed) } } @@ -212,15 +208,15 @@ let isValidHex = (hexStr: string): bool => { * timing attacks. It always examines the full length of both strings * regardless of where differences occur. */ -let constantTimeEqual = (hexA: string, hexB: string): bool => { - let normalizedA = String.toLowerCase(String.trim(hexA)) - let normalizedB = String.toLowerCase(String.trim(hexB)) +pub fn constantTimeEqual(hexA: String, hexB: String) -> Bool { + let normalizedA = String::toLowerCase(String::trim(hexA)); + let normalizedB = String::toLowerCase(String::trim(hexB)); - let lengthA = String.length(normalizedA) - let lengthB = String.length(normalizedB) + let lengthA = String::length(normalizedA); + let lengthB = String::length(normalizedB); // Length comparison must not short-circuit - let lengthMatch = lengthA == lengthB + let lengthMatch = lengthA == lengthB; // Use the longer length to ensure constant time let maxLength = if lengthA > lengthB { @@ -230,20 +226,20 @@ let constantTimeEqual = (hexA: string, hexB: string): bool => { } // Accumulate differences using XOR - let diff = ref(0) + let diff = ref(0); for i in 0 to maxLength - 1 { let charA = if i < lengthA { - String.charCodeAt(normalizedA, i)->Float.toInt + String::charCodeAt(normalizedA, i)->Float::toInt } else { 0 } let charB = if i < lengthB { - String.charCodeAt(normalizedB, i)->Float.toInt + String::charCodeAt(normalizedB, i)->Float::toInt } else { 0 } - diff := Int.Bitwise.lor(diff.contents, Int.Bitwise.lxor(charA, charB)) + diff := Int::Bitwise::lor(diff.contents, Int::Bitwise::lxor(charA, charB)) } lengthMatch && diff.contents == 0 @@ -253,11 +249,11 @@ let constantTimeEqual = (hexA: string, hexB: string): bool => { * * SECURITY: This function compares byte arrays in constant time. */ -let constantTimeEqualBytes = (bytesA: array, bytesB: array): bool => { - let lengthA = Array.length(bytesA) - let lengthB = Array.length(bytesB) +pub fn constantTimeEqualBytes(bytesA: [Int], bytesB: [Int]) -> Bool { + let lengthA = Array::length(bytesA); + let lengthB = Array::length(bytesB); - let lengthMatch = lengthA == lengthB + let lengthMatch = lengthA == lengthB; let maxLength = if lengthA > lengthB { lengthA @@ -265,47 +261,47 @@ let constantTimeEqualBytes = (bytesA: array, bytesB: array): bool => { lengthB } - let diff = ref(0) + let diff = ref(0); for i in 0 to maxLength - 1 { let byteA = if i < lengthA { - Array.getUnsafe(bytesA, i) + Array::getUnsafe(bytesA, i) } else { 0 } let byteB = if i < lengthB { - Array.getUnsafe(bytesB, i) + Array::getUnsafe(bytesB, i) } else { 0 } - diff := Int.Bitwise.lor(diff.contents, Int.Bitwise.lxor(byteA, byteB)) + diff := Int::Bitwise::lor(diff.contents, Int::Bitwise::lxor(byteA, byteB)) } lengthMatch && diff.contents == 0 } -/** Convert a hex string to lowercase */ -let toLowercase = (hexStr: string): result => { - if !isValidHex(hexStr) && String.length(String.trim(hexStr)) > 0 { +/** Convert a hex String to lowercase */ +let toLowercase = (hexStr: String): result => { + if !isValidHex(hexStr) && String::length(String::trim(hexStr)) > 0 { Error(InvalidCharacter) } else { - Ok(String.toLowerCase(String.trim(hexStr))) + Ok(String::toLowerCase(String::trim(hexStr))) } } -/** Convert a hex string to uppercase */ -let toUppercase = (hexStr: string): result => { - if !isValidHex(hexStr) && String.length(String.trim(hexStr)) > 0 { +/** Convert a hex String to uppercase */ +let toUppercase = (hexStr: String): result => { + if !isValidHex(hexStr) && String::length(String::trim(hexStr)) > 0 { Error(InvalidCharacter) } else { - Ok(String.toUpperCase(String.trim(hexStr))) + Ok(String::toUpperCase(String::trim(hexStr))) } } -/** Get the byte length of a hex string (hex length / 2) */ -let byteLength = (hexStr: string): result => { - let trimmed = String.trim(hexStr) - let length = String.length(trimmed) +/** Get the byte length of a hex String (hex length / 2) */ +let byteLength = (hexStr: String): result => { + let trimmed = String::trim(hexStr); + let length = String::length(trimmed); if length == 0 { Ok(0) @@ -318,20 +314,20 @@ let byteLength = (hexStr: string): result => { } } -/** Pad a hex string with leading zeros to a specified byte length */ -let padToByteLength = (hexStr: string, targetByteLength: int): result => { +/** Pad a hex String with leading zeros to a specified byte length */ +let padToByteLength = (hexStr: String, targetByteLength: Int): result => { if targetByteLength < 0 { Error(InvalidLength) } else { - switch decode(hexStr) { - | Error(e) => Error(e) - | Ok(bytes) => - let currentLength = Array.length(bytes) + match decode(hexStr) { + Error(e) => Error(e) + Ok(bytes) => + let currentLength = Array::length(bytes); if currentLength > targetByteLength { Error(InvalidLength) } else { - let padding = Array.make(~length=targetByteLength - currentLength, 0) - let paddedBytes = Array.concat(padding, bytes) + let padding = Array::make(~length=targetByteLength - currentLength, 0); + let paddedBytes = Array::concat(padding, bytes); encode(paddedBytes) } } @@ -339,42 +335,42 @@ let padToByteLength = (hexStr: string, targetByteLength: int): result => { - switch (decode(hexA), decode(hexB)) { - | (Error(e), _) | (_, Error(e)) => Error(e) - | (Ok(bytesA), Ok(bytesB)) => - if Array.length(bytesA) != Array.length(bytesB) { +let xorHex = (hexA: String, hexB: String): result => { + match (decode(hexA), decode(hexB)) { + (Error(e), _) | (_, Error(e)) => Error(e) + (Ok(bytesA), Ok(bytesB)) => + if Array::length(bytesA) != Array::length(bytesB) { Error(InvalidLength) } else { - let result = Array.mapWithIndex(bytesA, (byteA, i) => { - let byteB = Array.getUnsafe(bytesB, i) - Int.Bitwise.lxor(byteA, byteB) + let result = Array::mapWithIndex(bytesA, (byteA, i) => { + let byteB = Array::getUnsafe(bytesB, i); + Int::Bitwise::lxor(byteA, byteB) }) encode(result) } } } -/** Encode a byte array to a spaced hex string (for display) +/** Encode a byte array to a spaced hex String (for display) * * Example: [72, 101, 108] -> "48 65 6c" */ -let encodeSpaced = (bytes: array): result => { - let length = Array.length(bytes) +let encodeSpaced = (bytes: [Int]): result => { + let length = Array::length(bytes); if length == 0 { Ok("") } else { - let parts = Array.make(~length, "") - let valid = ref(true) + let parts = Array::make(~length, ""); + let valid = ref(true); for i in 0 to length - 1 { if valid.contents { - let byte = Array.getUnsafe(bytes, i) + let byte = Array::getUnsafe(bytes, i); if byte >= 0 && byte <= 255 { - let high = Int.Bitwise.lsr(byte, 4) - let low = Int.Bitwise.land(byte, 0x0f) - let hex = String.charAt(hexChars, high) ++ String.charAt(hexChars, low) - Array.setUnsafe(parts, i, hex) + let high = Int::Bitwise::lsr(byte, 4); + let low = Int::Bitwise::land(byte, 0x0f); + let hex = String::charAt(hexChars, high) ++ String::charAt(hexChars, low); + Array::setUnsafe(parts, i, hex) } else { valid := false } @@ -382,33 +378,33 @@ let encodeSpaced = (bytes: array): result => { } if valid.contents { - Ok(Array.join(parts, " ")) + Ok(Array::join(parts, " ")) } else { Error(InvalidCharacter) } } } -/** Encode a byte array to a spaced uppercase hex string (for display) +/** Encode a byte array to a spaced uppercase hex String (for display) * * Example: [72, 101, 108] -> "48 65 6C" */ -let encodeSpacedUppercase = (bytes: array): result => { - let length = Array.length(bytes) +let encodeSpacedUppercase = (bytes: [Int]): result => { + let length = Array::length(bytes); if length == 0 { Ok("") } else { - let parts = Array.make(~length, "") - let valid = ref(true) + let parts = Array::make(~length, ""); + let valid = ref(true); for i in 0 to length - 1 { if valid.contents { - let byte = Array.getUnsafe(bytes, i) + let byte = Array::getUnsafe(bytes, i); if byte >= 0 && byte <= 255 { - let high = Int.Bitwise.lsr(byte, 4) - let low = Int.Bitwise.land(byte, 0x0f) - let hex = String.charAt(hexCharsUpper, high) ++ String.charAt(hexCharsUpper, low) - Array.setUnsafe(parts, i, hex) + let high = Int::Bitwise::lsr(byte, 4); + let low = Int::Bitwise::land(byte, 0x0f); + let hex = String::charAt(hexCharsUpper, high) ++ String::charAt(hexCharsUpper, low); + Array::setUnsafe(parts, i, hex) } else { valid := false } @@ -416,11 +412,9 @@ let encodeSpacedUppercase = (bytes: array): result => { } if valid.contents { - Ok(Array.join(parts, " ")) + Ok(Array::join(parts, " ")) } else { Error(InvalidCharacter) } } } - -======================================== */ diff --git a/src/proven/Proven_SafeMath.affine b/src/proven/Proven_SafeMath.affine index 123c657..979468b 100644 --- a/src/proven/Proven_SafeMath.affine +++ b/src/proven/Proven_SafeMath.affine @@ -4,10 +4,6 @@ module Proven_SafeMath; -// TODO: Complete semantic implementation - - -/* === ORIGINAL RESCRIPT IMPLEMENTATION === // SPDX-License-Identifier: MPL-2.0 // SPDX-FileCopyrightText: 2025 Hyperpolymath @@ -19,7 +15,7 @@ module Proven_SafeMath; */ /** Safe division that returns None on division by zero */ -let div = (numerator: int, denominator: int): option => { +pub fn div(numerator: Int, denominator: Int) -> Option { if denominator == 0 { None } else { @@ -28,15 +24,15 @@ let div = (numerator: int, denominator: int): option => { } /** Safe division with default value */ -let divOr = (default: int, numerator: int, denominator: int): int => { - switch div(numerator, denominator) { - | Some(v) => v - | None => default +pub fn divOr(default: Int, numerator: Int, denominator: Int) -> Int { + match div(numerator, denominator) { + Some(v) => v + None => default } } /** Safe modulo that returns None on division by zero */ -let safeMod = (numerator: int, denominator: int): option => { +pub fn safeMod(numerator: Int, denominator: Int) -> Option { if denominator == 0 { None } else { @@ -45,8 +41,8 @@ let safeMod = (numerator: int, denominator: int): option => { } /** Addition with overflow detection */ -let addChecked = (a: int, b: int): option => { - let result = a + b +pub fn addChecked(a: Int, b: Int) -> Option { + let result = a + b; // Check for overflow if (a > 0 && b > 0 && result < 0) || (a < 0 && b < 0 && result > 0) { None @@ -56,8 +52,8 @@ let addChecked = (a: int, b: int): option => { } /** Subtraction with underflow detection */ -let subChecked = (a: int, b: int): option => { - let result = a - b +pub fn subChecked(a: Int, b: Int) -> Option { + let result = a - b; // Check for underflow if (a > 0 && b < 0 && result < 0) || (a < 0 && b > 0 && result > 0) { None @@ -67,11 +63,11 @@ let subChecked = (a: int, b: int): option => { } /** Multiplication with overflow detection */ -let mulChecked = (a: int, b: int): option => { +pub fn mulChecked(a: Int, b: Int) -> Option { if a == 0 || b == 0 { Some(0) } else { - let result = a * b + let result = a * b; if result / a != b { None } else { @@ -81,7 +77,7 @@ let mulChecked = (a: int, b: int): option => { } /** Safe absolute value that handles MIN_INT correctly */ -let absSafe = (n: int): option => { +pub fn absSafe(n: Int) -> Option { if n == min_int { None } else if n < 0 { @@ -92,7 +88,7 @@ let absSafe = (n: int): option => { } /** Clamp a value to range [lo, hi] */ -let clamp = (lo: int, hi: int, value: int): int => { +pub fn clamp(lo: Int, hi: Int, value: Int) -> Int { if value < lo { lo } else if value > hi { @@ -103,7 +99,7 @@ let clamp = (lo: int, hi: int, value: int): int => { } /** Integer exponentiation with overflow detection */ -let rec powChecked = (base: int, exp: int): option => { +pub fn powChecked(base: Int, exp: Int) -> Option { if exp < 0 { None } else if exp == 0 { @@ -111,12 +107,12 @@ let rec powChecked = (base: int, exp: int): option => { } else if exp == 1 { Some(base) } else { - switch powChecked(base, exp / 2) { - | None => None - | Some(half) => - switch mulChecked(half, half) { - | None => None - | Some(squared) => + match powChecked(base, exp / 2) { + None => None + Some(half) => + match mulChecked(half, half) { + None => None + Some(squared) => if mod(exp, 2) == 0 { Some(squared) } else { @@ -128,57 +124,57 @@ let rec powChecked = (base: int, exp: int): option => { } /** Calculate percentage safely */ -let percentOf = (percent: int, total: int): option => { - switch mulChecked(percent, total) { - | None => None - | Some(product) => div(product, 100) +pub fn percentOf(percent: Int, total: Int) -> Option { + match mulChecked(percent, total) { + None => None + Some(product) => div(product, 100) } } /** Calculate what percentage part is of whole */ -let asPercent = (part: int, whole: int): option => { - switch mulChecked(part, 100) { - | None => None - | Some(scaled) => div(scaled, whole) +pub fn asPercent(part: Int, whole: Int) -> Option { + match mulChecked(part, 100) { + None => None + Some(scaled) => div(scaled, whole) } } /** Check if a value is within a range [lo, hi] (inclusive) */ -let inRange = (value: int, lo: int, hi: int): bool => { +pub fn inRange(value: Int, lo: Int, hi: Int) -> Bool { value >= lo && value <= hi } /** Check if a value is within a range, excluding specific values */ -let inRangeExcluding = (value: int, lo: int, hi: int, excluded: array): bool => { +pub fn inRangeExcluding(value: Int, lo: Int, hi: Int, excluded: [Int]) -> Bool { if !inRange(value, lo, hi) { false } else { - !Array.some(excluded, e => e == value) + !Array::some(excluded, e => e == value) } } -/** Safe integer parsing from string, returns None on invalid input */ -let fromString = (str: string): option => { - let trimmed = String.trim(str) - if String.length(trimmed) == 0 { +/** Safe integer parsing from String, returns None on invalid input */ +pub fn fromString(str: String) -> Option { + let trimmed = String::trim(str); + if String::length(trimmed) == 0 { None } else { // Check for valid integer format - let isValid = RegExp.test(%re("/^-?\\d+$/"), trimmed) + let isValid = RegExp::test(%re("/^-?\\d+$/"), trimmed); if !isValid { None } else { - let parsed = Int.fromString(trimmed) + let parsed = Int::fromString(trimmed); parsed } } } /** Safe integer parsing with range validation */ -let fromStringInRange = (str: string, lo: int, hi: int): option => { - switch fromString(str) { - | None => None - | Some(value) => +pub fn fromStringInRange(str: String, lo: Int, hi: Int) -> Option { + match fromString(str) { + None => None + Some(value) => if inRange(value, lo, hi) { Some(value) } else { @@ -186,5 +182,3 @@ let fromStringInRange = (str: string, lo: int, hi: int): option => { } } } - -======================================== */ diff --git a/src/proven/Proven_SafePath.affine b/src/proven/Proven_SafePath.affine index 7b2ef28..bf87242 100644 --- a/src/proven/Proven_SafePath.affine +++ b/src/proven/Proven_SafePath.affine @@ -4,10 +4,6 @@ module Proven_SafePath; -// TODO: Complete semantic implementation - - -/* === ORIGINAL RESCRIPT IMPLEMENTATION === // SPDX-License-Identifier: MPL-2.0 // SPDX-FileCopyrightText: 2025 Hyperpolymath @@ -16,10 +12,10 @@ module Proven_SafePath; */ /** Check if a path contains directory traversal sequences */ -let hasTraversal = (path: string): bool => { - String.includes(path, "..") || - String.includes(path, "~") || - String.startsWith(path, "/") && String.includes(path, "..") +pub fn hasTraversal(path: String) -> Bool { + String::includes(path, "..") || + String::includes(path, "~") || + String::startsWith(path, "/") && String::includes(path, "..") } /** Check if a path contains a null byte @@ -27,8 +23,8 @@ let hasTraversal = (path: string): bool => { * SECURITY: Null bytes can truncate paths in C-based filesystem APIs, * allowing an attacker to bypass extension checks (e.g. "safe.txt\0.sh"). */ -let hasNullByte = (path: string): bool => { - String.includes(path, "\x00") +pub fn hasNullByte(path: String) -> Bool { + String::includes(path, "\x00") } /** Check if a path is safe @@ -38,30 +34,30 @@ let hasNullByte = (path: string): bool => { * - contains no home directory expansion (~) * - contains no null bytes */ -let isSafe = (path: string): bool => { +pub fn isSafe(path: String) -> Bool { !hasTraversal(path) && !hasNullByte(path) } /** Sanitize a filename by removing dangerous characters */ -let sanitizeFilename = (filename: string): string => { +pub fn sanitizeFilename(filename: String) -> String { filename - ->String.replaceRegExp(%re("/\\.\\./g"), "_") - ->String.replaceRegExp(%re("/[\\/\\\\]/g"), "_") - ->String.replaceRegExp(%re("/[\\x00-\\x1f]/g"), "") - ->String.replaceRegExp(%re("/[<>:\"\\|\\?\\*]/g"), "_") + ->String::replaceRegExp(%re("/\\.\\./g"), "_") + ->String::replaceRegExp(%re("/[\\/\\\\]/g"), "_") + ->String::replaceRegExp(%re("/[\\x00-\\x1f]/g"), "") + ->String::replaceRegExp(%re("/[<>:\"\\|\\?\\*]/g"), "_") } /** Safely join path components, rejecting traversal attempts */ -let safeJoin = (base: string, parts: array): option => { - let hasUnsafe = parts->Array.some(part => hasTraversal(part)) +pub fn safeJoin(base: String, parts: [String]) -> Option { + let hasUnsafe = parts->Array::some(part => hasTraversal(part)) if hasUnsafe { None } else { - let result = ref(base) - parts->Array.forEach(part => { - let sanitized = sanitizeFilename(part) - let base = result.contents - if String.endsWith(base, "/") { + let result = ref(base); + parts->Array::forEach(part => { + let sanitized = sanitizeFilename(part); + let base = result.contents; + if String::endsWith(base, "/") { result := base ++ sanitized } else { result := base ++ "/" ++ sanitized @@ -70,5 +66,3 @@ let safeJoin = (base: string, parts: array): option => { Some(result.contents) } } - -======================================== */ diff --git a/src/proven/Proven_SafeString.affine b/src/proven/Proven_SafeString.affine index ce6ccf0..1e3248b 100644 --- a/src/proven/Proven_SafeString.affine +++ b/src/proven/Proven_SafeString.affine @@ -4,67 +4,63 @@ module Proven_SafeString; -// TODO: Complete semantic implementation - - -/* === ORIGINAL RESCRIPT IMPLEMENTATION === // SPDX-License-Identifier: MPL-2.0 // SPDX-FileCopyrightText: 2025 Hyperpolymath /** * SafeString - String operations that cannot crash. * - * Provides safe escaping for SQL, HTML, and JavaScript. + * Provides safe escaping for SQL, HTML, and JavaScript:: */ -/** Escape a string for safe SQL interpolation */ -let escapeSql = (value: string): string => { - String.replaceRegExp(value, %re("/'/g"), "''") +/** Escape a String for safe SQL interpolation */ +pub fn escapeSql(value: String) -> String { + String::replaceRegExp(value, %re("/'/g"), "''") } -/** Escape a string for safe HTML insertion */ -let escapeHtml = (value: string): string => { +/** Escape a String for safe HTML insertion */ +pub fn escapeHtml(value: String) -> String { value - ->String.replaceRegExp(%re("/&/g"), "&") - ->String.replaceRegExp(%re("/String.replaceRegExp(%re("/>/g"), ">") - ->String.replaceRegExp(%re("/\"/g"), """) - ->String.replaceRegExp(%re("/'/g"), "'") + ->String::replaceRegExp(%re("/&/g"), "&") + ->String::replaceRegExp(%re("/String::replaceRegExp(%re("/>/g"), ">") + ->String::replaceRegExp(%re("/\"/g"), """) + ->String::replaceRegExp(%re("/'/g"), "'") } -/** Escape a string for safe JavaScript string literal insertion */ -let escapeJs = (value: string): string => { +/** Escape a String for safe JavaScript String literal insertion */ +pub fn escapeJs(value: String) -> String { value - ->String.replaceRegExp(%re("/\\\\/g"), "\\\\") - ->String.replaceRegExp(%re("/\"/g"), "\\\"") - ->String.replaceRegExp(%re("/'/g"), "\\'") - ->String.replaceRegExp(%re("/\n/g"), "\\n") - ->String.replaceRegExp(%re("/\r/g"), "\\r") - ->String.replaceRegExp(%re("/\t/g"), "\\t") + ->String::replaceRegExp(%re("/\\\\/g"), "\\\\") + ->String::replaceRegExp(%re("/\"/g"), "\\\"") + ->String::replaceRegExp(%re("/'/g"), "\\'") + ->String::replaceRegExp(%re("/\n/g"), "\\n") + ->String::replaceRegExp(%re("/\r/g"), "\\r") + ->String::replaceRegExp(%re("/\t/g"), "\\t") } -/** Convert a string to an array of ASCII code points (0-255 only) +/** Convert a String to an array of ASCII code points (0-255 only) * * Returns Error if any character is outside ASCII range (> 255). * For pure ASCII (0-127), this is always safe. */ -let toCodePoints = (str: string): result, string> => { - let length = String.length(str) +let toCodePoints = (str: String): result<[Int], String> => { + let length = String::length(str); if length == 0 { Ok([]) } else { - let codes = Array.make(~length, 0) - let valid = ref(true) - let errorIdx = ref(0) + let codes = Array::make(~length, 0); + let valid = ref(true); + let errorIdx = ref(0); for i in 0 to length - 1 { if valid.contents { - let code = String.charCodeAt(str, i)->Float.toInt + let code = String::charCodeAt(str, i)->Float::toInt; if code > 255 { valid := false errorIdx := i } else { - Array.setUnsafe(codes, i, code) + Array::setUnsafe(codes, i, code) } } } @@ -72,52 +68,52 @@ let toCodePoints = (str: string): result, string> => { if valid.contents { Ok(codes) } else { - Error(`Character at position ${Int.toString(errorIdx.contents)} is outside ASCII range`) + Error(`Character at position ${Int::toString(errorIdx.contents)} is outside ASCII range`) } } } -/** Convert an array of code points to a string +/** Convert an array of code points to a String * * All code points must be in range 0-65535 (valid UTF-16 code units). */ -let fromCodePoints = (codes: array): result => { - let length = Array.length(codes) +let fromCodePoints = (codes: [Int]): result => { + let length = Array::length(codes); if length == 0 { Ok("") } else { - let valid = ref(true) - let errorIdx = ref(0) - let chars = Array.make(~length, "") + let valid = ref(true); + let errorIdx = ref(0); + let chars = Array::make(~length, ""); for i in 0 to length - 1 { if valid.contents { - let code = Array.getUnsafe(codes, i) + let code = Array::getUnsafe(codes, i); if code < 0 || code > 65535 { valid := false errorIdx := i } else { - Array.setUnsafe(chars, i, String.fromCharCode(code)) + Array::setUnsafe(chars, i, String::fromCharCode(code)) } } } if valid.contents { - Ok(Array.join(chars, "")) + Ok(Array::join(chars, "")) } else { - Error(`Invalid code point at position ${Int.toString(errorIdx.contents)}`) + Error(`Invalid code point at position ${Int::toString(errorIdx.contents)}`) } } } -/** Check if a string contains only ASCII characters (0-127) */ -let isAscii = (str: string): bool => { - let length = String.length(str) - let valid = ref(true) +/** Check if a String contains only ASCII characters (0-127) */ +pub fn isAscii(str: String) -> Bool { + let length = String::length(str); + let valid = ref(true); for i in 0 to length - 1 { if valid.contents { - let code = String.charCodeAt(str, i)->Float.toInt + let code = String::charCodeAt(str, i)->Float::toInt; if code > 127 { valid := false } @@ -127,14 +123,14 @@ let isAscii = (str: string): bool => { valid.contents } -/** Check if a string contains only printable ASCII (32-126) */ -let isPrintableAscii = (str: string): bool => { - let length = String.length(str) - let valid = ref(true) +/** Check if a String contains only printable ASCII (32-126) */ +pub fn isPrintableAscii(str: String) -> Bool { + let length = String::length(str); + let valid = ref(true); for i in 0 to length - 1 { if valid.contents { - let code = String.charCodeAt(str, i)->Float.toInt + let code = String::charCodeAt(str, i)->Float::toInt; if code < 32 || code > 126 { valid := false } @@ -143,5 +139,3 @@ let isPrintableAscii = (str: string): bool => { valid.contents } - -======================================== */