Skip to main content

appcore_dnt/
io.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: io.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/08/02 00:04:12 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/08/02 12:07:11 by dnettoRaw
8//      ###########      S: 1.0.1-rc.8
9// =============================================================================
10
11//! DNT filesystem helpers.
12
13use crate::{
14    open_owned, seal, verify, DntCodec, DntError, DntKeyProvider, DntOpenOptions, DntResult,
15    DntSealOptions, OpenedDnt,
16};
17use std::fs::{self, File, OpenOptions};
18use std::io::{Read, Write};
19use std::path::{Path, PathBuf};
20use std::sync::atomic::{AtomicU64, Ordering};
21
22// appcore-norm: allow(global-state) reason: atomic sequence prevents process-local temporary path collisions
23static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
24
25impl DntOpenOptions {
26    /// Returns the largest complete file buffer allowed by these options.
27    ///
28    /// File-based readers require an explicit payload bound so they can reject
29    /// an oversized envelope before allocating its complete contents.
30    pub fn max_envelope_bytes(&self) -> DntResult<u64> {
31        self.max_payload_bytes
32            .ok_or(DntError::PayloadTooLarge)?
33            .checked_add(crate::DNT_MAX_HEADER_BYTES as u64)
34            .and_then(|length| length.checked_add(crate::DNT_MAX_ENCRYPTED_METADATA_BYTES as u64))
35            .and_then(|length| length.checked_add(20))
36            .ok_or(DntError::PayloadTooLarge)
37    }
38}
39
40/// Seals, verifies and atomically replaces one DNT file.
41pub fn write_atomic<P, C>(
42    path: impl AsRef<Path>,
43    payload: &[u8],
44    key_provider: &P,
45    codec: &C,
46    seal_options: DntSealOptions,
47    open_options: &DntOpenOptions,
48) -> DntResult<()>
49where
50    P: DntKeyProvider,
51    C: DntCodec,
52{
53    let envelope = seal(payload, key_provider, codec, seal_options)?;
54    verify(&envelope, key_provider, codec, open_options)?;
55    atomic_replace(path.as_ref(), &envelope)
56}
57
58/// Reads, authenticates and opens one DNT file.
59pub fn read_verified<P, C>(
60    path: impl AsRef<Path>,
61    key_provider: &P,
62    codec: &C,
63    options: &DntOpenOptions,
64) -> DntResult<OpenedDnt>
65where
66    P: DntKeyProvider,
67    C: DntCodec,
68{
69    let bytes = read_bounded(path.as_ref(), options.max_envelope_bytes()?)?;
70    open_owned(bytes, key_provider, codec, options)
71}
72
73fn read_bounded(path: &Path, max_bytes: u64) -> DntResult<Vec<u8>> {
74    reject_symlink(path)?;
75    let file = File::open(path).map_err(|_| DntError::Io)?;
76    let metadata = file.metadata().map_err(|_| DntError::Io)?;
77    if !metadata.is_file() {
78        return Err(DntError::Io);
79    }
80    if metadata.len() > max_bytes {
81        return Err(DntError::PayloadTooLarge);
82    }
83    let capacity = usize::try_from(metadata.len()).map_err(|_| DntError::PayloadTooLarge)?;
84    let read_limit = max_bytes.checked_add(1).ok_or(DntError::PayloadTooLarge)?;
85    let mut bytes = Vec::with_capacity(capacity);
86    file.take(read_limit)
87        .read_to_end(&mut bytes)
88        .map_err(|_| DntError::Io)?;
89    if bytes.len() as u64 > max_bytes {
90        return Err(DntError::PayloadTooLarge);
91    }
92    Ok(bytes)
93}
94
95fn atomic_replace(path: &Path, bytes: &[u8]) -> DntResult<()> {
96    let parent = path.parent().unwrap_or_else(|| Path::new("."));
97    fs::create_dir_all(parent).map_err(|_| DntError::Io)?;
98    reject_symlink(path)?;
99    let temporary = temporary_path(path, parent);
100    let result = write_and_rename(&temporary, path, parent, bytes);
101    if result.is_err() {
102        let _ = fs::remove_file(temporary);
103    }
104    result
105}
106
107fn write_and_rename(
108    temporary: &Path,
109    final_path: &Path,
110    parent: &Path,
111    bytes: &[u8],
112) -> DntResult<()> {
113    let mut file = OpenOptions::new()
114        .create_new(true)
115        .write(true)
116        .open(temporary)
117        .map_err(|_| DntError::Io)?;
118    file.write_all(bytes)
119        .and_then(|_| file.sync_all())
120        .map_err(|_| DntError::Io)?;
121    fs::rename(temporary, final_path).map_err(|_| DntError::Io)?;
122    sync_parent(parent)
123}
124
125fn temporary_path(path: &Path, parent: &Path) -> PathBuf {
126    let name = path
127        .file_name()
128        .and_then(|value| value.to_str())
129        .unwrap_or("appcore-dnt");
130    parent.join(format!(
131        ".{name}.{}-{}.tmp",
132        std::process::id(),
133        TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
134    ))
135}
136
137fn reject_symlink(path: &Path) -> DntResult<()> {
138    match fs::symlink_metadata(path) {
139        Ok(metadata) if metadata.file_type().is_symlink() => Err(DntError::Io),
140        Ok(_) => Ok(()),
141        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
142        Err(_) => Err(DntError::Io),
143    }
144}
145
146#[cfg(unix)]
147fn sync_parent(path: &Path) -> DntResult<()> {
148    fs::File::open(path)
149        .and_then(|directory| directory.sync_all())
150        .map_err(|_| DntError::Io)
151}
152
153#[cfg(not(unix))]
154fn sync_parent(_path: &Path) -> DntResult<()> {
155    Ok(())
156}