1use crate::chunks::{chunk_length, chunk_start, CHUNK_SIZE};
4use crate::error::{exact_length, require, Error, Rule};
5use coven_crypto::{CryptoError, FileKey, FILE_CHUNK_TAG_LEN};
6
7pub const FILE_HEADER_LEN: usize = 11;
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub struct FileHeader {
13 size: u64,
14}
15
16impl FileHeader {
17 pub fn new(size: u64) -> Self {
19 Self { size }
20 }
21
22 pub fn decode(bytes: &[u8]) -> Result<Self, Error> {
24 crate::sealed::prefix(bytes, crate::kinds::FILE)?;
25 exact_length(bytes.len(), FILE_HEADER_LEN)?;
26 Ok(Self::new(u64::from_be_bytes(bytes[3..11].try_into().expect("eight bytes"))))
27 }
28
29 pub fn encode(&self) -> [u8; FILE_HEADER_LEN] {
31 let mut bytes = [0; FILE_HEADER_LEN];
32 bytes[..3].copy_from_slice(&crate::FormatVersion::CURRENT.prefix(crate::kinds::FILE));
33 bytes[3..].copy_from_slice(&self.size.to_be_bytes());
34 bytes
35 }
36
37 pub fn size(&self) -> u64 {
39 self.size
40 }
41
42 pub fn chunk_count(&self) -> u64 {
44 crate::chunks::chunk_count(self.size)
45 }
46
47 pub fn encrypted_size(&self) -> Result<u64, Error> {
49 self.chunk_count()
50 .checked_mul(FILE_CHUNK_TAG_LEN as u64)
51 .and_then(|tags| tags.checked_add(self.size))
52 .and_then(|body| body.checked_add(FILE_HEADER_LEN as u64))
53 .ok_or_else(offset_error)
54 }
55
56 pub fn chunk(&self, index: u64) -> Result<FileChunk, Error> {
58 require(index < self.chunk_count(), "file chunk index", Rule::Chunk)?;
59 let offset = index
60 .checked_mul((CHUNK_SIZE + FILE_CHUNK_TAG_LEN) as u64)
61 .and_then(|n| n.checked_add(FILE_HEADER_LEN as u64))
62 .ok_or_else(offset_error)?;
63 let chunk = FileChunk { index, offset, plaintext_length: chunk_length(self.size, index) };
64 offset.checked_add(chunk.stored_length() as u64).ok_or_else(offset_error)?;
65 Ok(chunk)
66 }
67
68 pub fn chunk_at(&self, position: u64) -> Result<FileChunk, Error> {
71 let past_header = position.checked_sub(FILE_HEADER_LEN as u64).ok_or_else(offset_error)?;
72 let chunk = self.chunk(past_header / (CHUNK_SIZE + FILE_CHUNK_TAG_LEN) as u64)?;
73 require(position < chunk.end(), "file chunk position", Rule::Chunk)?;
74 Ok(chunk)
75 }
76
77 pub fn seal_chunk(
79 &self,
80 key: &FileKey,
81 path: &str,
82 index: u64,
83 plaintext: &[u8],
84 ) -> Result<Vec<u8>, FileError> {
85 let chunk = self.chunk(index)?;
86 require(plaintext.len() == chunk.plaintext_length, "file chunk length", Rule::Chunk)?;
87 Ok(key.seal_chunk(path, &self.encode(), index, plaintext))
88 }
89
90 pub fn open_chunk(
92 &self,
93 key: &FileKey,
94 path: &str,
95 index: u64,
96 bytes: &[u8],
97 ) -> Result<Vec<u8>, FileError> {
98 let chunk = self.chunk(index)?;
99 exact_length(bytes.len(), chunk.stored_length())?;
100 Ok(key.open_chunk(path, &self.encode(), index, bytes)?)
101 }
102}
103
104#[derive(Clone, Copy, Debug, PartialEq, Eq)]
106pub struct FileChunk {
107 pub index: u64,
109 pub offset: u64,
111 pub plaintext_length: usize,
113}
114impl FileChunk {
115 pub fn plaintext_start(&self) -> u64 {
117 chunk_start(self.index)
118 }
119
120 pub fn stored_length(&self) -> usize {
122 self.plaintext_length + FILE_CHUNK_TAG_LEN
123 }
124
125 pub fn end(&self) -> u64 {
127 self.offset + self.stored_length() as u64
128 }
129}
130
131fn offset_error() -> Error {
132 Error::Invalid { field: "file offset", rule: Rule::Chunk }
133}
134
135#[derive(Debug)]
137pub enum FileError {
138 Format(Error),
140 Crypto(CryptoError),
142}
143impl From<Error> for FileError {
144 fn from(error: Error) -> Self {
145 Self::Format(error)
146 }
147}
148impl From<CryptoError> for FileError {
149 fn from(error: CryptoError) -> Self {
150 Self::Crypto(error)
151 }
152}
153impl std::fmt::Display for FileError {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 match self {
156 Self::Format(e) => e.fmt(f),
157 Self::Crypto(e) => e.fmt(f),
158 }
159 }
160}
161impl std::error::Error for FileError {
162 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
163 match self {
164 Self::Format(e) => Some(e),
165 Self::Crypto(e) => Some(e),
166 }
167 }
168}
169
170#[cfg(test)]
171#[path = "file_tests.rs"]
172mod tests;