| | | 1 | | namespace Envilder.Domain; |
| | | 2 | | |
| | | 3 | | using System.Text.Json; |
| | | 4 | | |
| | | 5 | | /// <summary> |
| | | 6 | | /// Which strings are usable as environment variable names, per the map-file |
| | | 7 | | /// naming constraint in ADR-0008: an accepted name must survive a <c>.env</c> |
| | | 8 | | /// round-trip as itself and nothing else. Anything outside |
| | | 9 | | /// <c>^[A-Za-z0-9_./-]+$</c> is either read back as a different assignment |
| | | 10 | | /// (<c>=</c>, CR, LF, U+2028, U+2029) or silently lost (a space, <c>#</c>, a |
| | | 11 | | /// tab, a non-Latin letter). <c>/</c> stays valid: it is the section separator |
| | | 12 | | /// map files have always used (<c>Database/Password</c>). |
| | | 13 | | /// <c>__proto__</c> is inside the allowlist but every JavaScript reader drops |
| | | 14 | | /// it, so it is rejected as well to keep the contract uniform across the CLI |
| | | 15 | | /// and every SDK. |
| | | 16 | | /// </summary> |
| | | 17 | | internal static class EnvironmentVariableName |
| | | 18 | | { |
| | | 19 | | private const string UnsupportedName = "__proto__"; |
| | | 20 | | |
| | | 21 | | public static bool IsValid(string name) |
| | | 22 | | { |
| | 1 | 23 | | if (string.IsNullOrEmpty(name) || name == UnsupportedName) |
| | | 24 | | { |
| | 1 | 25 | | return false; |
| | | 26 | | } |
| | | 27 | | |
| | | 28 | | // A character check rather than a Regex: .NET's `$` also matches |
| | | 29 | | // before a trailing newline, which would accept "SAFE\n". |
| | 1 | 30 | | foreach (var c in name) |
| | | 31 | | { |
| | 1 | 32 | | if (!IsReadableNameChar(c)) |
| | | 33 | | { |
| | 1 | 34 | | return false; |
| | | 35 | | } |
| | | 36 | | } |
| | | 37 | | |
| | 1 | 38 | | return true; |
| | | 39 | | } |
| | | 40 | | |
| | | 41 | | /// <summary> |
| | | 42 | | /// Builds the rejection message. The name is untrusted input, so it is |
| | | 43 | | /// quoted rather than interpolated: a name carrying line terminators cannot |
| | | 44 | | /// break the message across lines. Only the name is echoed, never the |
| | | 45 | | /// mapped value. |
| | | 46 | | /// </summary> |
| | | 47 | | public static string InvalidMessage(string name) |
| | | 48 | | { |
| | 1 | 49 | | if (string.IsNullOrWhiteSpace(name)) |
| | | 50 | | { |
| | 1 | 51 | | return "Environment variable name cannot be empty"; |
| | | 52 | | } |
| | | 53 | | |
| | 1 | 54 | | if (name == UnsupportedName) |
| | | 55 | | { |
| | 1 | 56 | | return $"Unsupported environment variable name {Quote(name)}: " |
| | 1 | 57 | | + "a .env parser cannot read this name back, so it would be written " |
| | 1 | 58 | | + "but never resolved"; |
| | | 59 | | } |
| | | 60 | | |
| | 1 | 61 | | return $"Invalid environment variable name {Quote(name)}: names may only " |
| | 1 | 62 | | + "contain letters, digits, underscore, dot, hyphen and slash, so that " |
| | 1 | 63 | | + "they can be read back from a .env file"; |
| | | 64 | | } |
| | | 65 | | |
| | | 66 | | private static bool IsReadableNameChar(char c) |
| | | 67 | | { |
| | 1 | 68 | | return c is (>= 'A' and <= 'Z') or (>= 'a' and <= 'z') or (>= '0' and <= '9') |
| | 1 | 69 | | or '_' or '.' or '-' or '/'; |
| | | 70 | | } |
| | | 71 | | |
| | | 72 | | // The default encoder escapes control characters and every non-ASCII |
| | | 73 | | // character, U+2028/U+2029 included. |
| | | 74 | | private static string Quote(string name) |
| | | 75 | | { |
| | 1 | 76 | | return JsonSerializer.Serialize(name); |
| | | 77 | | } |
| | | 78 | | } |