//// The abstraction out of a pod archive. //// //// A pod built with `--pod2` carries the abstraction inside it, at //// `media/abstraction/..ssp`, and that is what is read. So this is //// a third input form and *not* a third model: the `.ssp` inside is parsed by //// `ocda/load_ssp` and comes back as the same value. A writer over the //// library gains pods without being touched, which is the whole reason the //// dispatch is in one place. //// //// ## The one thing this cannot do //// //// A `.sisupod` is **one zstd frame wrapping** the zip. This reads the zip //// and **not the frame**, because there is no zstd on the BEAM without a NIF, //// and the one that exists, `ezstd`, clones zstd from github in its build, so //// it cannot be built in a sandbox without patching it. The D library has //// both, through a declared `libzstd`. //// //// So: a `.zip` pod is read, a `.sisupod` is refused, and the refusal says //// why rather than pretending the file is malformed. It is the only place the //// two libraries differ in what they accept, and it is a packaging limit //// rather than anything about the format. import gleam/list import gleam/result import gleam/string import ocda/load_ssp import ocda/model.{type Document} import ocda/zip import simplifile /// The magic number of a zstd frame, little endian 0xFD2FB528. const zstd_magic = <<0x28, 0xB5, 0x2F, 0xFD>> /// Which languages of the abstraction the pod carries, sorted. pub fn languages(path: String) -> Result(List(String), String) { use bytes <- result.try(archive_bytes(path)) use entries <- result.try(zip.entries(bytes)) zip.find_suffix(entries, ".ssp") |> list.filter_map(fn(e) { lang_of(e.name) }) |> list.sort(string.compare) |> Ok } /// Load one language of the abstraction a pod carries. /// /// `lang` == "" takes the only one there is, and where there is a choice it /// says so rather than choosing, as the `.ocda.db` loader does. pub fn load_file(path: String, lang: String) -> Result(Document, String) { use bytes <- result.try(archive_bytes(path)) use entries <- result.try(zip.entries(bytes)) let ssps = zip.find_suffix(entries, ".ssp") use chosen <- result.try(case ssps, lang { [], _ -> Error( path <> ": carries no .ssp. A pod carries the abstraction only when spine" <> " was asked for it: spine --pod2 puts it in, spine --source does not.", ) [only], "" -> Ok(only) _, "" -> Error( path <> ": holds more than one language (" <> string.join(langs(ssps), " ") <> "); name the one wanted", ) _, wanted -> case list.find(ssps, fn(e) { lang_of(e.name) == Ok(wanted) }) { Ok(found) -> Ok(found) Error(_) -> Error( path <> ": no language '" <> wanted <> "'; it holds: " <> string.join(langs(ssps), " "), ) } }) use content <- result.try(zip.read(bytes, chosen)) case bit_array_to_string(content) { Ok(text) -> Ok(load_ssp.parse(text)) Error(_) -> Error(chosen.name <> ": not valid UTF-8") } } fn langs(entries: List(zip.Entry)) -> List(String) { list.filter_map(entries, fn(e) { lang_of(e.name) }) } /// `..ssp` -> `` pub fn lang_of(name: String) -> Result(String, Nil) { case string.ends_with(name, ".ssp") { False -> Error(Nil) True -> { let stem = string.drop_end(name, 4) case string.split(stem, on: ".") |> list.last { Ok(lang) if lang != stem -> Ok(lang) _ -> Error(Nil) } } } } /// The bytes decide, not the name: a file misnamed either way is treated by /// what it holds. pub fn archive_bytes(path: String) -> Result(BitArray, String) { case simplifile.read_bits(path) { Error(e) -> Error(path <> ": " <> simplifile.describe_error(e)) Ok(bytes) -> case is_zstd_frame(bytes) { False -> Ok(bytes) True -> Error( path <> " is a zstd frame wrapping the archive, and this library has no" <> " zstd: the only one on the BEAM is a NIF that fetches zstd in" <> " its own build, so it cannot be built sandboxed. Unwrap it" <> " first (zstd -d) and read the archive, or use the D library," <> " which reads both.", ) } } } fn is_zstd_frame(bytes: BitArray) -> Bool { case bytes { <> -> head == zstd_magic _ -> False } } @external(erlang, "unicode", "characters_to_binary") fn bit_array_to_string_ffi(b: BitArray) -> String pub fn bit_array_to_string(b: BitArray) -> Result(String, Nil) { Ok(bit_array_to_string_ffi(b)) }