///|
/// Resource category recorded in a `.reslock` file.
pub(all) enum AssetKind {
Text
Binary
Image
Audio
Font
Data
Other(String)
} derive(Eq)
///|
/// Cache and deployment intent for an asset.
pub(all) enum CachePolicy {
Immutable
Revalidate
NoStore
Runtime
} derive(Eq)
///|
/// Errors returned by parsers and constructors.
pub(all) enum ReslockError {
EmptyPath
AbsolutePath(String)
ParentTraversal(String)
InvalidPathSegment(String)
InvalidTag(String)
InvalidFingerprint(String)
DuplicatePath(String)
MissingHeader
InvalidLine(Int, String)
InvalidEscape(String)
BadNumber(String)
UnknownKind(String)
UnknownPolicy(String)
} derive(Eq)
///|
/// One resource entry inside a deterministic resource lockfile.
pub(all) struct Asset {
path : String
kind : AssetKind
bytes : Int
fingerprint : String
policy : CachePolicy
tags : Array[String]
note : String?
} derive(Eq)
///|
/// A resource manifest. Entries are sorted only when exported, so callers may
/// preserve discovery order during construction.
pub(all) struct Manifest {
name : String
version : String
entries : Array[Asset]
} derive(Eq)
///|
/// Validation severity.
pub(all) enum FindingLevel {
Error
Warning
Info
} derive(Eq)
///|
/// A validation finding with a stable machine-readable code.
pub(all) struct Finding {
level : FindingLevel
code : String
path : String?
message : String
} derive(Eq)
///|
/// Diff category for comparing two manifests.
pub(all) enum DiffKind {
Added
Removed
Changed
} derive(Eq)
///|
/// A single manifest diff row.
pub(all) struct EntryDiff {
kind : DiffKind
path : String
before : Asset?
after : Asset?
reasons : Array[String]
} derive(Eq)
///|
/// A compact, reproducible summary of a manifest.
pub(all) struct SealSummary {
entries : Int
total_bytes : Int
manifest_fingerprint : String
errors : Int
warnings : Int
} derive(Eq)
///|
pub fn AssetKind::to_wire(self : AssetKind) -> String {
match self {
Text => "text"
Binary => "binary"
Image => "image"
Audio => "audio"
Font => "font"
Data => "data"
Other(value) => "other:\{value}"
}
}
///|
pub fn CachePolicy::to_wire(self : CachePolicy) -> String {
match self {
Immutable => "immutable"
Revalidate => "revalidate"
NoStore => "no-store"
Runtime => "runtime"
}
}
///|
pub fn parse_kind(raw : String) -> Result[AssetKind, ReslockError] {
let value = raw.trim().to_owned().to_lower()
match value {
"text" => Ok(Text)
"binary" => Ok(Binary)
"image" => Ok(Image)
"audio" => Ok(Audio)
"font" => Ok(Font)
"data" => Ok(Data)
_ =>
match value.strip_prefix("other:") {
Some(rest) =>
if rest.is_empty() {
Err(UnknownKind(raw))
} else {
Ok(Other(rest.to_owned()))
}
None => Err(UnknownKind(raw))
}
}
}
///|
pub fn parse_policy(raw : String) -> Result[CachePolicy, ReslockError] {
match raw.trim().to_owned().to_lower() {
"immutable" => Ok(Immutable)
"revalidate" => Ok(Revalidate)
"no-store" => Ok(NoStore)
"runtime" => Ok(Runtime)
other => Err(UnknownPolicy(other))
}
}
///|
pub fn normalize_path(raw : String) -> Result[String, ReslockError] {
let trimmed = raw.trim().to_owned()
if trimmed.is_empty() {
return Err(EmptyPath)
}
let path = trimmed.replace_all(old="\\", new="/")
if path.has_prefix("/") {
return Err(AbsolutePath(raw))
}
if path.length() >= 2 && path.get_char(1) is Some(':') {
return Err(AbsolutePath(raw))
}
let parts : Array[String] = []
for piece in path.split("/") {
let part = piece.to_owned()
if part == "" || part == "." {
continue
}
if part == ".." {
return Err(ParentTraversal(raw))
}
if !valid_path_segment(part) {
return Err(InvalidPathSegment(part))
}
parts.push(part)
}
if parts.is_empty() {
Err(EmptyPath)
} else {
Ok(parts.join("/"))
}
}
///|
pub fn normalize_tags(tags : Array[String]) -> Array[String] {
let normalized : Array[String] = []
for tag in tags {
let clean = tag.trim().to_owned().to_lower()
if clean != "" && !normalized.contains(clean) {
normalized.push(clean)
}
}
normalized.sort()
normalized
}
///|
pub fn valid_tag(tag : String) -> Bool {
if tag.is_empty() {
return false
}
for ch in tag {
if !(ch.is_ascii_alphabetic() ||
ch.is_ascii_digit() ||
ch == '-' ||
ch == '_' ||
ch == '.') {
return false
}
}
true
}
///|
pub fn fingerprint_text(content : String) -> String {
let mut hash : UInt64 = 0xcbf29ce484222325UL
for ch in content {
hash = hash.lxor(ch.to_uint().to_uint64())
hash = hash * 0x100000001b3UL
}
"fnv1a64:" + hash.to_string(radix=16).pad_start(16, '0')
}
///|
pub fn fingerprint_fields(fields : Array[String]) -> String {
let joined = fields.join("\n")
fingerprint_text(joined)
}
///|
pub fn asset(
path : String,
content : String,
kind? : AssetKind = Data,
policy? : CachePolicy = Immutable,
tags? : Array[String] = [],
note? : String,
) -> Result[Asset, ReslockError] {
match normalize_path(path) {
Err(err) => Err(err)
Ok(path) => {
let tags = normalize_tags(tags)
for tag in tags {
if !valid_tag(tag) {
return Err(InvalidTag(tag))
}
}
Ok({
path,
kind,
bytes: content.length(),
fingerprint: fingerprint_text(content),
policy,
tags,
note,
})
}
}
}
///|
pub fn sealed_asset(
path : String,
kind : AssetKind,
bytes : Int,
fingerprint : String,
policy : CachePolicy,
tags? : Array[String] = [],
note? : String,
) -> Result[Asset, ReslockError] {
if bytes < 0 {
return Err(BadNumber(bytes.to_string()))
}
if !valid_fingerprint(fingerprint) {
return Err(InvalidFingerprint(fingerprint))
}
match normalize_path(path) {
Err(err) => Err(err)
Ok(path) => {
let tags = normalize_tags(tags)
for tag in tags {
if !valid_tag(tag) {
return Err(InvalidTag(tag))
}
}
Ok({ path, kind, bytes, fingerprint, policy, tags, note })
}
}
}
///|
pub fn manifest(
name : String,
version : String,
entries : Array[Asset],
) -> Manifest {
{ name, version, entries }
}
///|
pub fn validate_manifest(manifest : Manifest) -> Array[Finding] {
let findings : Array[Finding] = []
if manifest.name.trim().is_empty() {
findings.push(
finding_error("manifest-name-empty", None, "manifest name is empty"),
)
}
if manifest.version.trim().is_empty() {
findings.push(
finding_error("manifest-version-empty", None, "manifest version is empty"),
)
}
if manifest.entries.is_empty() {
findings.push(
finding_error("manifest-empty", None, "manifest has no entries"),
)
}
let seen : Map[String, Bool] = Map([])
for entry in manifest.entries {
match normalize_path(entry.path) {
Ok(path) =>
if path != entry.path {
findings.push(
finding_warning(
"path-not-normalized",
Some(entry.path),
"path normalizes to '\{path}'",
),
)
}
Err(err) =>
findings.push(
finding_error(
"path-invalid",
Some(entry.path),
"invalid path: " + error_message(err),
),
)
}
if seen.contains(entry.path) {
findings.push(
finding_error(
"path-duplicate",
Some(entry.path),
"same normalized path appears more than once",
),
)
} else {
seen.set(entry.path, true)
}
if entry.bytes < 0 {
findings.push(
finding_error(
"bytes-negative",
Some(entry.path),
"byte length is negative",
),
)
}
if !valid_fingerprint(entry.fingerprint) {
findings.push(
finding_error(
"fingerprint-invalid",
Some(entry.path),
"fingerprint must be fnv1a64 followed by 16 hex digits",
),
)
}
for tag in entry.tags {
if !valid_tag(tag) {
findings.push(
finding_error("tag-invalid", Some(entry.path), "invalid tag '\{tag}'"),
)
}
}
if entry.policy == NoStore && entry.kind == Image {
findings.push(
finding_warning(
"image-no-store",
Some(entry.path),
"image assets are usually cacheable unless generated at runtime",
),
)
}
}
findings
}
///|
pub fn is_valid(manifest : Manifest) -> Bool {
validate_manifest(manifest).all(f => f.level != Error)
}
///|
pub fn render_manifest(manifest : Manifest) -> String {
let entries = [ for entry in manifest.entries => entry ]
entries.sort_by((a, b) => a.path.compare(b.path))
let out = StringBuilder()
out.write_string("reslock|1|")
out.write_string(escape_field(manifest.name))
out.write_string("|")
out.write_string(escape_field(manifest.version))
out.write_string("\n")
for entry in entries {
out.write_string("entry|")
out.write_string(escape_field(entry.path))
out.write_string("|")
out.write_string(entry.kind.to_wire())
out.write_string("|")
out.write_string(entry.bytes.to_string())
out.write_string("|")
out.write_string(entry.fingerprint)
out.write_string("|")
out.write_string(entry.policy.to_wire())
out.write_string("|")
out.write_string(join_tags(entry.tags))
if entry.note is Some(note) {
out.write_string("|")
out.write_string(escape_field(note))
}
out.write_string("\n")
}
out.to_string()
}
///|
pub fn parse_manifest(input : String) -> Result[Manifest, ReslockError] {
let mut seen_header = false
let mut name = ""
let mut version = ""
let entries : Array[Asset] = []
let mut line_number = 0
for raw_line in input.split("\n") {
line_number = line_number + 1
let line0 = raw_line.to_owned()
let line = match line0.strip_suffix("\r") {
Some(clean) => clean.to_owned()
None => line0
}
let trimmed = line.trim()
if trimmed.is_empty() || trimmed.has_prefix("#") {
continue
}
let fields = match split_record(line, '|') {
Ok(fields) => fields
Err(err) => return Err(err)
}
if !seen_header {
if fields.length() != 4 || fields[0] != "reslock" || fields[1] != "1" {
return Err(InvalidLine(line_number, "expected reslock v1 header"))
}
name = fields[2]
version = fields[3]
seen_header = true
continue
}
if fields.length() != 7 && fields.length() != 8 {
return Err(InvalidLine(line_number, "entry rows need 7 or 8 fields"))
}
if fields[0] != "entry" {
return Err(InvalidLine(line_number, "unknown record '\{fields[0]}'"))
}
let kind = match parse_kind(fields[2]) {
Ok(kind) => kind
Err(err) => return Err(err)
}
let bytes = match parse_non_negative_int(fields[3]) {
Ok(bytes) => bytes
Err(err) => return Err(err)
}
let policy = match parse_policy(fields[5]) {
Ok(policy) => policy
Err(err) => return Err(err)
}
let tags = match parse_tags(fields[6]) {
Ok(tags) => tags
Err(err) => return Err(err)
}
let note = if fields.length() == 8 { Some(fields[7]) } else { None }
let entry = match note {
Some(note) =>
match
sealed_asset(fields[1], kind, bytes, fields[4], policy, tags~, note~) {
Ok(entry) => entry
Err(err) => return Err(err)
}
None =>
match sealed_asset(fields[1], kind, bytes, fields[4], policy, tags~) {
Ok(entry) => entry
Err(err) => return Err(err)
}
}
entries.push(entry)
}
if !seen_header {
Err(MissingHeader)
} else {
Ok({ name, version, entries })
}
}
///|
pub fn diff_manifest(before : Manifest, after : Manifest) -> Array[EntryDiff] {
let old_map : Map[String, Asset] = Map([])
for entry in before.entries {
old_map.set(entry.path, entry)
}
let touched : Map[String, Bool] = Map([])
let diffs : Array[EntryDiff] = []
for entry in after.entries {
touched.set(entry.path, true)
match old_map.get(entry.path) {
None =>
diffs.push({
kind: Added,
path: entry.path,
before: None,
after: Some(entry),
reasons: ["new"],
})
Some(old) => {
let reasons = changed_reasons(old, entry)
if !reasons.is_empty() {
diffs.push({
kind: Changed,
path: entry.path,
before: Some(old),
after: Some(entry),
reasons,
})
}
}
}
}
for entry in before.entries {
if !touched.contains(entry.path) {
diffs.push({
kind: Removed,
path: entry.path,
before: Some(entry),
after: None,
reasons: ["missing"],
})
}
}
diffs.sort_by((a, b) => a.path.compare(b.path))
diffs
}
///|
pub fn render_diff(diffs : Array[EntryDiff]) -> String {
if diffs.is_empty() {
return "no changes\n"
}
let out = StringBuilder()
for diff in diffs {
match diff.kind {
Added => out.write_string("+ ")
Removed => out.write_string("- ")
Changed => out.write_string("~ ")
}
out.write_string(diff.path)
if !diff.reasons.is_empty() {
out.write_string(" [")
out.write_string(diff.reasons.join(","))
out.write_string("]")
}
out.write_string("\n")
}
out.to_string()
}
///|
pub fn summarize(manifest : Manifest) -> SealSummary {
let findings = validate_manifest(manifest)
let mut errors = 0
let mut warnings = 0
for finding in findings {
match finding.level {
Error => errors = errors + 1
Warning => warnings = warnings + 1
Info => ()
}
}
{
entries: manifest.entries.length(),
total_bytes: total_bytes(manifest),
manifest_fingerprint: fingerprint_text(render_manifest(manifest)),
errors,
warnings,
}
}
///|
pub fn demo_manifest() -> Manifest {
let stylesheet = asset(
"public\\css\\app.css",
"body{color:#24313f}",
kind=Text,
tags=["critical", "style"],
note="first paint",
).unwrap()
let sprite = asset(
"public/img/logo.sprite",
"MOONBIT-RESOURCE-SPRITE",
kind=Image,
policy=Revalidate,
tags=["image", "ui"],
).unwrap()
manifest("reslock-demo", "2026.08", [stylesheet, sprite])
}
///|
fn finding_error(code : String, path : String?, message : String) -> Finding {
{ level: Error, code, path, message }
}
///|
fn finding_warning(code : String, path : String?, message : String) -> Finding {
{ level: Warning, code, path, message }
}
///|
fn valid_path_segment(segment : String) -> Bool {
if segment.is_empty() {
return false
}
for ch in segment {
if ch.is_control() || ch == '|' || ch == '\n' || ch == '\r' {
return false
}
}
true
}
///|
fn valid_fingerprint(value : String) -> Bool {
if !value.has_prefix("fnv1a64:") || value.length() != 24 {
return false
}
for ch in value.sub(start=8) {
if !ch.is_ascii_hexdigit() {
return false
}
}
true
}
///|
fn escape_field(value : String) -> String {
let out = StringBuilder()
for ch in value {
match ch {
'\\' => out.write_string("\\\\")
'|' => out.write_string("\\p")
',' => out.write_string("\\c")
'\n' => out.write_string("\\n")
'\r' => out.write_string("\\r")
'\t' => out.write_string("\\t")
_ => out.write_char(ch)
}
}
out.to_string()
}
///|
fn unescape_field(value : String) -> Result[String, ReslockError] {
let out = StringBuilder()
let mut escaping = false
for ch in value {
if escaping {
match ch {
'\\' => out.write_char('\\')
'p' => out.write_char('|')
'c' => out.write_char(',')
'n' => out.write_char('\n')
'r' => out.write_char('\r')
't' => out.write_char('\t')
_ => return Err(InvalidEscape(value))
}
escaping = false
} else if ch == '\\' {
escaping = true
} else {
out.write_char(ch)
}
}
if escaping {
Err(InvalidEscape(value))
} else {
Ok(out.to_string())
}
}
///|
fn split_escaped(
value : String,
delimiter : Char,
) -> Result[Array[String], ReslockError] {
let parts : Array[String] = []
let out = StringBuilder()
let mut escaping = false
for ch in value {
if escaping {
out.write_char('\\')
out.write_char(ch)
escaping = false
} else if ch == '\\' {
escaping = true
} else if ch == delimiter {
parts.push(out.to_string())
out.reset()
} else {
out.write_char(ch)
}
}
if escaping {
Err(InvalidEscape(value))
} else {
parts.push(out.to_string())
Ok(parts)
}
}
///|
fn split_record(
value : String,
delimiter : Char,
) -> Result[Array[String], ReslockError] {
let raw = match split_escaped(value, delimiter) {
Ok(raw) => raw
Err(err) => return Err(err)
}
let fields : Array[String] = []
for field in raw {
let value = match unescape_field(field) {
Ok(value) => value
Err(err) => return Err(err)
}
fields.push(value)
}
Ok(fields)
}
///|
fn join_tags(tags : Array[String]) -> String {
let rendered : Array[String] = []
for tag in tags {
rendered.push(escape_field(tag))
}
rendered.join(",")
}
///|
fn parse_tags(raw : String) -> Result[Array[String], ReslockError] {
if raw == "" {
return Ok([])
}
let pieces = match split_record(raw, ',') {
Ok(pieces) => pieces
Err(err) => return Err(err)
}
let tags = normalize_tags(pieces)
for tag in tags {
if !valid_tag(tag) {
return Err(InvalidTag(tag))
}
}
Ok(tags)
}
///|
fn changed_reasons(before : Asset, after : Asset) -> Array[String] {
let reasons : Array[String] = []
if before.fingerprint != after.fingerprint {
reasons.push("fingerprint")
}
if before.bytes != after.bytes {
reasons.push("bytes")
}
if before.kind != after.kind {
reasons.push("kind")
}
if before.policy != after.policy {
reasons.push("policy")
}
if before.tags != after.tags {
reasons.push("tags")
}
reasons
}
///|
fn total_bytes(manifest : Manifest) -> Int {
let mut total = 0
for entry in manifest.entries {
total = total + entry.bytes
}
total
}
///|
fn parse_non_negative_int(raw : String) -> Result[Int, ReslockError] {
let value = raw.trim().to_owned()
if value.is_empty() {
return Err(BadNumber(raw))
}
let mut total = 0
for ch in value {
if !ch.is_ascii_digit() {
return Err(BadNumber(raw))
}
total = total * 10 + ch.to_int() - '0'.to_int()
}
Ok(total)
}
///|
fn error_message(err : ReslockError) -> String {
match err {
EmptyPath => "empty path"
AbsolutePath(path) => "absolute path '\{path}'"
ParentTraversal(path) => "parent traversal in '\{path}'"
InvalidPathSegment(segment) => "invalid path segment '\{segment}'"
InvalidTag(tag) => "invalid tag '\{tag}'"
InvalidFingerprint(value) => "invalid fingerprint '\{value}'"
DuplicatePath(path) => "duplicate path '\{path}'"
MissingHeader => "missing reslock header"
InvalidLine(line, message) => "invalid line \{line}: \{message}"
InvalidEscape(value) => "invalid escape sequence in '\{value}'"
BadNumber(value) => "bad number '\{value}'"
UnknownKind(value) => "unknown kind '\{value}'"
UnknownPolicy(value) => "unknown policy '\{value}'"
}
}