// BlobRef -- how a record points at a binary attachment: an image, a video, an
// avatar.
//
// Two encodings exist and both must be READ; only one may be WRITTEN.
//
// {"$type":"blob","ref":{"$link":"bafkrei..."},"mimeType":"image/jpeg","size":12345}
// {"cid":"bafkrei...","mimeType":"image/jpeg"} <- legacy, pre-1.0
//
// The legacy form has no size and no `$type`. It still appears in old records,
// so refusing to decode it would make those records unreadable -- but writing
// one is refused, because a PDS rejects it. rsky enforces the same asymmetry,
// in its case by bailing out of the write path with "Legacy blob ref at ...".
//
// The distinction is an enum rather than a struct with optional fields. The
// alternative -- rsky's `Blob`, where `ref`, `cid` and `size` are all optional
// so one struct covers both shapes -- makes it possible to build a blob with
// neither `ref` nor `cid`, and its `size: Option` with no skip attribute
// emits `"size": null`, which changes the record's CID.
///|
/// A reference to a blob, in whichever of the two encodings it arrived in.
pub(all) enum BlobRef {
/// The current form. `size` is the byte length the server recorded.
Typed(cid~ : @syntax.Cid, mime_type~ : String, size~ : Int64)
/// The pre-1.0 form. Readable, never writable; carries no size.
Legacy(cid~ : @syntax.Cid, mime_type~ : String)
} derive(Eq, Debug)
///|
pub fn BlobRef::cid(self : Self) -> @syntax.Cid {
match self {
Typed(cid~, ..) => cid
Legacy(cid~, ..) => cid
}
}
///|
pub fn BlobRef::mime_type(self : Self) -> String {
match self {
Typed(mime_type~, ..) => mime_type
Legacy(mime_type~, ..) => mime_type
}
}
///|
/// `None` for a legacy ref, which does not carry one.
///
/// Note what this is not: a guarantee. The size is what the server recorded
/// when the blob was uploaded, and nothing re-checks it.
pub fn BlobRef::size(self : Self) -> Int64? {
match self {
Typed(size~, ..) => Some(size)
Legacy(..) => None
}
}
///|
pub fn BlobRef::is_legacy(self : Self) -> Bool {
self is Legacy(..)
}
///|
/// Accepts both encodings.
///
/// The typed form's `ref` is a `cid-link`, so by the time this sees it the JSON
/// codec has already turned `{"$link": ...}` into a `Link`. The legacy form's
/// `cid` is a plain string, and has to be parsed here.
pub fn BlobRef::from_lex(
value : LexValue,
path? : String = "",
) -> BlobRef raise DecodeError {
guard value.as_object() is Some(fields) else {
raise DecodeError(path~, reason="expected a blob, got \{value.kind()}")
}
let mime_type = match fields.get("mimeType") {
Some(Str(m)) => m
_ => raise DecodeError(path~, reason="blob is missing mimeType")
}
if fields.get("$type") is Some(Str("blob")) {
let cid = match fields.get("ref") {
Some(Link(cid)) => cid
_ =>
raise DecodeError(
path=field_path(path, "ref"),
reason="blob ref must be a cid-link",
)
}
// A missing size is tolerated as zero rather than refused: the field is
// required by the schema, but a blob that names its content is still usable
// without it, and refusing would lose the whole record.
let size = match fields.get("size") {
Some(Int(size)) => size
_ => 0L
}
return Typed(cid~, mime_type~, size~)
}
match fields.get("cid") {
Some(Str(text)) => {
let cid = @syntax.Cid::parse(text) catch {
e =>
raise DecodeError(
path=field_path(path, "cid"),
reason="invalid blob cid: \{e.reason()}",
)
}
Legacy(cid~, mime_type~)
}
_ =>
raise DecodeError(
path~,
reason="blob has neither a $type of \"blob\" nor a legacy cid",
)
}
}
///|
/// Encodes back into whichever form it came from.
///
/// A legacy ref is NOT upgraded on the way out. Rewriting it into the typed
/// form would change the record's bytes and therefore its CID, turning a
/// read-modify-write into a different record.
pub fn BlobRef::to_lex(self : Self) -> LexValue {
match self {
Typed(cid~, mime_type~, size~) =>
Obj({
"$type": Str("blob"),
"ref": Link(cid),
"mimeType": Str(mime_type),
"size": Int(size),
})
Legacy(cid~, mime_type~) =>
Obj({ "cid": Str(cid.to_string()), "mimeType": Str(mime_type) })
}
}