/
nv-lang
/
nova
Обзор
Документация
Войти
/
nv-lang
/
nova
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
std/src/encoding/base64/core.nv
271 строка
12 KB
Evgeniy Golovin
merge: comment-hygiene-2 — чистка комментариев std (batch 1-20) + линт-свип 330→13
01 авг 2026, 18:13
01 авг 2026, 18:13
bb8b33d
Код
Авторство
О чём код?
// stdlib/base64/core.nv — Base64 encoding/decoding по RFC 4648. // // Independent implementation. Спецификация: // https://www.rfc-editor.org/rfc/rfc4648 // // Поддержка: // - Standard Base64 (RFC 4648 § 4): A-Z, a-z, 0-9, +, / // - URL-safe Base64 (RFC 4648 § 5): A-Z, a-z, 0-9, -, _ // - Padding с `=` (опционально для URL-safe) // // Алгоритм: // - Encode: каждые 3 байта (24 бита) → 4 символа (по 6 бит) // Если входных байт 1 или 2 — добавляем `=` padding. // - Decode: каждые 4 символа → 3 байта. Padding определяет длину. // // Decisions: // - D72 bounds: ничего generic. // - Плоские static-функции: encode/decode, без протокола (D73/D77 // ретрактированы 2026-07-06 — `Base64.encode`/`Base64.decode` никогда // не зависели от From/Into, это просто именованные конверсии). // - D325 (Plan 181): decode → Result[[]u8, Base64Error] (Result-everywhere, не throw). // - Buffer для O(N) аккумуляции выходных байт/символов. // // API: // Base64.encode(data []u8) -> str // Base64.encode_url(data []u8) -> str // Base64.decode(s str) -> Result[[]u8, Base64Error] // Base64.decode_url(s str) -> Result[[]u8, Base64Error] module encoding.base64 // ────────────────────────────────────────────────────────────────────────── // Ошибки // ────────────────────────────────────────────────────────────────────────── /// Errors while decoding base64. Coverage: bad length, invalid char, bad padding. #stable(since = "0.1") export type Base64Error enum | InvalidLength { actual int } | InvalidChar { position int, char char } | InvalidPadding { position int } // ────────────────────────────────────────────────────────────────────────── // Alphabet и lookup // ────────────────────────────────────────────────────────────────────────── // RFC 4648 § 4 standard alphabet. // Plan 34 Ф.5.2: `int as char` запрещён D54, используем n.to_char(). // `value` всегда в [0, 63] (по семантике base64), значит codepoint // всегда в ASCII диапазоне — Err не возможен. Fallback нужен для exhaustiveness. fn encode_char_std(value u8) -> char { ro code = if value < 26 { 'A' as int + value as int // 0..25 → A..Z } else if value < 52 { 'a' as int + value as int - 26 // 26..51 → a..z } else if value < 62 { '0' as int + value as int - 52 // 52..61 → 0..9 } else if value == 62 { '+' as int } else { '/' as int } code.to_char() ?? '?' } // RFC 4648 § 5 URL-safe alphabet (отличие: `-` вместо `+`, `_` вместо `/`) fn encode_char_url(value u8) -> char { ro code = if value < 26 { 'A' as int + value as int } else if value < 52 { 'a' as int + value as int - 26 } else if value < 62 { '0' as int + value as int - 52 } else if value == 62 { '-' as int } else { '_' as int } code.to_char() ?? '?' } // Decode lookup. Возвращает Option[u8]: Some(0..63), None для невалидных. // Принимает оба алфавита (standard и URL-safe) — `+`/`-` дают 62, // `/`/`_` дают 63. fn decode_char(c char) -> Option[u8] { ro n = c as int if n >= 65 && n <= 90 { // A..Z → 0..25 Some((n - 65) as u8) } else if n >= 97 && n <= 122 { // a..z → 26..51 Some((n - 97 + 26) as u8) } else if n >= 48 && n <= 57 { // 0..9 → 52..61 Some((n - 48 + 52) as u8) } else if c == '+' || c == '-' { // оба варианта 62 Some(62 as u8) } else if c == '/' || c == '_' { // оба варианта 63 Some(63 as u8) } else { None } } // ────────────────────────────────────────────────────────────────────────── // Encoding // ────────────────────────────────────────────────────────────────────────── // Standard encoding с `=` padding. /// Standard base64 encoding (RFC 4648 § 4). Alphabet `A-Z a-z 0-9 + /` + padding `=`. #stable(since = "0.1") export fn Base64.encode(data []u8) -> str => encode_with(data, encode_char_std, true) // URL-safe encoding без padding (как в JWT, OAuth tokens). /// URL-safe base64 encoding (RFC 4648 § 5). `+`→`-`, `/`→`_`, no padding. #stable(since = "0.1") export fn Base64.encode_url(data []u8) -> str => encode_with(data, encode_char_url, false) // Encode с явным выбором alphabet и padding. fn encode_with(data []u8, alphabet fn(u8) -> char, with_padding bool) -> str { // Каждые 3 байта → 4 символа. Округляем вверх. ro n = data.len() ro chunks = n / 3 ro remainder = n - chunks * 3 ro out_len = chunks * 4 + (if remainder > 0 { 4 } else { 0 }) consume buf = StringBuilder.new(cap: out_len) // Полные chunks по 3 байта = 24 бита = 4 × 6 бит. for i in 0..chunks { ro base = i * 3 ro b0 = data[base] as u32 ro b1 = data[base + 1] as u32 ro b2 = data[base + 2] as u32 ro triple = (b0 << 16) | (b1 << 8) | b2 buf.append(alphabet(((triple >> 18) & 0x3F) as u8)) buf.append(alphabet(((triple >> 12) & 0x3F) as u8)) buf.append(alphabet(((triple >> 6) & 0x3F) as u8)) buf.append(alphabet((triple & 0x3F) as u8)) } // Хвост: 1 или 2 байта. if remainder == 1 { ro b0 = data[chunks * 3] as u32 ro triple = b0 << 16 buf.append(alphabet(((triple >> 18) & 0x3F) as u8)) buf.append(alphabet(((triple >> 12) & 0x3F) as u8)) if with_padding { buf.append('=') buf.append('=') } } else if remainder == 2 { ro b0 = data[chunks * 3] as u32 ro b1 = data[chunks * 3 + 1] as u32 ro triple = (b0 << 16) | (b1 << 8) buf.append(alphabet(((triple >> 18) & 0x3F) as u8)) buf.append(alphabet(((triple >> 12) & 0x3F) as u8)) buf.append(alphabet(((triple >> 6) & 0x3F) as u8)) if with_padding { buf.append('=') } } buf } // ────────────────────────────────────────────────────────────────────────── // Decoding // ────────────────────────────────────────────────────────────────────────── /// Decode standard base64 (with mandatory `=` padding to a multiple of 4). #stable(since = "0.1") export fn Base64.decode(s str) -> Result[[]u8, Base64Error] => decode_with(s, true) /// Decode URL-safe base64 (padding optional). #stable(since = "0.1") export fn Base64.decode_url(s str) -> Result[[]u8, Base64Error] => decode_with(s, false) fn decode_with(s str, require_padding bool) -> Result[[]u8, Base64Error] { // Если требуется padding — длина обязательно кратна 4. if require_padding && s.byte_len() % 4 != 0 { return Err(InvalidLength { actual: s.byte_len() }) } if s.byte_len() == 0 { return Ok([]) } // Считаем сколько `=` в конце — это даёт длину выхода. // URL-safe без padding: оцениваем по длине. // `=` — ASCII, base64-контент целиком ASCII → прямой byte-доступ O(1) // вместо ретрактированного `chars().nth()` (D260-амендмент). ro sb = s.bytes() ro trimmed_len = if require_padding { if sb[s.byte_len() - 1] == ('=' as u8) { if sb[s.byte_len() - 2] == ('=' as u8) { s.byte_len() - 2 } else { s.byte_len() - 1 } } else { s.byte_len() } } else { s.byte_len() } // Каждые 4 символа → 3 байта; неполный последний блок даёт 1 или 2. ro full_blocks = trimmed_len / 4 ro tail = trimmed_len - full_blocks * 4 ro out_capacity = full_blocks * 3 + (if tail == 0 { 0 } else { tail - 1 }) mut out []u8 = []u8.new(cap: out_capacity) mut pos = 0 for _ in 0..full_blocks { ro c0 = decode_at(s, pos)? ro c1 = decode_at(s, pos + 1)? ro c2 = decode_at(s, pos + 2)? ro c3 = decode_at(s, pos + 3)? ro triple = ((c0 as u32) << 18) | ((c1 as u32) << 12) | ((c2 as u32) << 6) | (c3 as u32) out.push(((triple >> 16) & 0xFF) as u8) out.push(((triple >> 8) & 0xFF) as u8) out.push((triple & 0xFF) as u8) pos += 4 } // Хвост (для URL-safe без padding или после strip'нутого padding). if tail == 2 { ro c0 = decode_at(s, pos)? ro c1 = decode_at(s, pos + 1)? ro triple = ((c0 as u32) << 18) | ((c1 as u32) << 12) out.push(((triple >> 16) & 0xFF) as u8) } else if tail == 3 { ro c0 = decode_at(s, pos)? ro c1 = decode_at(s, pos + 1)? ro c2 = decode_at(s, pos + 2)? ro triple = ((c0 as u32) << 18) | ((c1 as u32) << 12) | ((c2 as u32) << 6) out.push(((triple >> 16) & 0xFF) as u8) out.push(((triple >> 8) & 0xFF) as u8) } else if tail == 1 { // Невалидно — 1 символ хвоста не несёт целого байта. return Err(InvalidLength { actual: s.byte_len() }) } Ok(out) } // Base64-alphabet content is pure ASCII, so byte access at `pos` IS the // codepoint access — O(1) direct index instead of the ретрактированный // `chars().nth(pos)` O(n) scan (D260-амендмент). fn decode_at(s str, pos int) -> Result[u8, Base64Error] { match s.bytes().get(pos) { Some(b) => { ro c = (b as int).to_char() ?? '?' match decode_char(c) { Some(v) => Ok(v) None => Err(InvalidChar { position: pos, char: c }) } } None => Err(InvalidLength { actual: s.byte_len() }) } } // ────────────────────────────────────────────────────────────────────────── // Тесты — RFC 4648 § 10 test vectors — peer file `core_test.nv` (Plan 200 // П17 folder-module split, same module `encoding.base64`). // ──────────────────────────────────────────────────────────────────────────