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] 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 +} + +======================================== */