Skip to main content

urma_core/
format.rs

1use crate::error::{Error, bail, ensure};
2use crate::topics::Topics;
3use bitcoin::{Txid, hashes::Hash};
4
5pub struct Urma;
6
7impl Urma {
8    pub const MAGIC: [u8; 4] = *b"URMA";
9    pub const VERSION: u8 = 0;
10    pub const PREFIX_BYTES: usize = 8;
11    pub const CHUNK_BYTES: usize = 32_768;
12    pub const PRIVATE_HEADER_BYTES: usize = 64;
13    pub const METADATA_BYTES: usize = 48;
14    pub const IV_BYTES: usize = 16;
15    pub const BODY_OFFSET: usize = 80;
16    pub const BODY_BYTES: usize = 32_816;
17    pub const MAC_BYTES: usize = 32;
18    pub const MAC_OFFSET: usize = 32_896;
19    pub const PRIVATE_RECORD_BYTES: usize = 32_928;
20    pub const MAX_PRIVATE_BYTES: u64 = 140_737_488_322_560;
21    pub const MAX_PUBLIC_BYTES: usize = 32_768;
22    pub const PROFILE_IDENTIFIER_BYTES: usize = 8;
23    pub const AVATAR_BYTES: usize = 512;
24    pub const CONTAINER_HEADER_BYTES: usize = 12;
25    pub const CONTENT_DOMAIN: &'static [u8] = b"URMA/V0/private/content";
26    pub const AUTHENTICATION_DOMAIN: &'static [u8] = b"URMA/V0/private/authentication";
27    pub const DISCOVERY_DOMAIN: &'static [u8] = b"URMA/V0/private/discovery";
28}
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31#[repr(u8)]
32pub enum RecordKind {
33    Private = 1,
34    Post = 2,
35    Container = 3,
36    Reply = 4,
37    Profile = 5,
38    Avatar = 6,
39    DataPart = 7,
40    LeafManifest = 8,
41    RootManifest = 9,
42    WirePost = 10,
43    WireReply = 11,
44    ProfileRecord = 12,
45}
46
47impl RecordKind {
48    pub fn byte(self) -> u8 {
49        match self {
50            Self::Private => 1,
51            Self::Post => 2,
52            Self::Container => 3,
53            Self::Reply => 4,
54            Self::Profile => 5,
55            Self::Avatar => 6,
56            Self::DataPart => 7,
57            Self::LeafManifest => 8,
58            Self::RootManifest => 9,
59            Self::WirePost => 10,
60            Self::WireReply => 11,
61            Self::ProfileRecord => 12,
62        }
63    }
64
65    pub fn prefix(self) -> [u8; 8] {
66        [b'U', b'R', b'M', b'A', Urma::VERSION, self.byte(), 0, 0]
67    }
68
69    pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
70        ensure!(bytes.len() >= Urma::PREFIX_BYTES, "truncated URMA prefix");
71        if bytes[..4] != Urma::MAGIC {
72            return Err(Error::Unsupported("unsupported record marker".into()));
73        }
74        if bytes[4] != Urma::VERSION {
75            return Err(Error::Unsupported("unsupported URMA version".into()));
76        }
77        if bytes[6..8] != [0, 0] {
78            return Err(Error::Unsupported("unsupported URMA flags".into()));
79        }
80        match bytes[5] {
81            1 => Ok(Self::Private),
82            2 => Ok(Self::Post),
83            3 => Ok(Self::Container),
84            4 => Ok(Self::Reply),
85            5 => Ok(Self::Profile),
86            6 => Ok(Self::Avatar),
87            7 => Ok(Self::DataPart),
88            8 => Ok(Self::LeafManifest),
89            9 => Ok(Self::RootManifest),
90            10 => Ok(Self::WirePost),
91            11 => Ok(Self::WireReply),
92            12 => Ok(Self::ProfileRecord),
93            kind => Err(Error::Unsupported(format!("unsupported URMA kind {kind}"))),
94        }
95    }
96}
97
98#[derive(Clone, Copy, Debug, PartialEq, Eq)]
99pub enum ContentType {
100    Opaque,
101    Text,
102    Image,
103    Audio,
104    Video,
105}
106
107impl ContentType {
108    pub fn code(self) -> u32 {
109        match self {
110            Self::Opaque => 0,
111            Self::Text => 1,
112            Self::Image => 2,
113            Self::Audio => 3,
114            Self::Video => 4,
115        }
116    }
117
118    pub fn from_code(code: u32) -> Result<Self, Error> {
119        match code {
120            0 => Ok(Self::Opaque),
121            1 => Ok(Self::Text),
122            2 => Ok(Self::Image),
123            3 => Ok(Self::Audio),
124            4 => Ok(Self::Video),
125            value => Err(Error::Unsupported(format!(
126                "unsupported private content type {value}"
127            ))),
128        }
129    }
130}
131
132#[derive(Clone, Debug, PartialEq, Eq)]
133pub enum PublicRecord {
134    Post(String),
135    Reply {
136        target: Txid,
137        text: String,
138    },
139    Profile(String),
140    Avatar(Box<[u8; 512]>),
141    WirePost {
142        topics: Topics,
143        text: String,
144    },
145    WireReply {
146        target: Txid,
147        topics: Topics,
148        text: String,
149    },
150    ProfileRecord {
151        profile: [u8; 8],
152        payload: Vec<u8>,
153    },
154}
155
156impl PublicRecord {
157    pub fn kind(&self) -> RecordKind {
158        match self {
159            Self::Post(..) => RecordKind::Post,
160            Self::Reply { .. } => RecordKind::Reply,
161            Self::Profile(..) => RecordKind::Profile,
162            Self::Avatar(..) => RecordKind::Avatar,
163            Self::WirePost { .. } => RecordKind::WirePost,
164            Self::WireReply { .. } => RecordKind::WireReply,
165            Self::ProfileRecord { .. } => RecordKind::ProfileRecord,
166        }
167    }
168
169    pub fn encode(&self) -> Result<Vec<u8>, Error> {
170        if let Self::WirePost { topics, text } | Self::WireReply { topics, text, .. } = self {
171            let mut bytes = self.kind().prefix().to_vec();
172            if let Self::WireReply { target, .. } = self {
173                bytes.extend_from_slice(target.as_byte_array());
174            }
175            bytes.extend_from_slice(&topics.encode_body(text)?);
176            ensure!(
177                bytes.len() <= Urma::MAX_PUBLIC_BYTES,
178                "public record exceeds 32 KiB"
179            );
180            return Ok(bytes);
181        }
182        let body_size = match self {
183            Self::Post(text) | Self::Profile(text) => text.len(),
184            Self::Reply { text, .. } => text
185                .len()
186                .checked_add(32)
187                .ok_or_else(|| Error::Invalid("reply size overflow".into()))?,
188            Self::Avatar(pixels) => pixels.len(),
189            Self::ProfileRecord { payload, .. } => payload
190                .len()
191                .checked_add(Urma::PROFILE_IDENTIFIER_BYTES)
192                .ok_or_else(|| Error::Invalid("profile record size overflow".into()))?,
193            Self::WirePost { .. } | Self::WireReply { .. } => {
194                bail!("structured record handled separately")
195            }
196        };
197        ensure!(
198            body_size <= Urma::MAX_PUBLIC_BYTES - Urma::PREFIX_BYTES,
199            "public record exceeds 32 KiB"
200        );
201        let mut bytes = Vec::with_capacity(Urma::PREFIX_BYTES + body_size);
202        bytes.extend_from_slice(&self.kind().prefix());
203        match self {
204            Self::Post(text) | Self::Profile(text) => bytes.extend_from_slice(text.as_bytes()),
205            Self::Reply { target, text } => {
206                bytes.extend_from_slice(target.as_byte_array());
207                bytes.extend_from_slice(text.as_bytes());
208            }
209            Self::Avatar(pixels) => bytes.extend_from_slice(pixels.as_slice()),
210            Self::ProfileRecord { profile, payload } => {
211                bytes.extend_from_slice(profile);
212                bytes.extend_from_slice(payload);
213            }
214            Self::WirePost { .. } | Self::WireReply { .. } => {
215                bail!("structured record handled separately")
216            }
217        }
218        ensure!(
219            bytes.len() <= Urma::MAX_PUBLIC_BYTES,
220            "public record exceeds 32 KiB"
221        );
222        Ok(bytes)
223    }
224
225    pub fn decode(bytes: &[u8]) -> Result<Self, Error> {
226        let kind = RecordKind::parse(bytes)?;
227        ensure!(
228            bytes.len() <= Urma::MAX_PUBLIC_BYTES,
229            "public record exceeds 32 KiB"
230        );
231        let body = &bytes[Urma::PREFIX_BYTES..];
232        match kind {
233            RecordKind::Post => Ok(Self::Post(std::str::from_utf8(body)?.to_owned())),
234            RecordKind::Profile => Ok(Self::Profile(std::str::from_utf8(body)?.to_owned())),
235            RecordKind::Reply => {
236                ensure!(body.len() >= 32, "truncated reply target");
237                Ok(Self::Reply {
238                    target: Txid::from_byte_array(body[..32].try_into()?),
239                    text: std::str::from_utf8(&body[32..])?.to_owned(),
240                })
241            }
242            RecordKind::Avatar => Ok(Self::Avatar(Box::new(body.try_into()?))),
243            RecordKind::ProfileRecord => {
244                ensure!(
245                    body.len() >= Urma::PROFILE_IDENTIFIER_BYTES,
246                    "truncated profile identifier"
247                );
248                Ok(Self::ProfileRecord {
249                    profile: body[..Urma::PROFILE_IDENTIFIER_BYTES].try_into()?,
250                    payload: body[Urma::PROFILE_IDENTIFIER_BYTES..].to_vec(),
251                })
252            }
253            RecordKind::WirePost => {
254                let (topics, text) = Topics::decode_body(body)?;
255                Ok(Self::WirePost { topics, text })
256            }
257            RecordKind::WireReply => {
258                ensure!(body.len() >= 32, "truncated reply target");
259                let target = Txid::from_byte_array(body[..32].try_into()?);
260                let (topics, text) = Topics::decode_body(&body[32..])?;
261                Ok(Self::WireReply {
262                    target,
263                    topics,
264                    text,
265                })
266            }
267            RecordKind::Private
268            | RecordKind::Container
269            | RecordKind::DataPart
270            | RecordKind::LeafManifest
271            | RecordKind::RootManifest => bail!("not an atomic public record"),
272        }
273    }
274}
275
276pub fn chunk_count(length: u64) -> Result<u32, Error> {
277    ensure!(
278        (1..=Urma::MAX_PRIVATE_BYTES).contains(&length),
279        "private length outside representational bounds"
280    );
281    Ok(u32::try_from(
282        length.div_ceil(u64::try_from(Urma::CHUNK_BYTES)?),
283    )?)
284}