1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
|
//// The abstraction out of a pod archive.
////
//// A pod built with `--pod2` carries the abstraction inside it, at
//// `media/abstraction/<uid>.<lang>.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) })
}
/// `<uid>.<lang>.ssp` -> `<lang>`
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.
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:bytes-size(4), _:bits>> -> head == zstd_magic
_ -> False
}
}
@external(erlang, "unicode", "characters_to_binary")
fn bit_array_to_string_ffi(b: BitArray) -> String
fn bit_array_to_string(b: BitArray) -> Result(String, Nil) {
Ok(bit_array_to_string_ffi(b))
}
|