///|
/// Incremental decoded-text input. Limits are caller policy, not BagIt rules.
pub struct ManifestReader {
name : String
alg : Algorithm
tag : Bool
max_line_units : Int32?
max_entries : Int32?
entries : Array[Entry]
seen : Map[String, Bool]
mut buffer : StringBuilder
mut buffer_units : Int32
mut line : Int32
mut after_cr : Bool
mut finished : Bool
mut failure : String?
}
///|
pub fn manifest_reader(
name : String,
max_line_units? : Int32,
max_entries? : Int32,
) -> ManifestReader raise FormatError {
let header = parse_manifest(name, "")
for limit in [max_line_units, max_entries] {
if limit is Some(value) && value < 1 {
raise Invalid("manifest reader limits must be positive")
}
}
{
name,
alg: header.algorithm,
tag: header.tag,
max_line_units,
max_entries,
entries: [],
seen: Map([]),
buffer: StringBuilder(),
buffer_units: 0,
line: 1,
after_cr: false,
finished: false,
failure: None,
}
}
///|
fn ManifestReader::ensure_open(self : ManifestReader) -> Unit raise FormatError {
if self.failure is Some(message) {
raise Invalid(message)
}
guard !self.finished else {
raise Invalid("manifest reader is already finalized")
}
}
///|
fn ManifestReader::fail(
self : ManifestReader,
message : String,
) -> Unit raise FormatError {
let message = self.name + ":" + self.line.to_string() + ": " + message
self.failure = Some(message)
raise Invalid(message)
}
///|
fn ManifestReader::append_part(
self : ManifestReader,
part : String,
) -> Unit raise FormatError {
if self.max_line_units is Some(limit) &&
part.length() > limit - self.buffer_units {
self.fail("decoded line exceeds caller limit")
}
self.buffer.write_string(part)
self.buffer_units = self.buffer_units + part.length()
}
///|
fn ManifestReader::emit(self : ManifestReader) -> Unit raise FormatError {
if self.max_entries is Some(limit) && self.entries.length() >= limit {
self.fail("manifest entry count exceeds caller limit")
}
let parsed = parse_manifest(self.name, self.buffer.to_string() + "\n") catch {
Invalid(message) => {
self.fail(message)
abort("unreachable")
}
}
let entry = parsed.entries[0]
if self.seen.contains(entry.path) {
self.fail("duplicate manifest path: " + entry.path)
}
self.seen[entry.path] = true
self.entries.push(entry)
self.buffer = StringBuilder()
self.buffer_units = 0
self.line = self.line + 1
}
///|
/// Feed well-formed decoded chunks; a CRLF may straddle two chunks.
pub fn ManifestReader::feed(
self : ManifestReader,
chunk : String,
) -> Unit raise FormatError {
self.ensure_open()
let mut start = 0
let mut i = 0
while i < chunk.length() {
if self.after_cr {
self.after_cr = false
if chunk[i] == '\n' {
i = i + 1
start = i
continue
}
}
if chunk[i] == '\r' || chunk[i] == '\n' {
self.append_part(chunk[start:i].to_owned())
self.emit()
self.after_cr = chunk[i] == '\r'
i = i + 1
start = i
} else {
i = i + 1
}
}
self.append_part(chunk[start:].to_owned())
}
///|
pub fn ManifestReader::finish(
self : ManifestReader,
) -> Manifest raise FormatError {
self.ensure_open()
if self.buffer_units != 0 {
self.emit()
}
self.finished = true
{
name: self.name,
algorithm: self.alg,
tag: self.tag,
entries: self.entries.copy(),
}
}
///|
pub(all) struct LocatedDiagnostic {
path : String
line : Int32
code : String
message : String
} derive(ToJson)
///|
pub extend LocatedDiagnostic with ToJson::{to_json}
///|
pub(all) struct ManifestInspection {
name : String
parsed_entries : Array[Entry]
diagnostics : Array[LocatedDiagnostic]
}
///|
/// Recovery is for diagnostics only; its partial entries never imply validity.
pub fn inspect_manifest(name : String, text : String) -> ManifestInspection {
let diagnostics : Array[LocatedDiagnostic] = []
let entries : Array[Entry] = []
let header_ok = try {
ignore(parse_manifest(name, ""))
true
} catch {
Invalid(message) => {
diagnostics.push({ path: name, line: 0, code: "manifest_name", message, })
false
}
}
if !header_ok {
return { name, parsed_entries: entries, diagnostics, }
}
let rows = text
.replace_all(old="\r\n", new="\n")
.replace_all(old="\r", new="\n")
.split("\n")
.map(s => s.to_owned())
.collect()
if !rows.is_empty() && rows[rows.length() - 1] == "" {
ignore(rows.pop())
}
let seen : Map[String, Int32] = Map([])
for i in 0..
diagnostics.push({
path: name,
line: i + 1,
code: "manifest_duplicate",
message: entry.path + " first appears at line " + first.to_string(),
})
None => {
seen[entry.path] = i + 1
entries.push(entry)
}
}
} catch {
Invalid(message) =>
diagnostics.push({
path: name,
line: i + 1,
code: "manifest_row",
message,
})
}
}
{ name, parsed_entries: entries, diagnostics, }
}