1use chrono::NaiveDateTime;
2use serde::{Deserialize, Serialize};
3use std::sync::Arc;
4
5#[cfg(not(target_arch = "wasm32"))]
6use std::path::Path;
7
8#[derive(Deserialize, Serialize, Clone, Debug)]
10pub struct EncryptedPayload {
11 pub payload: Vec<u8>,
12}
13
14#[derive(Deserialize, Serialize, Clone, Debug)]
16pub struct SecretId(pub String);
17
18#[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#[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#[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}