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    fs::{self, File},
11    io::{self, Write},
12    path::Path,
13};
14
15use serde::{Serialize, de::DeserializeOwned};
16
17use ic_host_fs::durable::{PublicationMode, WriteOptions};
18
19/// Check the maintained pretty-JSON budget without allocating an encoded record.
20pub(super) fn check_json_size(
21    value: &impl Serialize,
22    max_bytes: u64,
23) -> Result<(), PersistenceError> {
24    let mut writer = ic_host_artifacts::artifact::BoundedWriter::new(io::sink(), max_bytes);
25    let result = serde_json::to_writer_pretty(&mut writer, value);
26    if writer.limit_exceeded() {
27        return Err(PersistenceError::RecordTooLarge { limit: max_bytes });
28    }
29    result?;
30    Ok(())
31}
32
33/// Durably replace a machine record using a sibling temporary and rename.
34///
35/// # Errors
36/// Returns encoding/parent IO failures or structured shared publication failures.
37/// An after-publication failure or lost response requires reconciliation.
38pub fn write_json_durable<T>(path: &Path, value: &T) -> Result<(), PersistenceError>
39where
40    T: Serialize,
41{
42    let bytes = serde_json::to_vec_pretty(value)?;
43    publish_bytes_at_barriers(path, &bytes, PublicationMode::Replace, |_| {})
44}
45
46/// Publish a new machine record without replacing an existing entry.
47///
48/// # Errors
49/// Returns encoding/parent IO failures or structured shared publication failures,
50/// including create-only conflicts and visible output after failed completion.
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    publish_bytes_at_barriers(path, &bytes, PublicationMode::CreateNew, |_| {})
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::NotRegularFile => PersistenceError::Io(io::Error::new(
94            io::ErrorKind::InvalidInput,
95            "record must be a regular file",
96        )),
97        ArtifactError::LimitExceeded { .. } => {
98            PersistenceError::RecordTooLarge { limit: max_bytes }
99        }
100        error => PersistenceError::Io(error.into()),
101    }
102}
103
104#[derive(Clone, Copy, Debug, Eq, PartialEq)]
105pub(crate) enum DurableWriteBarrier {
106    BeforeRename,
107    AfterDirectorySync,
108}
109
110// Keep one shared publication engine for ordinary writes and crash qualification.
111fn publish_bytes_at_barriers(
112    path: &Path,
113    bytes: &[u8],
114    mode: PublicationMode,
115    mut barrier: impl FnMut(DurableWriteBarrier),
116) -> Result<(), PersistenceError> {
117    let parent = path
118        .parent()
119        .filter(|parent| !parent.as_os_str().is_empty())
120        .unwrap_or_else(|| Path::new("."));
121    create_private_parents(parent)?;
122    let options = WriteOptions {
123        mode,
124        permissions: 0o600,
125    };
126    let produce = |file: &mut File| -> io::Result<()> {
127        file.write_all(bytes)?;
128        // Acknowledged pre-publication death must leave synchronized staging.
129        // Host repeats this sync as part of its own identity/publication admission.
130        file.sync_all()?;
131        barrier(DurableWriteBarrier::BeforeRename);
132        Ok(())
133    };
134    #[cfg(unix)]
135    let result = {
136        use std::os::fd::AsFd;
137        let name = path.file_name().ok_or_else(|| {
138            io::Error::new(
139                io::ErrorKind::InvalidInput,
140                "durable write target has no filename",
141            )
142        })?;
143        let directory = File::open(parent)?;
144        ic_host_fs::durable::write_at_with(directory.as_fd(), name, options, produce)
145    };
146    #[cfg(not(unix))]
147    let result = ic_host_fs::durable::write_typed_with(path, options, produce);
148    result.map_err(PersistenceError::Publication)?;
149    // Successful Host completion includes publication and the held-parent sync.
150    barrier(DurableWriteBarrier::AfterDirectorySync);
151    Ok(())
152}
153
154#[cfg(test)]
155pub(crate) fn write_json_durable_at_barriers<T>(
156    path: &Path,
157    value: &T,
158    barrier: impl FnMut(DurableWriteBarrier),
159) -> Result<(), PersistenceError>
160where
161    T: Serialize,
162{
163    let bytes = serde_json::to_vec_pretty(value)?;
164    publish_bytes_at_barriers(path, &bytes, PublicationMode::Replace, barrier)
165}
166
167#[cfg(test)]
168pub(crate) fn create_json_durable_at_barriers<T>(
169    path: &Path,
170    value: &T,
171    mut before_publication: impl FnMut(),
172    mut after_directory_sync: impl FnMut(),
173) -> Result<(), PersistenceError>
174where
175    T: Serialize,
176{
177    let bytes = serde_json::to_vec_pretty(value)?;
178    publish_bytes_at_barriers(
179        path,
180        &bytes,
181        PublicationMode::CreateNew,
182        |barrier| match barrier {
183            DurableWriteBarrier::BeforeRename => before_publication(),
184            DurableWriteBarrier::AfterDirectorySync => after_directory_sync(),
185        },
186    )
187}
188
189// -----------------------------------------------------------------------------
190// Tests
191// -----------------------------------------------------------------------------
192
193#[cfg(test)]
194mod regressions;
195#[cfg(test)]
196mod tests;
197
198fn create_private_parents(parent: &Path) -> io::Result<()> {
199    let mut missing = Vec::new();
200    let mut current = parent;
201    while !current.try_exists()? {
202        missing.push(current.to_path_buf());
203        current = current
204            .parent()
205            .filter(|path| !path.as_os_str().is_empty())
206            .unwrap_or_else(|| Path::new("."));
207    }
208    let mut builder = fs::DirBuilder::new();
209    builder.recursive(true);
210    #[cfg(unix)]
211    {
212        use std::os::unix::fs::DirBuilderExt;
213        builder.mode(0o700);
214    }
215    builder.create(parent)?;
216    // Persist each newly created directory and its link from the existing root.
217    for directory in missing {
218        File::open(&directory)?.sync_all()?;
219        let ancestor = directory
220            .parent()
221            .filter(|path| !path.as_os_str().is_empty())
222            .unwrap_or_else(|| Path::new("."));
223        File::open(ancestor)?.sync_all()?;
224    }
225    Ok(())
226}