use serde_json::Value;
use vta_sdk::prelude::*;
use vtc_client::VtcClient;
use vtc_client::rooms::body::{
RecordLocation, SealedRecordBody, VerifiedRecordBody, verify_record_body,
};
use vtc_client::rooms::{CleartextContent, RoomSession, SealedContent, Visibility};
pub struct RoomTarget<'a> {
pub host_url: &'a str,
pub host_did: Option<&'a str>,
pub room_id: &'a str,
}
impl RoomTarget<'_> {
fn client(&self) -> VtcClient {
VtcClient::anonymous(self.host_url, self.host_did.unwrap_or("did:key:zHost"))
}
}
async fn present(
client: &VtaClient,
target: &RoomTarget<'_>,
action: &str,
) -> Result<RoomSession, Box<dyn std::error::Error>> {
let minted = client.room_present(target.room_id, action).await?;
session_from_minted(target.room_id, &minted)
}
pub fn session_from_minted(
room_id: &str,
minted: &Value,
) -> Result<RoomSession, Box<dyn std::error::Error>> {
let presentation = minted
.get("presentation")
.ok_or_else(|| format!("the VTA's reply carried no presentation: {minted}"))?;
let membership = presentation
.get("membership")
.and_then(Value::as_str)
.ok_or("the minted presentation carried no membership credential")?;
let authority_values = presentation
.get("authority")
.and_then(Value::as_array)
.ok_or("the minted presentation carried no authority chain")?;
let authority: Vec<String> = authority_values
.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect();
if authority.len() != authority_values.len() {
return Err("the minted authority chain contained a non-string link".into());
}
let mut session = RoomSession::new(room_id, membership, authority)?;
if let Some(binding) = presentation.get("subjectBinding").and_then(Value::as_str) {
session = session.with_subject_binding(binding);
}
Ok(session)
}
pub fn pinned_from_flags(pin: bool, unpin: bool) -> Option<bool> {
if pin {
Some(true)
} else if unpin {
Some(false)
} else {
None
}
}
pub struct RoomSigner<'a> {
pub did: &'a str,
pub key_multibase: &'a str,
}
pub async fn cmd_rooms_list(
client: &VtaClient,
target: RoomTarget<'_>,
signer: RoomSigner<'_>,
prefix: Option<&str>,
since_version: Option<u64>,
limit: Option<usize>,
) -> Result<(), Box<dyn std::error::Error>> {
let session = present(client, &target, "read").await?;
let mut listing = target
.client()
.list_records(
&session,
prefix,
since_version,
None,
signer.did,
signer.key_multibase,
)
.await?;
let mut cursor = listing.cursor.clone();
while let Some(c) = cursor {
let next = target
.client()
.list_records(
&session,
prefix,
since_version,
Some(&c),
signer.did,
signer.key_multibase,
)
.await?;
cursor = next.cursor.clone();
listing.records.extend(next.records);
}
if crate::render::is_json_output() {
crate::render::print_json(&listing.records)?;
return Ok(());
}
if listing.records.is_empty() {
println!(
"No records{}.",
prefix.map(|p| format!(" under `{p}`")).unwrap_or_default()
);
return Ok(());
}
let shown = limit.unwrap_or(usize::MAX).min(listing.records.len());
if shown < listing.records.len() {
println!(
"{} record(s), showing {shown} — pass a larger --limit for the rest:",
listing.records.len()
);
} else {
println!("{} record(s):", listing.records.len());
}
for r in listing.records.iter().take(shown) {
let key = r.get("key").and_then(Value::as_str).unwrap_or("?");
let version = r.get("version").and_then(Value::as_u64).unwrap_or(0);
let status = r.get("status").and_then(Value::as_str).unwrap_or("active");
let title = r
.get("cleartext")
.and_then(|c| c.get("title"))
.and_then(Value::as_str)
.unwrap_or("(sealed)");
println!(" {key} v{version} {status:<10} {title}");
}
Ok(())
}
pub async fn cmd_rooms_get(
client: &VtaClient,
target: RoomTarget<'_>,
signer: RoomSigner<'_>,
key: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let session = present(client, &target, "read").await?;
let record = target
.client()
.get_record(&session, key, signer.did, signer.key_multibase)
.await?;
if let Some(cleartext) = record.get("cleartext") {
if crate::render::is_json_output() {
crate::render::print_json(cleartext)?;
} else {
if let Some(title) = cleartext.get("title").and_then(Value::as_str) {
println!("{}\n", inert(title));
}
println!(
"{}",
inert(cleartext.get("body").and_then(Value::as_str).unwrap_or(""))
);
}
return Ok(());
}
let sealed: SealedContent =
serde_json::from_value(record.get("sealed").cloned().ok_or(
"the record carried neither cleartext nor sealed content; it may be retracted",
)?)
.map_err(|e| format!("the host's sealed record is malformed: {e}"))?;
let version = record
.get("version")
.and_then(Value::as_u64)
.ok_or("the host's record carried no version")?;
let author = record.get("author").and_then(Value::as_str);
let body = open_and_verify(client, &target, key, version, &sealed, author).await?;
let body = body.body();
if crate::render::is_json_output() {
crate::render::print_json(body)?;
return Ok(());
}
if let Some(title) = &body.title {
println!("{}\n", inert(title));
}
if let Some(text) = &body.body {
println!("{}", inert(text));
}
if let Some(file) = &body.file {
println!(
"\nFile: {} ({} bytes). Fetch it with `pnm rooms file get {key}`.",
inert(&file.name),
file.size
);
}
println!("\nWritten by {} at version {version}.", body.author);
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub async fn cmd_rooms_put(
client: &VtaClient,
target: RoomTarget<'_>,
signer: RoomSigner<'_>,
key: &str,
title: Option<String>,
body: String,
expected_version: Option<u64>,
) -> Result<(), Box<dyn std::error::Error>> {
let read = present(client, &target, "read").await?;
let sealed_tier = is_sealed_tier(&target, &signer, &read).await?;
let write = present(client, &target, "write").await?;
if !sealed_tier {
let put = target
.client()
.put_record(
&write,
key,
None,
Some(CleartextContent {
title,
body,
..Default::default()
}),
expected_version,
signer.did,
signer.key_multibase,
)
.await?;
println!("Wrote {} at version {}", put.key, put.version);
return Ok(());
}
let mut content = SealedRecordBody::new(
RecordLocation {
room_id: target.room_id,
key,
version: 1,
},
signer.did,
);
content.title = title;
content.body = Some(body);
let version = write_sealed(
client,
&target,
&signer,
&read,
&write,
content,
None,
expected_version,
)
.await?;
println!(
"Wrote {key} at version {version}, sealed and signed by {}",
signer.did
);
Ok(())
}
pub async fn cmd_rooms_curate(
client: &VtaClient,
target: RoomTarget<'_>,
signer: RoomSigner<'_>,
key: &str,
status: Option<String>,
pinned: Option<bool>,
reason: Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
if status.is_none() && pinned.is_none() {
return Err("nothing to change — pass --status and/or --pin/--unpin".into());
}
let session = present(client, &target, "curate").await?;
let out = target
.client()
.curate_record(
&session,
key,
status,
pinned,
reason,
signer.did,
signer.key_multibase,
)
.await?;
println!(
"{} is now {} at version {}{}",
out.key,
serde_json::to_value(out.status)
.ok()
.and_then(|v| v.as_str().map(str::to_string))
.unwrap_or_else(|| "?".into()),
out.version,
if out.pinned { " (pinned)" } else { "" }
);
Ok(())
}
pub async fn cmd_rooms_renew(
client: &VtaClient,
target: RoomTarget<'_>,
signer: RoomSigner<'_>,
epoch: u32,
reason: Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
let session = present(client, &target, "admin").await?;
let minted = target
.client()
.mint_epoch(
&session,
epoch,
reason.as_deref(),
signer.did,
signer.key_multibase,
)
.await?;
println!("Room {} is at epoch {}", minted.room_id, minted.epoch);
Ok(())
}
pub async fn cmd_rooms_create(
target: RoomTarget<'_>,
signer: RoomSigner<'_>,
visibility: &str,
retention_days: Option<u32>,
) -> Result<(), Box<dyn std::error::Error>> {
let visibility: Visibility = serde_json::from_value(Value::String(visibility.to_string()))
.map_err(|_| "visibility must be one of: open, attributed, private")?;
target
.client()
.create_room(
target.room_id,
signer.did,
visibility,
retention_days,
signer.did,
signer.key_multibase,
)
.await
.map_err(|e| {
format!(
"{e}\n\nA community host decides whose rooms it stores. If this says \
`not-a-member`, you are not a member there; if it says \
`private-tier-not-enabled`, that community has not turned the tier on."
)
})?;
println!("Registered {} as {}", target.room_id, signer.did);
println!(
" Next: the room must issue you a membership and an authority credential before \
you can act in it."
);
Ok(())
}
const BLOB_MANIFEST: &str = "blob.json";
const FILE_MANIFEST: &str = "file.json";
const CHUNKS_DIR: &str = "chunks";
fn chunk_path(dir: &std::path::Path, index: usize) -> std::path::PathBuf {
dir.join(CHUNKS_DIR).join(format!("{index:06}.bin"))
}
fn file_key_bytes(
resp: &trust_tasks_rs::specs::rooms::keys::file_key::v0_1::Response,
) -> Result<vti_rooms::files::FileKey, Box<dyn std::error::Error>> {
use base64::Engine as _;
let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(resp.key.as_str())?;
bytes
.try_into()
.map_err(|_| "the VTA returned a file key that is not 32 bytes".into())
}
pub async fn cmd_rooms_file_seal(
client: &VtaClient,
room_id: &str,
path: &std::path::Path,
out: &std::path::Path,
padme: bool,
) -> Result<(), Box<dyn std::error::Error>> {
use vti_rooms::files::{self, FileBinding, Padding};
if out.exists() {
return Err(format!("{} already exists; choose a new directory", out.display()).into());
}
let plaintext = std::fs::read(path)?;
let name = path
.file_name()
.and_then(|n| n.to_str())
.ok_or("the file has no usable name")?;
let file_id = files::new_file_id();
let resp = client
.room_file_key(room_id, &files::encode_file_id(&file_id), None)
.await?;
let key = file_key_bytes(&resp)?;
let sealed = files::seal_file(
&key,
FileBinding {
room_id: room_id.to_string(),
file_id,
epoch: resp.epoch.get(),
segment_size: files::DEFAULT_SEGMENT_SIZE,
},
name,
None,
&plaintext,
if padme { Padding::Padme } else { Padding::None },
)?;
std::fs::create_dir_all(out.join(CHUNKS_DIR))?;
for (i, chunk) in sealed.chunks.iter().enumerate() {
std::fs::write(chunk_path(out, i), chunk)?;
}
std::fs::write(
out.join(BLOB_MANIFEST),
serde_json::to_vec_pretty(&sealed.blob)?,
)?;
std::fs::write(
out.join(FILE_MANIFEST),
serde_json::to_vec_pretty(&sealed.file)?,
)?;
println!(
"Sealed {} ({} bytes) under epoch {} into {}\n blobRef {}\n chunks {} of {} bytes",
name,
sealed.file.size,
sealed.file.epoch,
out.display(),
sealed.file.blob_ref,
sealed.blob.chunks.chunk_count,
sealed.blob.chunks.chunk_size,
);
println!(
" {FILE_MANIFEST} goes inside the sealed record body; it names the file and is never \
sent to the host in the clear."
);
Ok(())
}
pub async fn cmd_rooms_file_open(
client: &VtaClient,
room_id: &str,
dir: &std::path::Path,
out: &std::path::Path,
) -> Result<(), Box<dyn std::error::Error>> {
use vti_rooms::files::{self, BlobManifest, FileManifest};
if out.exists() {
return Err(format!("{} already exists; refusing to overwrite it", out.display()).into());
}
let file: FileManifest = serde_json::from_slice(&std::fs::read(dir.join(FILE_MANIFEST))?)?;
let blob: BlobManifest = serde_json::from_slice(&std::fs::read(dir.join(BLOB_MANIFEST))?)?;
file.check_blob(&blob)?;
let mut chunks = Vec::with_capacity(blob.chunks.chunk_count as usize);
for (i, expected) in blob.chunks.chunk_digests.iter().enumerate() {
let chunk = std::fs::read(chunk_path(dir, i))?;
if !files::same_digest(&files::digest_multibase(&chunk), expected) {
return Err(format!("chunk {i} does not match the manifest").into());
}
chunks.push(chunk);
}
let resp = client
.room_file_key(room_id, &file.file_id, Some(file.epoch))
.await?;
let key = file_key_bytes(&resp)?;
let plaintext = files::open_file(&key, room_id, &file, &blob, &chunks)?;
let tmp = out.with_extension("partial");
std::fs::write(&tmp, &plaintext)?;
std::fs::rename(&tmp, out)?;
println!(
"Opened {} ({} bytes) into {} — the digest its author signed matches.",
file.name.escape_debug(),
plaintext.len(),
out.display()
);
Ok(())
}
const SEALED_WRITE_ATTEMPTS: usize = 4;
fn inert(text: &str) -> String {
text.chars()
.filter(|c| !c.is_control() || *c == '\n' || *c == '\t')
.collect()
}
fn opaque_key() -> String {
let bytes: [u8; 16] = rand::random();
base64::Engine::encode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, bytes)
}
fn wire_str<T: serde::Serialize>(value: &T) -> String {
serde_json::to_value(value)
.ok()
.and_then(|v| v.as_str().map(str::to_string))
.unwrap_or_default()
}
async fn is_sealed_tier(
target: &RoomTarget<'_>,
signer: &RoomSigner<'_>,
read: &RoomSession,
) -> Result<bool, Box<dyn std::error::Error>> {
let info = target
.client()
.room_info(read, signer.did, signer.key_multibase)
.await
.map_err(|e| format!("could not read the room's tier (rooms/info): {e}"))?;
Ok(wire_str(&info.visibility) != "open")
}
#[allow(clippy::too_many_arguments)]
async fn write_sealed(
client: &VtaClient,
target: &RoomTarget<'_>,
signer: &RoomSigner<'_>,
read: &RoomSession,
write: &RoomSession,
content: SealedRecordBody,
blobs: Option<Vec<String>>,
expected_version: Option<u64>,
) -> Result<u64, Box<dyn std::error::Error>> {
let holder =
vta_sdk::trust_task_sign::HolderKey::from_did_key(signer.did, signer.key_multibase)?;
let host = target.client();
let head = host
.list_records(read, None, None, None, signer.did, signer.key_multibase)
.await?
.head_version
.unwrap_or(0);
let key = content.key.clone();
let mut intended = head + 1;
let mut expected = expected_version;
let mut landed = 0;
for _ in 0..SEALED_WRITE_ATTEMPTS {
let signed = content.clone().at_version(intended).sign(&holder).await?;
let sealed = client
.room_seal(target.room_id, &key, intended, &signed)
.await?
.sealed;
let sealed = SealedContent {
ciphertext: sealed.ciphertext,
nonce: sealed.nonce,
epoch: u32::try_from(sealed.epoch.get())
.map_err(|_| "the VTA sealed under an epoch this client cannot represent")?,
};
landed = match &blobs {
Some(blobs) => host
.put_sealed_record_with_blobs(
write,
&key,
sealed,
blobs,
expected,
signer.did,
signer.key_multibase,
)
.await?
.version
.get(),
None => {
host.put_record(
write,
&key,
Some(sealed),
None,
expected,
signer.did,
signer.key_multibase,
)
.await?
.version
}
};
if landed == intended {
return Ok(landed);
}
expected = Some(landed);
intended = landed + 1;
}
Err(format!(
"{key} is stored at version {landed} but sealed for another version, so it does not \
open: the room kept moving under {SEALED_WRITE_ATTEMPTS} attempts. Write it again \
with --expected-version {landed}."
)
.into())
}
async fn open_and_verify(
client: &VtaClient,
target: &RoomTarget<'_>,
key: &str,
version: u64,
sealed: &SealedContent,
host_author: Option<&str>,
) -> Result<VerifiedRecordBody, Box<dyn std::error::Error>> {
let opened = client
.room_open(
target.room_id,
key,
version,
&sealed.ciphertext,
&sealed.nonce,
sealed.epoch,
)
.await
.map_err(|e| {
format!(
"{e}\n\nIf this mentions an epoch, your VTA has not been given the room's \
latest commit — ask the room's owner to deliver it, then retry."
)
})?;
let plaintext = opened
.get("plaintext")
.and_then(Value::as_str)
.ok_or("the VTA opened the record but returned no plaintext")?;
let bytes =
base64::Engine::decode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, plaintext)?;
let resolver = vta_sdk::trust_task_proof::TrustTaskVmResolver::from_optional(
vta_sdk::resolver::shared_did_resolver_from_env().await.ok(),
);
let location = RecordLocation {
room_id: target.room_id,
key,
version,
};
Ok(verify_record_body(&bytes, location, host_author, &resolver)
.await
.map_err(|e| format!("refusing record {key}: {e}"))?)
}
#[allow(clippy::too_many_arguments)]
pub async fn cmd_rooms_file_put(
client: &VtaClient,
target: RoomTarget<'_>,
signer: RoomSigner<'_>,
path: &std::path::Path,
title: Option<String>,
description: Option<String>,
tags: Vec<String>,
padme: bool,
) -> Result<(), Box<dyn std::error::Error>> {
use vti_rooms::files::{self, FileBinding, Padding};
let read = present(client, &target, "read").await?;
if !is_sealed_tier(&target, &signer, &read).await? {
return Err(
"this room is `open`: its records are cleartext, and a file's manifest \
belongs inside a sealed body. Files go in `attributed` and `private` rooms."
.into(),
);
}
let write = present(client, &target, "write").await?;
let plaintext = std::fs::read(path)?;
let name = path
.file_name()
.and_then(|n| n.to_str())
.ok_or("the file has no usable name")?;
let file_id = files::new_file_id();
let resp = client
.room_file_key(target.room_id, &files::encode_file_id(&file_id), None)
.await?;
let file_key = file_key_bytes(&resp)?;
let sealed = files::seal_file(
&file_key,
FileBinding {
room_id: target.room_id.to_string(),
file_id,
epoch: resp.epoch.get(),
segment_size: files::DEFAULT_SEGMENT_SIZE,
},
name,
None,
&plaintext,
if padme { Padding::Padme } else { Padding::None },
)?;
let host = target.client();
let begun = host
.begin_upload(&write, &sealed.blob, signer.did, signer.key_multibase)
.await?;
let blob_ref = if begun.already_committed == Some(true) {
sealed.file.blob_ref.clone()
} else {
let upload_id = wire_str(&begun.upload_id);
let sent: Result<(), Box<dyn std::error::Error>> = async {
for index in &begun.missing {
let i = usize::try_from(index.0)
.ok()
.filter(|i| *i < sealed.chunks.len())
.ok_or_else(|| {
format!("the host asked for chunk {}, which does not exist", index.0)
})?;
host.upload_chunk(
&upload_id,
i as u64,
&sealed.chunks[i],
signer.did,
signer.key_multibase,
)
.await?;
}
Ok(())
}
.await;
if let Err(e) = sent {
let _ = host
.abort_upload(&upload_id, signer.did, signer.key_multibase)
.await;
return Err(e);
}
let committed = host
.commit_upload(&upload_id, signer.did, signer.key_multibase)
.await?;
wire_str(&committed.blob_ref)
};
if !files::same_digest(&blob_ref, &sealed.file.blob_ref) {
return Err(format!(
"the host committed {blob_ref}, not the blob sealed here ({})",
sealed.file.blob_ref
)
.into());
}
let key = opaque_key();
let mut content = SealedRecordBody::new(
RecordLocation {
room_id: target.room_id,
key: &key,
version: 1,
},
signer.did,
);
content.title = title;
content.description = description;
content.tags = tags;
content.file = Some(sealed.file.clone());
let version = write_sealed(
client,
&target,
&signer,
&read,
&write,
content,
Some(vec![blob_ref.clone()]),
Some(0),
)
.await?;
if crate::render::is_json_output() {
crate::render::print_json(&serde_json::json!({
"key": key,
"version": version,
"blobRef": blob_ref,
"size": sealed.file.size,
}))?;
return Ok(());
}
println!(
"Put {name} ({} bytes) as record {key} at version {version}\n blobRef {blob_ref}\n Members fetch it with `pnm rooms file get {key}`.",
sealed.file.size
);
Ok(())
}
fn local_name(untrusted: &str) -> String {
let base = untrusted
.rsplit(['/', '\\'])
.next()
.unwrap_or_default()
.trim_start_matches('.');
let clean = inert(base).replace(['\n', '\t'], " ");
if clean.trim().is_empty() {
"room-file".to_string()
} else {
clean
}
}
pub async fn cmd_rooms_file_get(
client: &VtaClient,
target: RoomTarget<'_>,
signer: RoomSigner<'_>,
key: &str,
out: Option<&std::path::Path>,
) -> Result<(), Box<dyn std::error::Error>> {
use vti_rooms::files::{self, BlobManifest};
let read = present(client, &target, "read").await?;
let host = target.client();
let record = host
.get_record_v2(&read, key, signer.did, signer.key_multibase)
.await?;
let sealed = record.sealed.as_ref().ok_or(
"the record has no sealed body: it is on an `open` room, or it has been retracted",
)?;
let sealed = SealedContent {
ciphertext: sealed.ciphertext.clone(),
nonce: sealed.nonce.clone(),
epoch: u32::try_from(sealed.epoch.get())
.map_err(|_| "the record's epoch is beyond what this client can represent")?,
};
let verified = open_and_verify(
client,
&target,
key,
record.version.get(),
&sealed,
record.author.as_deref(),
)
.await?;
let file = verified
.body()
.file
.clone()
.ok_or("the record carries no file")?;
let named = record
.blobs
.iter()
.flatten()
.any(|b| files::same_digest(&wire_str(b), &file.blob_ref));
if !named {
return Err(
"the host does not name the file's blob on this record; it may have been \
released"
.into(),
);
}
let started = host
.get_blob(&read, &file.blob_ref, signer.did, signer.key_multibase)
.await?;
let blob: BlobManifest = serde_json::from_value(serde_json::to_value(&started.manifest)?)?;
file.check_blob(&blob)?;
let download_id = wire_str(&started.download_id);
let mut chunks = Vec::with_capacity(blob.chunks.chunk_digests.len());
for (i, expected) in blob.chunks.chunk_digests.iter().enumerate() {
let chunk = host
.download_chunk(&download_id, i as u64, signer.did, signer.key_multibase)
.await?;
if !files::same_digest(&files::digest_multibase(&chunk), expected) {
return Err(format!("chunk {i} does not match the manifest").into());
}
chunks.push(chunk);
}
let resp = client
.room_file_key(target.room_id, &file.file_id, Some(file.epoch))
.await?;
let file_key = file_key_bytes(&resp)?;
let plaintext = files::open_file(&file_key, target.room_id, &file, &blob, &chunks)?;
let out = match out {
Some(p) => p.to_path_buf(),
None => std::path::PathBuf::from(local_name(&file.name)),
};
if out.exists() {
return Err(format!("{} already exists; refusing to overwrite it", out.display()).into());
}
let tmp = out.with_extension("partial");
std::fs::write(&tmp, &plaintext)?;
std::fs::rename(&tmp, &out)?;
println!(
"Fetched {} ({} bytes) into {}, written by {}; it matches the digest they signed.",
inert(&file.name).escape_debug(),
plaintext.len(),
out.display(),
verified.body().author
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn minted(authority: Value) -> Value {
json!({
"presentation": {
"membership": "vmc-blob",
"authority": authority,
},
"expiresAt": "2026-09-07T16:00:00Z",
})
}
#[test]
fn a_minted_presentation_becomes_a_session() {
let session = session_from_minted("did:webvh:room", &minted(json!(["leaf", "root"])))
.expect("a well-formed reply rebuilds");
assert_eq!(session.room_id(), "did:webvh:room");
assert_eq!(session.chain_depth(), 2, "leaf first, root last");
}
#[test]
fn a_subject_binding_survives_the_rebuild() {
let mut m = minted(json!(["leaf"]));
m["presentation"]["subjectBinding"] = json!("zk-proof-blob");
let session =
session_from_minted("did:webvh:room", &m).expect("a private presentation rebuilds");
assert_eq!(session.chain_depth(), 1);
}
#[test]
fn an_unreadable_reply_is_refused_rather_than_guessed() {
assert!(
session_from_minted("r", &json!({})).is_err(),
"no presentation"
);
assert!(
session_from_minted("r", &json!({ "presentation": { "authority": ["a"] } })).is_err(),
"no membership"
);
assert!(
session_from_minted("r", &json!({ "presentation": { "membership": "m" } })).is_err(),
"no chain"
);
assert!(
session_from_minted("r", &minted(json!([]))).is_err(),
"an empty chain authorizes nothing and must not be sent"
);
}
#[test]
fn a_malformed_link_does_not_silently_shorten_the_chain() {
let err = session_from_minted("r", &minted(json!(["leaf", 7])))
.expect_err("a non-string link is refused");
assert!(
err.to_string().contains("non-string"),
"the error must name the cause: {err}"
);
}
#[test]
fn untrusted_text_loses_its_control_characters_but_keeps_its_lines() {
assert_eq!(inert("a\x1b[2Jb\nc\td\x07"), "a[2Jb\nc\td");
}
#[test]
fn a_file_name_cannot_leave_the_directory_or_hide() {
assert_eq!(local_name("../../etc/passwd"), "passwd");
assert_eq!(local_name("C:\\Users\\x\\evil.exe"), "evil.exe");
assert_eq!(local_name(".bashrc"), "bashrc");
assert_eq!(local_name("term sheet.pdf"), "term sheet.pdf");
assert_eq!(local_name("../"), "room-file");
assert_eq!(local_name("a\nb.txt"), "a b.txt");
}
#[test]
fn opaque_keys_are_fresh_and_uninformative() {
let (a, b) = (opaque_key(), opaque_key());
assert_ne!(a, b);
assert_eq!(a.len(), 22, "16 random bytes, base64url");
}
#[test]
fn pin_flags_keep_unchanged_distinct_from_unpinned() {
assert_eq!(pinned_from_flags(true, false), Some(true));
assert_eq!(pinned_from_flags(false, true), Some(false));
assert_eq!(
pinned_from_flags(false, false),
None,
"neither flag must leave the pin alone, not clear it"
);
}
}