///|
/// Failures preserve the session's confirmed coverage. Storage failures belong
/// to the caller; release its lease before retrying. Checkpoint failures require
/// retaining the input and explicitly starting a new session.
pub suberror LedgerError {
InvalidSpec(String)
InvalidCheckpoint(String)
InvalidManifest(String)
StaleLease
InvalidResponse(String)
ReadbackMismatch(Int64)
RecoveryRequired
Incomplete(Int64)
DigestMismatch(String)
} derive(Debug)
///|
/// Identity includes the resource, representation length, strong HTTP validator
/// and independently obtained SHA-256. The ETag is not an integrity digest.
pub struct ObjectSpec {
resource : String
length : Int64
etag : String
sha256 : String
} derive(Eq, Debug)
///|
pub fn ObjectSpec::new(
resource : String,
length : Int64,
etag : String,
sha256 : String,
) -> ObjectSpec raise LedgerError {
if resource.is_empty() || resource.length() > 8192 {
raise InvalidSpec("resource must contain 1..8192 code units")
}
if length < 0L {
raise InvalidSpec("length must be nonnegative")
}
if !strong_etag(etag) {
raise InvalidSpec("a quoted strong ETag is required")
}
if !valid_digest(sha256) {
raise InvalidSpec("SHA-256 must be 64 lowercase hex digits")
}
{ resource, length, etag, sha256, }
}
///|
fn strong_etag(tag : String) -> Bool {
if tag.length() < 2 ||
tag.length() > 1024 ||
!tag.has_prefix("\"") ||
!tag.has_suffix("\"") {
return false
}
for i in 1..<(tag.length() - 1) {
let c = tag[i].to_int()
if c < 33 || c == 34 || c == 127 || c > 255 {
return false
}
}
true
}
///|
fn valid_digest(text : String) -> Bool {
if text.length() != 64 {
return false
}
for c in text {
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
return false
}
}
true
}
///|
/// SHA-256 of bytes, without decoding text. Provided for storage integration.
pub fn digest(bytes : Bytes) -> String {
@crypto.bytes_to_hex_string(@crypto.sha256(bytes))
}
///|
/// Half-open byte range [start, end).
pub(all) struct ByteRange {
start : Int64
end : Int64
} derive(Eq, Debug)
///|
pub fn ByteRange::length(self : ByteRange) -> Int64 {
self.end - self.start
}
///|
priv enum Block {
Missing
Reserved(Int64)
Confirmed(Int64, String)
Unverified(String)
}
///|
pub struct Session {
spec : ObjectSpec
chunk_size : Int32
priv blocks : Array[Block]
priv mut next_id : Int64
priv mut recovering : Bool
priv mut search_start : Int32
priv mut confirmed : Int64
}
///|
/// Session-bound capability; cannot be constructed or reused across restores.
pub struct Lease {
range : ByteRange
priv owner : Session
priv index : Int32
priv id : Int64
}
///|
pub struct PreparedChunk {
priv lease : Lease
priv sha256 : String
}
///|
pub(all) struct RecoveryReport {
retained_bytes : Int64
invalidated : Array[ByteRange]
} derive(Debug)
///|
pub extend LedgerError with Debug::{to_repr}
///|
pub extend ObjectSpec with Eq::{equal, not_equal}
///|
pub extend ObjectSpec with Debug::{to_repr}
///|
pub extend ByteRange with Eq::{equal, not_equal}
///|
pub extend ByteRange with Debug::{to_repr}
///|
pub extend RecoveryReport with Debug::{to_repr}