///|
/// MoonGuard Manifest Module
/// Package manifest generation and hash calculation
pub struct FileHash {
path : String
hash : String
size : Int
} derive(Eq)
///|
pub struct Manifest {
version : String
package_name : String
package_version : String
total_files : Int
total_size : Int
created_at : String
files : Array[FileHash]
}
///|
pub(all) struct FileEntry {
path : String
content : String
}
///|
/// Create a new manifest
pub fn Manifest::new(
package_name : String,
package_version : String,
) -> Manifest {
Manifest::{
version: "1.0.0",
package_name,
package_version,
total_files: 0,
total_size: 0,
created_at: "2025-01-01",
files: [],
}
}
///|
/// SHA-256 initial hash values (first 32 bits of fractional parts of square roots of first 8 primes)
let sha256_h0 : Array[UInt] = [
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
0x5be0cd19,
]
///|
/// SHA-256 round constants (first 32 bits of fractional parts of cube roots of first 64 primes)
let sha256_k : Array[UInt] = [
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
0xc67178f2,
]
///|
/// Right-rotate a 32-bit unsigned integer
fn rotr(x : UInt, n : Int) -> UInt {
(x >> n) | (x << (32 - n))
}
///|
/// SHA-256 危0 function
fn sigma0(x : UInt) -> UInt {
rotr(x, 2) ^ rotr(x, 13) ^ rotr(x, 22)
}
///|
/// SHA-256 危1 function
fn sigma1(x : UInt) -> UInt {
rotr(x, 6) ^ rotr(x, 11) ^ rotr(x, 25)
}
///|
/// SHA-256 蟽0 function (message schedule)
fn small_sigma0(x : UInt) -> UInt {
rotr(x, 7) ^ rotr(x, 18) ^ (x >> 3)
}
///|
/// SHA-256 蟽1 function (message schedule)
fn small_sigma1(x : UInt) -> UInt {
rotr(x, 17) ^ rotr(x, 19) ^ (x >> 10)
}
///|
/// SHA-256 Ch function
fn ch(x : UInt, y : UInt, z : UInt) -> UInt {
(x & y) ^ (x.lnot() & z)
}
///|
/// SHA-256 Maj function
fn maj(x : UInt, y : UInt, z : UInt) -> UInt {
(x & y) ^ (x & z) ^ (y & z)
}
///|
/// Convert a string to bytes (UTF-8 encoding)
fn string_to_bytes(s : String) -> Array[Byte] {
let bytes : Array[Byte] = []
for i = 0; i < s.length(); i = i + 1 {
let c = s[i].to_int()
if c < 0x80 {
bytes.push(c.to_byte())
} else if c < 0x800 {
bytes.push(((0xC0 | (c >> 6)) & 0xFF).to_byte())
bytes.push(((0x80 | (c & 0x3F)) & 0xFF).to_byte())
} else if c < 0x10000 {
bytes.push(((0xE0 | (c >> 12)) & 0xFF).to_byte())
bytes.push(((0x80 | ((c >> 6) & 0x3F)) & 0xFF).to_byte())
bytes.push(((0x80 | (c & 0x3F)) & 0xFF).to_byte())
} else {
bytes.push(((0xF0 | (c >> 18)) & 0xFF).to_byte())
bytes.push(((0x80 | ((c >> 12) & 0x3F)) & 0xFF).to_byte())
bytes.push(((0x80 | ((c >> 6) & 0x3F)) & 0xFF).to_byte())
bytes.push(((0x80 | (c & 0x3F)) & 0xFF).to_byte())
}
}
bytes
}
///|
/// Pad message according to SHA-256 spec
fn sha256_pad(msg : Array[Byte]) -> Array[Byte] {
let orig_len = msg.length()
let bit_len = orig_len * 8
let padded : Array[Byte] = Array::new(capacity=orig_len + 72)
for i = 0; i < orig_len; i = i + 1 {
padded.push(msg[i])
}
padded.push(b'\x80')
while padded.length() % 64 != 56 {
padded.push(b'\x00')
}
// 64-bit big-endian length: upper 4 bytes are 0 for messages < 512MB
padded.push(b'\x00')
padded.push(b'\x00')
padded.push(b'\x00')
padded.push(b'\x00')
padded.push(((bit_len >> 24) & 0xFF).to_byte())
padded.push(((bit_len >> 16) & 0xFF).to_byte())
padded.push(((bit_len >> 8) & 0xFF).to_byte())
padded.push((bit_len & 0xFF).to_byte())
padded
}
///|
/// Process a single 512-bit block
fn sha256_process_block(
block : Array[Byte],
offset : Int,
h : Array[UInt],
) -> Unit {
let w : Array[UInt] = Array::make(64, 0U)
for i = 0; i < 16; i = i + 1 {
let base = offset + i * 4
w[i] = (block[base].to_int().reinterpret_as_uint() << 24) |
(block[base + 1].to_int().reinterpret_as_uint() << 16) |
(block[base + 2].to_int().reinterpret_as_uint() << 8) |
block[base + 3].to_int().reinterpret_as_uint()
}
for i = 16; i < 64; i = i + 1 {
w[i] = small_sigma1(w[i - 2]) +
w[i - 7] +
small_sigma0(w[i - 15]) +
w[i - 16]
}
let mut a = h[0]
let mut b = h[1]
let mut c = h[2]
let mut d = h[3]
let mut e = h[4]
let mut f = h[5]
let mut g = h[6]
let mut hh = h[7]
for i = 0; i < 64; i = i + 1 {
let t1 = hh + sigma1(e) + ch(e, f, g) + sha256_k[i] + w[i]
let t2 = sigma0(a) + maj(a, b, c)
hh = g
g = f
f = e
e = d + t1
d = c
c = b
b = a
a = t1 + t2
}
h[0] = h[0] + a
h[1] = h[1] + b
h[2] = h[2] + c
h[3] = h[3] + d
h[4] = h[4] + e
h[5] = h[5] + f
h[6] = h[6] + g
h[7] = h[7] + hh
}
///|
/// Convert a UInt to 8-character hex string
fn uint_to_hex(value : UInt) -> String {
let mut result = ""
for i = 7; i >= 0; i = i - 1 {
let nibble = ((value >> (i * 4)) & 0xFU).reinterpret_as_int()
let c = if nibble < 10 {
(nibble + 48).unsafe_to_char()
} else {
(nibble - 10 + 97).unsafe_to_char()
}
result = result + c.to_string()
}
result
}
///|
/// Calculate SHA-256 hash of a string
pub fn sha256(data : String) -> String {
let bytes = string_to_bytes(data)
let padded = sha256_pad(bytes)
let h : Array[UInt] = Array::make(8, 0U)
for i = 0; i < 8; i = i + 1 {
h[i] = sha256_h0[i]
}
let num_blocks = padded.length() / 64
for i = 0; i < num_blocks; i = i + 1 {
sha256_process_block(padded, i * 64, h)
}
let mut result = ""
for i = 0; i < 8; i = i + 1 {
result = result + uint_to_hex(h[i])
}
result
}
///|
/// Hash string content
pub fn hash_string(content : String) -> String {
sha256(content)
}
///|
/// Compute hash for a single file entry
pub fn compute_file_hash(entry : FileEntry) -> FileHash {
FileHash::{
path: entry.path,
hash: sha256(entry.content),
size: entry.content.length(),
}
}
///|
/// Generate manifest from a list of file entries
pub fn generate_manifest(
package_name : String,
package_version : String,
entries : Array[FileEntry],
) -> Manifest {
let files : Array[FileHash] = []
let mut total_size = 0
for i = 0; i < entries.length(); i = i + 1 {
let fh = compute_file_hash(entries[i])
total_size = total_size + fh.size
files.push(fh)
}
Manifest::{
version: "1.0.0",
package_name,
package_version,
total_files: entries.length(),
total_size,
created_at: "2025-01-01",
files,
}
}
///|
/// Verify file entries against an existing manifest, returns list of mismatched paths
pub fn verify_manifest(
manifest : Manifest,
entries : Array[FileEntry],
) -> Array[String] {
let mismatches : Array[String] = []
for i = 0; i < entries.length(); i = i + 1 {
let current_hash = sha256(entries[i].content)
let mut found = false
for j = 0; j < manifest.files.length(); j = j + 1 {
if manifest.files[j].path == entries[i].path {
found = true
if manifest.files[j].hash != current_hash {
mismatches.push(entries[i].path)
}
break
}
}
if !found {
mismatches.push(entries[i].path)
}
}
for j = 0; j < manifest.files.length(); j = j + 1 {
let mut found = false
for i = 0; i < entries.length(); i = i + 1 {
if entries[i].path == manifest.files[j].path {
found = true
break
}
}
if !found {
mismatches.push(manifest.files[j].path)
}
}
mismatches
}
///|
/// Get manifest summary
pub fn manifest_summary(manifest : Manifest) -> String {
"Package: " +
manifest.package_name +
" v" +
manifest.package_version +
", Files: " +
manifest.total_files.to_string() +
", Size: " +
manifest.total_size.to_string()
}
///|
/// Serialize manifest to JSON string
pub fn manifest_to_json(manifest : Manifest) -> String {
let mut json = "{\n"
json = json + " \"version\": \"" + manifest.version + "\",\n"
json = json + " \"package_name\": \"" + manifest.package_name + "\",\n"
json = json + " \"package_version\": \"" + manifest.package_version + "\",\n"
json = json + " \"total_files\": " + manifest.total_files.to_string() + ",\n"
json = json + " \"total_size\": " + manifest.total_size.to_string() + ",\n"
json = json + " \"files\": [\n"
for i = 0; i < manifest.files.length(); i = i + 1 {
let f = manifest.files[i]
json = json +
" {\"path\": \"" +
f.path +
"\", \"hash\": \"" +
f.hash +
"\", \"size\": " +
f.size.to_string() +
"}"
if i < manifest.files.length() - 1 {
json = json + ","
}
json = json + "\n"
}
json = json + " ]\n}"
json
}
///|
/// Parse a `manifest_to_json` document back into a `Manifest`. Returns
/// `None` when the document is malformed so the caller can fall back to a
/// freshly generated manifest. This is what lets the CLI audit an existing
/// on-disk `manifest.json` instead of only freshly generated ones.
pub fn manifest_from_json(json : String) -> Manifest? {
let version = match find_string_field(json, "version") {
Some(s) => s
None => "1.0.0"
}
let package_name = match find_string_field(json, "package_name") {
Some(s) => s
None => return None
}
let package_version = match find_string_field(json, "package_version") {
Some(s) => s
None => return None
}
let total_files = match find_json_int(json, "total_files") {
Some(n) => n
None => 0
}
let total_size = match find_json_int(json, "total_size") {
Some(n) => n
None => 0
}
let files : Array[FileHash] = []
let file_entries = match find_json_array(json, "files") {
Some(a) => a
None => []
}
for i = 0; i < file_entries.length(); i = i + 1 {
let e = file_entries[i]
let path = match find_string_field(e, "path") {
Some(s) => s
None => return None
}
let hash = match find_string_field(e, "hash") {
Some(s) => s
None => return None
}
let size = match find_json_int(e, "size") {
Some(n) => n
None => 0
}
files.push(FileHash::{ path, hash, size })
}
Some(Manifest::{
version,
package_name,
package_version,
total_files,
total_size,
created_at: "2025-01-01",
files,
})
}
///|
/// Locate the index of `needle` inside `haystack`, or `-1` when absent.
fn index_of(haystack : String, needle : String) -> Int {
if needle.length() == 0 || needle.length() > haystack.length() {
return -1
}
let mut i = 0
while i <= haystack.length() - needle.length() {
let mut j = 0
let mut match_ok = true
while j < needle.length() {
if haystack[i + j] != needle[j] {
match_ok = false
break
}
j = j + 1
}
if match_ok {
return i
}
i = i + 1
}
-1
}
///|
/// Pull a quoted string field out of a flat JSON object (`"key": "value"`).
fn find_string_field(json : String, key : String) -> String? {
let start = index_of(json, "\"" + key + "\":")
if start < 0 {
return None
}
// Skip the colon and any whitespace after it.
let needle = "\"" + key + "\":"
let mut i = start + needle.length()
while i < json.length() &&
(
json[i] == ' ' ||
json[i] == '\t' ||
json[i] == '\n' ||
json[i] == '\r'
) {
i = i + 1
}
if i < json.length() && json[i] != '"' {
return None
}
i = i + 1
let buf = StringBuilder::new(size_hint=16)
while i < json.length() {
let c = json[i]
if c == '"' {
break
}
buf.write_char(c.unsafe_to_char())
i = i + 1
}
Some(buf.to_string())
}
///|
/// Pull an integer field out of a flat JSON object (`"key": 123`).
fn find_json_int(json : String, key : String) -> Int? {
let start = index_of(json, "\"" + key + "\":")
if start < 0 {
return None
}
let needle = "\"" + key + "\":"
let mut i = start + needle.length()
while i < json.length() &&
(
json[i] == ' ' ||
json[i] == '\t' ||
json[i] == '\n' ||
json[i] == '\r'
) {
i = i + 1
}
let mut value = 0
let mut any = false
while i < json.length() {
let c = json[i].to_int()
if c >= 48 && c <= 57 {
value = value * 10 + (c - 48)
any = true
} else {
break
}
i = i + 1
}
if any {
Some(value)
} else {
None
}
}
///|
///|
/// Split a `"key": [ ... ]` array body into individual object strings.
fn find_json_array(json : String, key : String) -> Array[String]? {
let start = index_of(json, "\"" + key + "\":")
if start < 0 {
return None
}
let needle = "\"" + key + "\":"
let mut i = start + needle.length()
// Skip whitespace up to the opening `[`.
while i < json.length() &&
(
json[i] == ' ' ||
json[i] == '\t' ||
json[i] == '\n' ||
json[i] == '\r'
) {
i = i + 1
}
if i >= json.length() || json[i] != '[' {
return None
}
i = i + 1
let buf = StringBuilder::new(size_hint=16)
while i < json.length() {
let c = json[i]
if c == ']' {
break
}
buf.write_char(c.unsafe_to_char())
i = i + 1
}
let body = buf.to_string()
// Split the raw body into individual `{...}` objects by brace depth.
let result : Array[String] = []
let mut item_start = -1
let mut level = 0
for j = 0; j < body.length(); j = j + 1 {
let c = body[j]
if c == '{' {
if item_start < 0 {
item_start = j
}
level = level + 1
} else if c == '}' {
level = level - 1
if level == 0 && item_start >= 0 {
result.push(slice_chars_(body, item_start, j + 1))
item_start = -1
}
}
}
Some(result)
}
///|
/// Take the first `n` characters of `s`. Replaces the deprecated
/// `String::substring(start=0, end=n)` call site used by the verify and
/// CLI modules.
pub fn take_chars_(s : String, n : Int) -> String {
let buf = StringBuilder::new(size_hint=n)
let cap = if n < s.length() { n } else { s.length() }
for i = 0; i < cap; i = i + 1 {
buf.write_char(s[i].unsafe_to_char())
}
buf.to_string()
}
///|
/// Take the inclusive-exclusive `[start, end)` slice of `s`. Replaces the
/// deprecated `String::substring(start, end)` call site used by the verify
/// and CLI modules.
pub fn slice_chars_(s : String, start : Int, end_ : Int) -> String {
let buf = StringBuilder::new(size_hint=end_ - start)
let upper = if end_ < s.length() { end_ } else { s.length() }
for i = start; i < upper; i = i + 1 {
buf.write_char(s[i].unsafe_to_char())
}
buf.to_string()
}