///|
/// A stored snapshot whose PSL text or manifest cannot be trusted.
pub(all) enum SnapshotLoadError {
InvalidManifest(String)
InvalidRevisionEncoding
InvalidDigest(String)
InvalidRuleCount(String)
InvalidSnapshotRules(ListParseError)
NonCanonicalPsl
DigestMismatch(String, String)
RuleCountMismatch(Int, Int)
} derive(Eq, Debug)
///|
pub fn SnapshotLoadError::message(self : SnapshotLoadError) -> String {
match self {
InvalidManifest(reason) => "invalid snapshot manifest: \{reason}"
InvalidRevisionEncoding =>
"invalid snapshot manifest: source revision is not canonical percent-encoded UTF-8"
InvalidDigest(digest) =>
"invalid snapshot manifest: '\{digest}' is not a lowercase SHA-256 digest"
InvalidRuleCount(count) =>
"invalid snapshot manifest: '\{count}' is not a canonical rule count"
InvalidSnapshotRules(error) =>
"invalid snapshot PSL text: \{error.message()}"
NonCanonicalPsl => "snapshot PSL text is not in canonical form"
DigestMismatch(expected, actual) =>
"snapshot digest mismatch: manifest has \{expected}, PSL text has \{actual}"
RuleCountMismatch(expected, actual) =>
"snapshot rule-count mismatch: manifest has \{expected}, PSL text has \{actual}"
}
}
///|
fn hex_value(character : UInt16) -> Int? {
if character >= 0x30 && character <= 0x39 {
Some(character.to_int() - 0x30)
} else if character >= 0x61 && character <= 0x66 {
Some(character.to_int() - 0x61 + 10)
} else {
None
}
}
///|
fn decode_manifest_revision(
encoded : String,
) -> Result[String, SnapshotLoadError] {
let bytes : Array[Byte] = []
let mut index = 0
while index < encoded.length() {
let character = encoded[index]
if character == '%' {
if index + 2 >= encoded.length() {
return Err(InvalidRevisionEncoding)
}
let high = match hex_value(encoded[index + 1]) {
Some(value) => value
None => return Err(InvalidRevisionEncoding)
}
let low = match hex_value(encoded[index + 2]) {
Some(value) => value
None => return Err(InvalidRevisionEncoding)
}
bytes.push((high * 16 + low).to_byte())
index = index + 3
} else {
let value = character.to_int()
let unreserved = (character >= 'A' && character <= 'Z') ||
(character >= 'a' && character <= 'z') ||
(character >= '0' && character <= '9') ||
character == '-' ||
character == '.' ||
character == '_' ||
character == '~'
if !unreserved || value > 0x7f {
return Err(InvalidRevisionEncoding)
}
bytes.push(value.to_byte())
index = index + 1
}
}
let decoded = Ok(@utf8.decode(Bytes::from_array(bytes).exact_view())) catch {
_ => Err(InvalidRevisionEncoding)
}
match decoded {
Ok(revision) =>
if manifest_escape(revision) == encoded {
Ok(revision)
} else {
Err(InvalidRevisionEncoding)
}
Err(error) => Err(error)
}
}
///|
fn parse_rule_count(text : String) -> Result[Int, SnapshotLoadError] {
if text.length() == 0 {
return Err(InvalidRuleCount(text))
}
let mut value = 0
for character in text {
if character < '0' || character > '9' {
return Err(InvalidRuleCount(text))
}
let digit = character.to_int() - '0'.to_int()
if value > (0x7fffffff - digit) / 10 {
return Err(InvalidRuleCount(text))
}
value = value * 10 + digit
}
if value.to_string() != text {
Err(InvalidRuleCount(text))
} else {
Ok(value)
}
}
///|
fn valid_sha256_digest(digest : String) -> Bool {
if digest.length() != 64 {
return false
}
for character in digest {
if !((character >= '0' && character <= '9') ||
(character >= 'a' && character <= 'f')) {
return false
}
}
true
}
///|
fn manifest_field(line : String, prefix : String) -> String? {
if line.has_prefix(prefix) {
Some(line[prefix.length():].to_owned())
} else {
None
}
}
///|
/// Restore a snapshot from canonical PSL text and its stored manifest.
///
/// The manifest structure, revision encoding, digest, rule count, PSL syntax,
/// and canonical byte representation are all verified before a `Snapshot` is
/// returned.
pub fn Snapshot::restore(
psl_text : String,
manifest : String,
) -> Result[Snapshot, SnapshotLoadError] {
match restore_compiled(psl_text, manifest) {
Ok((snapshot, _)) => Ok(snapshot)
Err(error) => Err(error)
}
}
///|
/// Verify the snapshot and retain the parsed rules for callers that need them.
fn restore_compiled(
psl_text : String,
manifest : String,
) -> Result[(Snapshot, SuffixList), SnapshotLoadError] {
let lines : Array[String] = []
for line in manifest.split("\n") {
lines.push(line.to_owned())
}
if lines.length() != 5 || lines[4] != "" {
return Err(InvalidManifest("expected four LF-terminated fields"))
}
if lines[0] != "format=moonsuffix-snapshot-v1" {
return Err(InvalidManifest("unsupported format"))
}
let encoded_revision = match manifest_field(lines[1], "source_revision=") {
Some(value) => value
None => return Err(InvalidManifest("missing source_revision field"))
}
let expected_digest = match manifest_field(lines[2], "psl_sha256=") {
Some(value) => value
None => return Err(InvalidManifest("missing psl_sha256 field"))
}
let encoded_count = match manifest_field(lines[3], "rule_count=") {
Some(value) => value
None => return Err(InvalidManifest("missing rule_count field"))
}
let revision = match decode_manifest_revision(encoded_revision) {
Ok(value) => value
Err(error) => return Err(error)
}
if !valid_sha256_digest(expected_digest) {
return Err(InvalidDigest(expected_digest))
}
let expected_count = match parse_rule_count(encoded_count) {
Ok(value) => value
Err(error) => return Err(error)
}
let suffixes = match SuffixList::parse(psl_text) {
Ok(value) => value
Err(error) => return Err(InvalidSnapshotRules(error))
}
if suffixes.to_psl_text() != psl_text {
return Err(NonCanonicalPsl)
}
let actual_digest = sha256_hex(psl_text)
if actual_digest != expected_digest {
return Err(DigestMismatch(expected_digest, actual_digest))
}
let actual_count = suffixes.rule_count()
if actual_count != expected_count {
return Err(RuleCountMismatch(expected_count, actual_count))
}
Ok(
(
{
source_revision_: revision,
psl_text_: psl_text,
sha256_: actual_digest,
rule_count_: actual_count,
},
suffixes,
),
)
}