coven_format/
file_reference.rs1use crate::{error::Rule, Error};
4use coven_crypto::{FileKey, SecretText};
5use coven_foundation::id_source::{DeviceId, ObjectId};
6use zeroize::Zeroizing;
7
8#[derive(Debug)]
11pub struct FileReference {
12 pub device: DeviceId,
14 pub object: ObjectId,
16 pub key: FileKey,
18}
19
20#[cfg(test)]
21#[path = "file_reference_tests.rs"]
22mod tests;
23
24impl FileReference {
25 #[expect(clippy::map_err_ignore, reason = "a malformed reference is invalid as a whole")]
27 pub fn decode(text: &str) -> Result<Self, Error> {
28 let invalid = || Error::Invalid { field: "file reference", rule: Rule::KeyEncoding };
29 let rest = text.strip_prefix("file ").ok_or_else(invalid)?;
30 let (device, rest) = rest.split_once(' ').ok_or_else(invalid)?;
31 let (object, key) = rest.split_once(' ').ok_or_else(invalid)?;
32 let number = crate::path::canonical_decimal(device).ok_or_else(invalid)?;
33 let uuid = coven_foundation::id_source::canonical_uuid(object).map_err(|_| invalid())?;
34 let mut bytes = Zeroizing::new([0; 32]);
35 hex::decode_to_slice(key, bytes.as_mut()).map_err(|_| invalid())?;
36 if key.bytes().any(|b| b.is_ascii_uppercase()) {
37 return Err(invalid());
38 }
39 Ok(Self {
40 device: DeviceId(number),
41 object: ObjectId(uuid),
42 key: FileKey::from_bytes(*bytes),
43 })
44 }
45
46 pub fn encode(&self) -> SecretText {
48 let mut text = format!("file {} {} ", self.device.0, self.object);
49 let mut key = Zeroizing::new([0; 64]);
50 hex::encode_to_slice(self.key.to_secret_bytes().as_bytes(), key.as_mut())
51 .expect("64 hex digits for a 32-byte key");
52 text.push_str(std::str::from_utf8(key.as_ref()).expect("hex is ASCII"));
53 SecretText::new(text)
54 }
55}