summaryrefslogtreecommitdiffhomepage
path: root/gleam/src/ocda/inventory.gleam
blob: 604731361ac2f6a423587fd76b1297c8e4f57799 (plain)
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
//// What a source holds, beyond the one document the model is loaded from.
////
//// The model is one language of one document. Two questions a reader asks
//// are about the file instead: which languages it holds, and, for a
//// database, what else it carries. They are here rather than in a reader
//// because the answer depends on the input form, which is the library's to
//// know, and so that a reader asking them does not learn which form it was
//// handed. A mirror of the D library's `ocda.inventory`.
////
//// `languages` answers for every form, and answers the same: the languages
//// in the order the manifest gave them (which every `.ssp` records, in
//// `@source languages`), each with the markup file it was made from and the
//// sha256 of its `.ssp`. The database stores that digest; for a `.ssp` and a
//// pod it is taken from the bytes, which are the same bytes.
////
//// `carried` is the database's: a `.ocda.db` holds the markup, images,
//// configuration and catalogues it was built from, a `.ssp` holds none of
//// them, and a pod's are its archive entries rather than anything the
//// abstraction says.

import gleam/bit_array
import gleam/dict
import gleam/list
import gleam/result
import gleam/string
import ocda/load
import ocda/load_db
import ocda/load_pod
import ocda/load_ssp
import ocda/zip
import simplifile

pub type Language {
  Language(lang: String, source_filename: String, ssp_digest: String)
}

pub type Carried {
  Carried(
    role: String,
    compression: String,
    count: String,
    bytes: String,
    stored: String,
  )
}

pub fn languages(path: String) -> Result(List(Language), String) {
  case load.source_of(path) {
    load.OcdaDb -> {
      use held <- result.try(load_db.languages_of_file(path))
      Ok(
        list.map(held, fn(l) {
          Language(l.lang, l.source_filename, l.ssp_digest)
        }),
      )
    }
    load.Ssp -> {
      use bytes <- result.try(
        simplifile.read_bits(path)
        |> result.map_error(fn(e) { path <> ": " <> simplifile.describe_error(e) }),
      )
      use text <- result.try(
        bit_array.to_string(bytes)
        |> result.replace_error(path <> ": not valid UTF-8"),
      )
      let doc = load_ssp.parse(text)
      Ok([
        Language(
          source_value(doc.source, "language"),
          doc.source_filename,
          sha256_hex(bytes),
        ),
      ])
    }
    load.Pod -> {
      use archive <- result.try(load_pod.archive_bytes(path))
      use entries <- result.try(zip.entries(archive))
      use found <- result.try(
        zip.find_suffix(entries, ".ssp")
        |> list.try_map(fn(e) {
          use bytes <- result.try(zip.read(archive, e))
          use text <- result.try(
            bit_array.to_string(bytes)
            |> result.replace_error(e.name <> ": not valid UTF-8"),
          )
          let doc = load_ssp.parse(text)
          let lang = result.unwrap(load_pod.lang_of(e.name), "")
          Ok(#(
            Language(lang, doc.source_filename, sha256_hex(bytes)),
            string.split(source_value(doc.source, "languages"), " "),
          ))
        }),
      )
      let order = case found {
        [#(_, o), ..] -> list.filter(o, fn(l) { l != "" })
        [] -> []
      }
      let by_lang = list.map(found, fn(f) { #({ f.0 }.lang, f.0) }) |> dict.from_list
      // the manifest's order, and anything it did not name after it
      let named = list.filter_map(order, fn(l) { dict.get(by_lang, l) })
      let rest =
        list.filter(found, fn(f) { !list.contains(order, { f.0 }.lang) })
        |> list.map(fn(f) { f.0 })
      Ok(list.append(named, rest))
    }
    load.Unknown -> Error(path <> ": " <> load.source_name(load.Unknown))
  }
}

/// Only a `.ocda.db` carries files.
pub fn is_carrier(path: String) -> Bool {
  load.source_of(path) == load.OcdaDb
}

pub fn carried(path: String) -> Result(List(Carried), String) {
  case is_carrier(path) {
    False -> Ok([])
    True -> {
      use rows <- result.try(load_db.carried_of_file(path))
      Ok(
        list.filter_map(rows, fn(r) {
          case r {
            [role, compression, n, bytes, stored] ->
              Ok(Carried(role, compression, n, bytes, stored))
            _ -> Error(Nil)
          }
        }),
      )
    }
  }
}

fn source_value(source: dict.Dict(String, String), key: String) -> String {
  result.unwrap(dict.get(source, key), "")
}

@external(erlang, "ocda_ffi", "sha256_hex")
fn sha256_hex(bytes: BitArray) -> String