|
| 1 | +// SPDX-License-Identifier: PMPL-1.0-or-later |
| 2 | +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (@hyperpolymath) |
| 3 | +// |
| 4 | +// safety.gleam - Proven-compatible safety functions for Glyphbase |
| 5 | +// |
| 6 | +// Pure Gleam implementations of critical safety checks from the proven |
| 7 | +// library (SafePath, SafeString). These match the proven API signatures |
| 8 | +// so they can be replaced with real NIF calls when the proven build |
| 9 | +// pipeline (Idris2 → RefC → Zig → NIF) is operational. |
| 10 | +// |
| 11 | +// TODO: Replace with `import proven/path`, `import proven/string_ops` |
| 12 | +// once libproven_nif.so is built. |
| 13 | + |
| 14 | +import gleam/string |
| 15 | + |
| 16 | +// ============================================================ |
| 17 | +// SafePath — Directory traversal prevention |
| 18 | +// ============================================================ |
| 19 | + |
| 20 | +/// Check if a path contains directory traversal sequences. |
| 21 | +/// Matches proven/path.has_traversal/1 API. |
| 22 | +pub fn path_has_traversal(path: String) -> Result(Bool, String) { |
| 23 | + case string.contains(path, "..") { |
| 24 | + True -> Ok(True) |
| 25 | + False -> |
| 26 | + case string.contains(path, "\u{0000}") { |
| 27 | + True -> Ok(True) |
| 28 | + False -> Ok(False) |
| 29 | + } |
| 30 | + } |
| 31 | +} |
| 32 | + |
| 33 | +/// Sanitize a filename by removing path separators and traversal sequences. |
| 34 | +/// Matches proven/path.sanitize_filename/1 API. |
| 35 | +pub fn sanitize_filename(filename: String) -> Result(String, String) { |
| 36 | + let cleaned = |
| 37 | + filename |
| 38 | + |> string.replace("..", "") |
| 39 | + |> string.replace("/", "_") |
| 40 | + |> string.replace("\\", "_") |
| 41 | + |> string.replace("\u{0000}", "") |
| 42 | + |
| 43 | + case string.is_empty(cleaned) { |
| 44 | + True -> Error("filename_empty_after_sanitization") |
| 45 | + False -> Ok(cleaned) |
| 46 | + } |
| 47 | +} |
| 48 | + |
| 49 | +// ============================================================ |
| 50 | +// SafeString — Escaping for injection prevention |
| 51 | +// ============================================================ |
| 52 | + |
| 53 | +/// Escape a string for safe use in SQL (single-quote doubling). |
| 54 | +/// Matches proven/string_ops.escape_sql/1 API. |
| 55 | +pub fn escape_sql(value: String) -> Result(String, String) { |
| 56 | + Ok(string.replace(value, "'", "''")) |
| 57 | +} |
| 58 | + |
| 59 | +/// Escape a string for safe use in HTML content. |
| 60 | +/// Matches proven/string_ops.escape_html/1 API. |
| 61 | +pub fn escape_html(value: String) -> Result(String, String) { |
| 62 | + value |
| 63 | + |> string.replace("&", "&") |
| 64 | + |> string.replace("<", "<") |
| 65 | + |> string.replace(">", ">") |
| 66 | + |> string.replace("\"", """) |
| 67 | + |> string.replace("'", "'") |
| 68 | + |> Ok |
| 69 | +} |
| 70 | + |
| 71 | +/// Escape a string for safe use in JavaScript string literals. |
| 72 | +/// Matches proven/string_ops.escape_js/1 API. |
| 73 | +pub fn escape_js(value: String) -> Result(String, String) { |
| 74 | + value |
| 75 | + |> string.replace("\\", "\\\\") |
| 76 | + |> string.replace("\"", "\\\"") |
| 77 | + |> string.replace("'", "\\'") |
| 78 | + |> string.replace("<", "\\u003C") |
| 79 | + |> string.replace(">", "\\u003E") |
| 80 | + |> string.replace("/", "\\/") |
| 81 | + |> string.replace("\n", "\\n") |
| 82 | + |> string.replace("\r", "\\r") |
| 83 | + |> Ok |
| 84 | +} |
0 commit comments