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