diff --git a/lib/ocaml/App.affine b/lib/ocaml/App.affine new file mode 100644 index 0000000..eb92faa --- /dev/null +++ b/lib/ocaml/App.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module App; + +// TODO: Complete semantic implementation diff --git a/lib/ocaml/App.res b/lib/ocaml/App.res deleted file mode 100644 index ea38fa9..0000000 --- a/lib/ocaml/App.res +++ /dev/null @@ -1,29 +0,0 @@ -// 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/lib/ocaml/Bindings.affine b/lib/ocaml/Bindings.affine new file mode 100644 index 0000000..4f77b1a --- /dev/null +++ b/lib/ocaml/Bindings.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Bindings; + +// TODO: Complete semantic implementation diff --git a/lib/ocaml/Bindings.res b/lib/ocaml/Bindings.res deleted file mode 100644 index 4708553..0000000 --- a/lib/ocaml/Bindings.res +++ /dev/null @@ -1,126 +0,0 @@ -// 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/lib/ocaml/Proven_SafeHex.affine b/lib/ocaml/Proven_SafeHex.affine new file mode 100644 index 0000000..8ae6163 --- /dev/null +++ b/lib/ocaml/Proven_SafeHex.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Proven_SafeHex; + +// TODO: Complete semantic implementation diff --git a/lib/ocaml/Proven_SafeHex.res b/lib/ocaml/Proven_SafeHex.res deleted file mode 100644 index 40d447c..0000000 --- a/lib/ocaml/Proven_SafeHex.res +++ /dev/null @@ -1,414 +0,0 @@ -// 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/lib/ocaml/Proven_SafeMath.affine b/lib/ocaml/Proven_SafeMath.affine new file mode 100644 index 0000000..749200b --- /dev/null +++ b/lib/ocaml/Proven_SafeMath.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Proven_SafeMath; + +// TODO: Complete semantic implementation diff --git a/lib/ocaml/Proven_SafeMath.res b/lib/ocaml/Proven_SafeMath.res deleted file mode 100644 index 650c3fd..0000000 --- a/lib/ocaml/Proven_SafeMath.res +++ /dev/null @@ -1,178 +0,0 @@ -// 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/lib/ocaml/Proven_SafePath.affine b/lib/ocaml/Proven_SafePath.affine new file mode 100644 index 0000000..66f86b7 --- /dev/null +++ b/lib/ocaml/Proven_SafePath.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Proven_SafePath; + +// TODO: Complete semantic implementation diff --git a/lib/ocaml/Proven_SafePath.res b/lib/ocaml/Proven_SafePath.res deleted file mode 100644 index 81fe804..0000000 --- a/lib/ocaml/Proven_SafePath.res +++ /dev/null @@ -1,62 +0,0 @@ -// 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/lib/ocaml/Proven_SafeString.affine b/lib/ocaml/Proven_SafeString.affine new file mode 100644 index 0000000..e0b1dc2 --- /dev/null +++ b/lib/ocaml/Proven_SafeString.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Proven_SafeString; + +// TODO: Complete semantic implementation diff --git a/lib/ocaml/Proven_SafeString.res b/lib/ocaml/Proven_SafeString.res deleted file mode 100644 index dbc68fe..0000000 --- a/lib/ocaml/Proven_SafeString.res +++ /dev/null @@ -1,135 +0,0 @@ -// 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 -} diff --git a/lib/ocaml/RuntimeBridge.affine b/lib/ocaml/RuntimeBridge.affine new file mode 100644 index 0000000..585655f --- /dev/null +++ b/lib/ocaml/RuntimeBridge.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module RuntimeBridge; + +// TODO: Complete semantic implementation diff --git a/lib/ocaml/RuntimeBridge.res b/lib/ocaml/RuntimeBridge.res deleted file mode 100644 index 0eb0e99..0000000 --- a/lib/ocaml/RuntimeBridge.res +++ /dev/null @@ -1,207 +0,0 @@ -// 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/lib/ocaml/Types.affine b/lib/ocaml/Types.affine new file mode 100644 index 0000000..4006671 --- /dev/null +++ b/lib/ocaml/Types.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Types; + +// TODO: Complete semantic implementation diff --git a/lib/ocaml/Types.res b/lib/ocaml/Types.res deleted file mode 100644 index cce1d69..0000000 --- a/lib/ocaml/Types.res +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Shared types for DotMatrix-FilePrinter -// Uses proven library for formally verified safety operations - -// Contaminant info -type contaminant = { - position: int, - value: int, - description: string, -} - -// Preview result from backend -type previewResult = { - hex_preview: string, - would_contaminate: bool, - contaminants: array, - byte_count: int, -} - -// Verify result from backend -type verifyResult = { - clean: bool, - contaminants: array, - hexdump: string, - size: int, -} - -// Constraints (mirror of Nickel/Rust) -// Uses SafeMath from proven library for formally verified bounds checking -module Constraints = { - let maxByte = 127 - let forbiddenNbsp = 160 - let forbiddenUtf8 = 194 - - // Forbidden byte values that can cause contamination - let forbiddenBytes = [forbiddenNbsp, forbiddenUtf8] - - // Use SafeMath.inRangeExcluding for formally verified byte validation - let isValidByte = (byte: int): bool => { - Proven_SafeMath.inRangeExcluding(byte, 0, maxByte, forbiddenBytes) - } - - let describeInvalidByte = (byte: int): string => { - if byte == forbiddenNbsp { - "NBSP (Non-Breaking Space)" - } else if byte == forbiddenUtf8 { - "UTF-8 continuation marker" - } else if byte > maxByte { - `Non-ASCII (> 127)` - } else if byte < 0 { - "Negative value" - } else { - "Unknown" - } - } -} diff --git a/rescript.json b/rescript.json deleted file mode 100644 index ca44cc6..0000000 --- a/rescript.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "dotmatrix-fileprinter", - "sources": [ - { - "dir": "src", - "subdirs": true - } - ], - "package-specs": [ - { - "module": "esmodule", - "in-source": true - } - ], - "suffix": ".res.js", - "dependencies": [ - "@rescript/core" - ], - "compiler-flags": [ - "-open RescriptCore" - ], - "warnings": { - "error": "+101" - }, - "jsx": { - "version": 4, - "mode": "automatic" - } -} diff --git a/src/App.affine b/src/App.affine new file mode 100644 index 0000000..eb92faa --- /dev/null +++ b/src/App.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module App; + +// TODO: Complete semantic implementation diff --git a/src/App.res b/src/App.res deleted file mode 100644 index ea38fa9..0000000 --- a/src/App.res +++ /dev/null @@ -1,29 +0,0 @@ -// 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 new file mode 100644 index 0000000..4f77b1a --- /dev/null +++ b/src/Bindings.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Bindings; + +// TODO: Complete semantic implementation diff --git a/src/Bindings.res b/src/Bindings.res deleted file mode 100644 index 4708553..0000000 --- a/src/Bindings.res +++ /dev/null @@ -1,126 +0,0 @@ -// 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 new file mode 100644 index 0000000..585655f --- /dev/null +++ b/src/RuntimeBridge.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module RuntimeBridge; + +// TODO: Complete semantic implementation diff --git a/src/RuntimeBridge.res b/src/RuntimeBridge.res deleted file mode 100644 index 0eb0e99..0000000 --- a/src/RuntimeBridge.res +++ /dev/null @@ -1,207 +0,0 @@ -// 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/Types.affine b/src/Types.affine new file mode 100644 index 0000000..4006671 --- /dev/null +++ b/src/Types.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Types; + +// TODO: Complete semantic implementation diff --git a/src/Types.res b/src/Types.res deleted file mode 100644 index cce1d69..0000000 --- a/src/Types.res +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Shared types for DotMatrix-FilePrinter -// Uses proven library for formally verified safety operations - -// Contaminant info -type contaminant = { - position: int, - value: int, - description: string, -} - -// Preview result from backend -type previewResult = { - hex_preview: string, - would_contaminate: bool, - contaminants: array, - byte_count: int, -} - -// Verify result from backend -type verifyResult = { - clean: bool, - contaminants: array, - hexdump: string, - size: int, -} - -// Constraints (mirror of Nickel/Rust) -// Uses SafeMath from proven library for formally verified bounds checking -module Constraints = { - let maxByte = 127 - let forbiddenNbsp = 160 - let forbiddenUtf8 = 194 - - // Forbidden byte values that can cause contamination - let forbiddenBytes = [forbiddenNbsp, forbiddenUtf8] - - // Use SafeMath.inRangeExcluding for formally verified byte validation - let isValidByte = (byte: int): bool => { - Proven_SafeMath.inRangeExcluding(byte, 0, maxByte, forbiddenBytes) - } - - let describeInvalidByte = (byte: int): string => { - if byte == forbiddenNbsp { - "NBSP (Non-Breaking Space)" - } else if byte == forbiddenUtf8 { - "UTF-8 continuation marker" - } else if byte > maxByte { - `Non-ASCII (> 127)` - } else if byte < 0 { - "Negative value" - } else { - "Unknown" - } - } -} diff --git a/src/proven/Proven_SafeHex.affine b/src/proven/Proven_SafeHex.affine new file mode 100644 index 0000000..8ae6163 --- /dev/null +++ b/src/proven/Proven_SafeHex.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Proven_SafeHex; + +// TODO: Complete semantic implementation diff --git a/src/proven/Proven_SafeHex.res b/src/proven/Proven_SafeHex.res deleted file mode 100644 index 40d447c..0000000 --- a/src/proven/Proven_SafeHex.res +++ /dev/null @@ -1,414 +0,0 @@ -// 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 new file mode 100644 index 0000000..749200b --- /dev/null +++ b/src/proven/Proven_SafeMath.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Proven_SafeMath; + +// TODO: Complete semantic implementation diff --git a/src/proven/Proven_SafeMath.res b/src/proven/Proven_SafeMath.res deleted file mode 100644 index 650c3fd..0000000 --- a/src/proven/Proven_SafeMath.res +++ /dev/null @@ -1,178 +0,0 @@ -// 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 new file mode 100644 index 0000000..66f86b7 --- /dev/null +++ b/src/proven/Proven_SafePath.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Proven_SafePath; + +// TODO: Complete semantic implementation diff --git a/src/proven/Proven_SafePath.res b/src/proven/Proven_SafePath.res deleted file mode 100644 index 81fe804..0000000 --- a/src/proven/Proven_SafePath.res +++ /dev/null @@ -1,62 +0,0 @@ -// 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 new file mode 100644 index 0000000..e0b1dc2 --- /dev/null +++ b/src/proven/Proven_SafeString.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Proven_SafeString; + +// TODO: Complete semantic implementation diff --git a/src/proven/Proven_SafeString.res b/src/proven/Proven_SafeString.res deleted file mode 100644 index dbc68fe..0000000 --- a/src/proven/Proven_SafeString.res +++ /dev/null @@ -1,135 +0,0 @@ -// 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 -}