Skip to main content

ic_backup/ops/persistence/json/
mod.rs

1//! Module: `persistence::json`
2//!
3//! Responsibility: read and durably create or replace JSON persistence documents.
4//! Does not own: document validation, layout paths, or integrity checks.
5//! Boundary: provides explicit create-only and replace filesystem primitives.
6
7use crate::ops::persistence::PersistenceError;
8
9use std::{
10    ffi::OsString,
11    fs::{self, File, OpenOptions},
12    io::{self, Write},
13    path::{Path, PathBuf},
14    sync::atomic::{AtomicU64, Ordering},
15};
16
17use serde::{Serialize, de::DeserializeOwned};
18
19static TEMP_SEQUENCE: AtomicU64 = AtomicU64::new(0);
20
21/// Check the maintained pretty-JSON budget without allocating an encoded record.
22pub(super) fn check_json_size(
23    value: &impl Serialize,
24    max_bytes: u64,
25) -> Result<(), PersistenceError> {
26    let mut writer = ic_host_artifacts::artifact::BoundedWriter::new(io::sink(), max_bytes);
27    let result = serde_json::to_writer_pretty(&mut writer, value);
28    if writer.limit_exceeded() {
29        return Err(PersistenceError::RecordTooLarge { limit: max_bytes });
30    }
31    result?;
32    Ok(())
33}
34
35/// Durably replace a machine record using a sibling temporary and rename.
36///
37/// # Errors
38/// Returns encoding or IO failures; a lost post-rename response requires reconciliation.
39pub fn write_json_durable<T>(path: &Path, value: &T) -> Result<(), PersistenceError>
40where
41    T: Serialize,
42{
43    let bytes = serde_json::to_vec_pretty(value)?;
44    replace_bytes_at_barriers(path, &bytes, |_| {}).map_err(PersistenceError::from)
45}
46
47/// Publish a new machine record without replacing an existing entry.
48///
49/// # Errors
50/// Returns encoding, existing-destination or IO failures.
51pub fn create_json_durable<T>(path: &Path, value: &T) -> Result<(), PersistenceError>
52where
53    T: Serialize,
54{
55    let bytes = serde_json::to_vec_pretty(value)?;
56    create_bytes_at_barriers(path, &bytes, || {}, || {}).map_err(PersistenceError::from)
57}
58
59/// Read one regular no-follow JSON file within an explicit byte limit.
60///
61/// Reuses the shared bounded regular-file reader; caller-selected parents and
62/// concurrent byte custody remain local. Only the final symlink is rejected.
63///
64/// # Errors
65/// Rejects unsafe files, excessive bytes, invalid JSON and filesystem failures.
66pub fn read_json<T>(path: &Path, max_bytes: u64) -> Result<T, PersistenceError>
67where
68    T: DeserializeOwned,
69{
70    #[cfg(unix)]
71    {
72        let bytes = ic_host_fs::read::read_file_no_follow(
73            path,
74            usize::try_from(max_bytes).unwrap_or(usize::MAX),
75        )
76        .map_err(|error| record_read_error(error, max_bytes))?;
77        Ok(serde_json::from_slice(&bytes)?)
78    }
79    #[cfg(not(unix))]
80    {
81        let _ = (path, max_bytes);
82        Err(io::Error::from(io::ErrorKind::Unsupported).into())
83    }
84}
85
86#[cfg(unix)]
87fn record_read_error(
88    error: ic_host_artifacts::artifact::ArtifactError,
89    max_bytes: u64,
90) -> PersistenceError {
91    use ic_host_artifacts::artifact::ArtifactError;
92    match error {
93        ArtifactError::Io(error) => PersistenceError::Io(error),
94        ArtifactError::NotRegularFile => PersistenceError::Io(io::Error::new(
95            io::ErrorKind::InvalidInput,
96            "record must be a regular file",
97        )),
98        ArtifactError::LimitExceeded { .. } => {
99            PersistenceError::RecordTooLarge { limit: max_bytes }
100        }
101        error => PersistenceError::Io(io::Error::other(error)),
102    }
103}
104
105fn create_bytes_at_barriers(
106    path: &Path,
107    bytes: &[u8],
108    mut before_publication: impl FnMut(),
109    mut after_directory_sync: impl FnMut(),
110) -> io::Result<()> {
111    let parent = path
112        .parent()
113        .filter(|parent| !parent.as_os_str().is_empty())
114        .unwrap_or_else(|| Path::new("."));
115    create_private_parents(parent)?;
116
117    let (temp_path, mut temp_file) = create_sibling_temp(path, parent)?;
118    if let Err(error) = temp_file
119        .write_all(bytes)
120        .and_then(|()| temp_file.sync_all())
121    {
122        drop(temp_file);
123        let _ = fs::remove_file(&temp_path);
124        return Err(error);
125    }
126    drop(temp_file);
127    before_publication();
128
129    if let Err(error) = fs::hard_link(&temp_path, path) {
130        let _ = fs::remove_file(&temp_path);
131        return Err(error);
132    }
133    fs::remove_file(&temp_path)?;
134    File::open(parent)?.sync_all()?;
135    after_directory_sync();
136    Ok(())
137}
138
139#[derive(Clone, Copy, Debug, Eq, PartialEq)]
140pub(crate) enum DurableWriteBarrier {
141    BeforeRename,
142    AfterDirectorySync,
143}
144
145fn replace_bytes_at_barriers(
146    path: &Path,
147    bytes: &[u8],
148    mut barrier: impl FnMut(DurableWriteBarrier),
149) -> io::Result<()> {
150    let parent = path
151        .parent()
152        .filter(|parent| !parent.as_os_str().is_empty())
153        .unwrap_or_else(|| Path::new("."));
154    create_private_parents(parent)?;
155
156    let (temp_path, mut temp_file) = create_sibling_temp(path, parent)?;
157    if let Err(error) = temp_file
158        .write_all(bytes)
159        .and_then(|()| temp_file.sync_all())
160    {
161        drop(temp_file);
162        let _ = fs::remove_file(&temp_path);
163        return Err(error);
164    }
165    drop(temp_file);
166    barrier(DurableWriteBarrier::BeforeRename);
167
168    if let Err(error) = fs::rename(&temp_path, path) {
169        let _ = fs::remove_file(&temp_path);
170        return Err(error);
171    }
172
173    File::open(parent)?.sync_all()?;
174    barrier(DurableWriteBarrier::AfterDirectorySync);
175    Ok(())
176}
177
178#[cfg(test)]
179pub(crate) fn write_json_durable_at_barriers<T>(
180    path: &Path,
181    value: &T,
182    barrier: impl FnMut(DurableWriteBarrier),
183) -> Result<(), PersistenceError>
184where
185    T: Serialize,
186{
187    let bytes = serde_json::to_vec_pretty(value)?;
188    replace_bytes_at_barriers(path, &bytes, barrier).map_err(PersistenceError::from)
189}
190
191#[cfg(test)]
192pub(crate) fn create_json_durable_at_barriers<T>(
193    path: &Path,
194    value: &T,
195    before_publication: impl FnMut(),
196    after_directory_sync: impl FnMut(),
197) -> Result<(), PersistenceError>
198where
199    T: Serialize,
200{
201    let bytes = serde_json::to_vec_pretty(value)?;
202    create_bytes_at_barriers(path, &bytes, before_publication, after_directory_sync)
203        .map_err(PersistenceError::from)
204}
205
206fn create_sibling_temp(path: &Path, parent: &Path) -> io::Result<(PathBuf, File)> {
207    let file_name = path.file_name().ok_or_else(|| {
208        io::Error::new(
209            io::ErrorKind::InvalidInput,
210            format!("durable write target has no file name: {}", path.display()),
211        )
212    })?;
213
214    for _ in 0..64 {
215        let sequence = TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed);
216        let mut temp_name = OsString::from(".");
217        temp_name.push(file_name);
218        temp_name.push(format!(".ic-backup-tmp-{}-{sequence}", std::process::id()));
219        let temp_path = parent.join(temp_name);
220        let mut options = OpenOptions::new();
221        options.write(true).create_new(true);
222        #[cfg(unix)]
223        {
224            use std::os::unix::fs::OpenOptionsExt;
225            options.mode(0o600);
226        }
227        match options.open(&temp_path) {
228            Ok(file) => return Ok((temp_path, file)),
229            Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {}
230            Err(error) => return Err(error),
231        }
232    }
233
234    Err(io::Error::new(
235        io::ErrorKind::AlreadyExists,
236        format!(
237            "could not allocate a unique sibling temporary file for {}",
238            path.display()
239        ),
240    ))
241}
242
243// -----------------------------------------------------------------------------
244// Tests
245// -----------------------------------------------------------------------------
246
247#[cfg(test)]
248mod regressions;
249#[cfg(test)]
250mod tests;
251
252fn create_private_parents(parent: &Path) -> io::Result<()> {
253    let mut missing = Vec::new();
254    let mut current = parent;
255    while !current.try_exists()? {
256        missing.push(current.to_path_buf());
257        current = current
258            .parent()
259            .filter(|path| !path.as_os_str().is_empty())
260            .unwrap_or_else(|| Path::new("."));
261    }
262    let mut builder = fs::DirBuilder::new();
263    builder.recursive(true);
264    #[cfg(unix)]
265    {
266        use std::os::unix::fs::DirBuilderExt;
267        builder.mode(0o700);
268    }
269    builder.create(parent)?;
270    // Persist each newly created directory and its link from the existing root.
271    for directory in missing {
272        File::open(&directory)?.sync_all()?;
273        let ancestor = directory
274            .parent()
275            .filter(|path| !path.as_os_str().is_empty())
276            .unwrap_or_else(|| Path::new("."));
277        File::open(ancestor)?.sync_all()?;
278    }
279    Ok(())
280}