1use std::fs::{self, File, OpenOptions};
4#[cfg(test)]
5use std::io::Read;
6use std::io::{self, Write};
7use std::path::{Path, PathBuf};
8use std::sync::atomic::{AtomicU64, Ordering};
9
10use rns_core::constants::RESOURCE_AUTO_COMPRESS_MAX_SIZE;
11
12static NEXT_PERSIST_TEMP: AtomicU64 = AtomicU64::new(1);
13static NEXT_RECEIVE_TEMP: AtomicU64 = AtomicU64::new(1);
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub struct ResourceTransferId(pub u64);
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum ResourceTransferError {
22 Source(String),
23 Storage(String),
24 Protocol(String),
25 Cancelled,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum ResourceReceiveMode {
31 Memory { max_bytes: u64 },
43 TemporaryFile {
45 directory: PathBuf,
46 max_bytes: Option<u64>,
47 },
48}
49
50impl Default for ResourceReceiveMode {
51 fn default() -> Self {
52 Self::Memory {
53 max_bytes: RESOURCE_AUTO_COMPRESS_MAX_SIZE as u64,
54 }
55 }
56}
57
58#[derive(Debug)]
63pub struct ReceivedResourceFile {
64 path: Option<PathBuf>,
65 pub original_hash: [u8; 32],
66 pub size: u64,
67 pub metadata: Option<Vec<u8>>,
68}
69
70impl ReceivedResourceFile {
71 pub(crate) fn new(
72 path: PathBuf,
73 original_hash: [u8; 32],
74 size: u64,
75 metadata: Option<Vec<u8>>,
76 ) -> Self {
77 Self {
78 path: Some(path),
79 original_hash,
80 size,
81 metadata,
82 }
83 }
84
85 pub fn path(&self) -> &Path {
87 self.path
88 .as_deref()
89 .expect("persisted resource has no path")
90 }
91
92 pub fn persist(
97 mut self,
98 destination: impl AsRef<Path>,
99 overwrite: bool,
100 ) -> io::Result<PathBuf> {
101 let destination = destination.as_ref();
102 let source = self.path.as_ref().expect("persisted resource has no path");
103 match rename_final(source, destination, overwrite) {
104 Ok(()) => {
105 self.path = None;
106 sync_parent(destination)?;
107 return Ok(destination.to_path_buf());
108 }
109 Err(error) if error.raw_os_error() != Some(libc::EXDEV) => return Err(error),
110 Err(_) => {}
111 }
112
113 let parent = destination.parent().unwrap_or_else(|| Path::new("."));
114 let sequence = NEXT_PERSIST_TEMP.fetch_add(1, Ordering::Relaxed);
115 let stage = parent.join(format!(
116 ".rns-resource-{}-{}.tmp",
117 std::process::id(),
118 sequence
119 ));
120 let mut input = File::open(source)?;
121 let mut output = OpenOptions::new()
122 .write(true)
123 .create_new(true)
124 .open(&stage)?;
125 let copied = io::copy(&mut input, &mut output)?;
126 if copied != self.size {
127 let _ = fs::remove_file(&stage);
128 return Err(io::Error::new(
129 io::ErrorKind::UnexpectedEof,
130 "resource size changed",
131 ));
132 }
133 output.flush()?;
134 output.sync_all()?;
135 drop(output);
136 rename_final(&stage, destination, overwrite).inspect_err(|_| {
137 let _ = fs::remove_file(&stage);
138 })?;
139 sync_parent(destination)?;
140 fs::remove_file(source)?;
141 self.path = None;
142 Ok(destination.to_path_buf())
143 }
144}
145
146#[cfg(target_os = "linux")]
147fn rename_final(source: &Path, destination: &Path, overwrite: bool) -> io::Result<()> {
148 if overwrite {
149 return fs::rename(source, destination);
150 }
151 use std::ffi::CString;
152 use std::os::unix::ffi::OsStrExt;
153 let source = CString::new(source.as_os_str().as_bytes())
154 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "source path contains NUL"))?;
155 let destination = CString::new(destination.as_os_str().as_bytes()).map_err(|_| {
156 io::Error::new(io::ErrorKind::InvalidInput, "destination path contains NUL")
157 })?;
158 let result = unsafe {
159 libc::renameat2(
160 libc::AT_FDCWD,
161 source.as_ptr(),
162 libc::AT_FDCWD,
163 destination.as_ptr(),
164 libc::RENAME_NOREPLACE,
165 )
166 };
167 if result == 0 {
168 Ok(())
169 } else {
170 Err(io::Error::last_os_error())
171 }
172}
173
174#[cfg(not(target_os = "linux"))]
175fn rename_final(source: &Path, destination: &Path, overwrite: bool) -> io::Result<()> {
176 if !overwrite && destination.exists() {
177 return Err(io::Error::new(
178 io::ErrorKind::AlreadyExists,
179 "resource destination already exists",
180 ));
181 }
182 fs::rename(source, destination)
183}
184
185impl Drop for ReceivedResourceFile {
186 fn drop(&mut self) {
187 if let Some(path) = self.path.take() {
188 let _ = fs::remove_file(path);
189 }
190 }
191}
192
193fn sync_parent(path: &Path) -> io::Result<()> {
194 if let Some(parent) = path.parent() {
195 File::open(parent)?.sync_all()?;
196 }
197 Ok(())
198}
199
200pub(crate) fn create_receive_file(
201 directory: &Path,
202 original_hash: &[u8; 32],
203) -> io::Result<(File, PathBuf)> {
204 fs::create_dir_all(directory)?;
205 for _ in 0..100 {
206 let sequence = NEXT_RECEIVE_TEMP.fetch_add(1, Ordering::Relaxed);
207 let path = directory.join(format!(
208 "rns-resource-{:02x}{:02x}{:02x}{:02x}-{}-{}.part",
209 original_hash[0],
210 original_hash[1],
211 original_hash[2],
212 original_hash[3],
213 std::process::id(),
214 sequence
215 ));
216 match OpenOptions::new().write(true).create_new(true).open(&path) {
217 Ok(file) => return Ok((file, path)),
218 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
219 Err(error) => return Err(error),
220 }
221 }
222 Err(io::Error::new(
223 io::ErrorKind::AlreadyExists,
224 "could not allocate unique Resource temporary file",
225 ))
226}
227
228#[cfg(test)]
230fn read_declared(mut reader: impl Read, length: u64) -> io::Result<Vec<u8>> {
231 let length = usize::try_from(length)
232 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "length does not fit usize"))?;
233 let mut data = vec![0; length];
234 reader.read_exact(&mut data)?;
235 let mut trailing = [0u8; 1];
236 if reader.read(&mut trailing)? != 0 {
237 return Err(io::Error::new(
238 io::ErrorKind::InvalidData,
239 "resource source exceeds declared length",
240 ));
241 }
242 Ok(data)
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 #[test]
250 fn managed_file_is_deleted_on_drop_and_can_be_persisted() {
251 let root = tempfile::tempdir().unwrap();
252 let temporary = root.path().join("temporary");
253 fs::write(&temporary, b"resource").unwrap();
254 {
255 let artifact = ReceivedResourceFile::new(temporary.clone(), [7; 32], 8, None);
256 assert_eq!(artifact.path(), temporary);
257 }
258 assert!(!temporary.exists());
259
260 fs::write(&temporary, b"resource").unwrap();
261 let destination = root.path().join("saved");
262 let artifact = ReceivedResourceFile::new(temporary.clone(), [7; 32], 8, None);
263 artifact.persist(&destination, false).unwrap();
264 assert_eq!(fs::read(destination).unwrap(), b"resource");
265 assert!(!temporary.exists());
266 }
267
268 #[test]
269 fn declared_reader_rejects_short_and_long_sources() {
270 assert_eq!(read_declared(&b"abc"[..], 3).unwrap(), b"abc");
271 assert_eq!(
272 read_declared(&b"ab"[..], 3).unwrap_err().kind(),
273 io::ErrorKind::UnexpectedEof
274 );
275 assert_eq!(
276 read_declared(&b"abcd"[..], 3).unwrap_err().kind(),
277 io::ErrorKind::InvalidData
278 );
279 }
280}