Skip to main content

vial_shared/
lib.rs

1use chrono::NaiveDateTime;
2use serde::{Deserialize, Serialize};
3use std::sync::Arc;
4
5#[cfg(not(target_arch = "wasm32"))]
6use std::path::Path;
7
8/// Received via HTTPS from the server then decrypted to Payload struct
9#[derive(Deserialize, Serialize, Clone, Debug)]
10pub struct EncryptedPayload {
11    pub payload: Vec<u8>,
12}
13
14/// Gets returned by the server when creating a new secret
15#[derive(Deserialize, Serialize, Clone, Debug)]
16pub struct SecretId(pub String);
17
18/// Sent to the server when creating a new secret
19#[derive(Deserialize, Serialize, Clone, Debug)]
20pub struct CreateSecretRequest {
21    pub ciphertext: Vec<u8>,
22    pub expires_at: Option<NaiveDateTime>,
23    pub max_views: Option<i32>,
24}
25
26/// Once the bytes gets decrypted, should be deserialized to this struct.
27/// Similarly, before encrypting, this struct is serialized to bytes.
28///
29/// After deserializing, the bytes can be deserialized to `FullSecretV1` struct.
30/// Similarly, payload is generated from the serialized `FullSecretV1` struct.
31#[derive(Deserialize, Serialize, Clone, Debug)]
32pub struct Payload {
33    pub version: u8,
34    pub payload: Vec<u8>,
35}
36
37impl Payload {
38    pub fn new(full_secret: &FullSecretV1) -> Result<Self, postcard::Error> {
39        let payload = full_secret.to_bytes()?;
40        Ok(Self {
41            payload,
42            version: 1,
43        })
44    }
45    pub fn to_bytes(&self) -> Result<Vec<u8>, postcard::Error> {
46        postcard::to_stdvec(self)
47    }
48
49    pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, postcard::Error> {
50        postcard::from_bytes(&bytes)
51    }
52
53    pub fn to_full_secret(&self) -> Result<FullSecretV1, postcard::Error> {
54        let secret = FullSecretV1::from_bytes(&self.payload)?;
55        Ok(secret)
56    }
57}
58
59#[derive(Deserialize, Serialize, Clone, Debug)]
60pub struct FullSecretV1 {
61    pub text: String,
62    pub files: Vec<SecretFileV1>,
63}
64
65#[derive(Deserialize, Serialize, Clone, Debug)]
66pub struct SecretFileV1 {
67    filename: String,
68    content: Vec<u8>,
69}
70
71impl SecretFileV1 {
72    #[cfg(not(target_arch = "wasm32"))]
73    pub fn new(filename: &str, content: Vec<u8>) -> Result<Self, Box<dyn std::error::Error>> {
74        let safe_filename = sanitize_filename(filename)?;
75
76        if content.is_empty() {
77            return Err("File is empty".into());
78        }
79
80        Ok(Self {
81            filename: safe_filename,
82            content,
83        })
84    }
85
86    #[must_use]
87    pub fn filename(&self) -> &str {
88        &self.filename
89    }
90
91    #[must_use]
92    pub fn content(&self) -> &[u8] {
93        &self.content
94    }
95
96    #[cfg(not(target_arch = "wasm32"))]
97    pub fn write(&self, path: &Path) -> Result<(), std::io::Error> {
98        std::fs::write(path, self.content())?;
99        Ok(())
100    }
101}
102
103impl FullSecretV1 {
104    fn to_bytes(&self) -> Result<Vec<u8>, postcard::Error> {
105        postcard::to_stdvec(self)
106    }
107
108    fn from_bytes(bytes: &[u8]) -> Result<Self, postcard::Error> {
109        postcard::from_bytes(bytes)
110    }
111
112    pub fn to_payload(&self) -> Result<Payload, postcard::Error> {
113        let payload = self.to_bytes()?;
114        Ok(Payload {
115            version: 1,
116            payload,
117        })
118    }
119
120    #[must_use]
121    pub fn total_files(&self) -> usize {
122        self.files.len()
123    }
124
125    #[must_use]
126    pub fn into_shared(self) -> FullSecret {
127        FullSecret {
128            text: Arc::new(self.text),
129            files: self
130                .files
131                .into_iter()
132                .map(|f| SecretFile {
133                    filename: f.filename,
134                    content: Arc::new(f.content),
135                })
136                .collect(),
137        }
138    }
139}
140
141#[derive(Clone, Debug)]
142pub struct FullSecret {
143    pub text: Arc<String>,
144    pub files: Vec<SecretFile>,
145}
146
147#[derive(Clone, Debug)]
148pub struct SecretFile {
149    pub filename: String,
150    pub content: Arc<Vec<u8>>,
151}
152
153impl FullSecret {
154    #[must_use]
155    pub fn total_files(&self) -> usize {
156        self.files.len()
157    }
158}
159
160impl SecretFile {
161    #[must_use]
162    pub fn filename(&self) -> &str {
163        &self.filename
164    }
165
166    #[must_use]
167    pub fn content(&self) -> &[u8] {
168        &self.content
169    }
170
171    #[cfg(not(target_arch = "wasm32"))]
172    pub fn write(&self, path: &Path) -> Result<(), std::io::Error> {
173        std::fs::write(path, self.content())?;
174        Ok(())
175    }
176}
177
178/// Helper function to sanitize a filename from potentially unsafe characters
179#[cfg(not(target_arch = "wasm32"))]
180pub fn sanitize_filename(name: &str) -> Result<String, &'static str> {
181    let path = Path::new(name);
182
183    if path.components().count() != 1 {
184        return Err("Invalid filename (must not contain path separators)");
185    }
186
187    let file_name = path.file_name().ok_or("Invalid filename")?;
188
189    let file_name = file_name.to_str().ok_or("Filename is not valid UTF-8")?;
190
191    if file_name.is_empty() || file_name == "." || file_name == ".." || !path.is_file() {
192        return Err("Invalid filename");
193    }
194
195    Ok(file_name.to_string())
196}