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::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    ic_host_fs::durable::prepare_private_parents(parent)?;
122    let options = WriteOptions {
123        mode,
124        permissions: 0o600,
125    };
126    let produce = |file: &mut File| file.write_all(bytes);
127    #[cfg(unix)]
128    let result = {
129        use std::os::fd::AsFd;
130        let name = path.file_name().ok_or_else(|| {
131            io::Error::new(
132                io::ErrorKind::InvalidInput,
133                "durable write target has no filename",
134            )
135        })?;
136        let directory = File::open(parent)?;
137        ic_host_fs::durable::write_at_validated_with(
138            directory.as_fd(),
139            name,
140            options,
141            produce,
142            |()| {
143                barrier(DurableWriteBarrier::BeforeRename);
144                Ok(())
145            },
146        )
147    };
148    #[cfg(not(unix))]
149    let result = ic_host_fs::durable::write_validated_with(path, options, produce, |_, ()| {
150        barrier(DurableWriteBarrier::BeforeRename);
151        Ok(())
152    });
153    result.map_err(|error| {
154        PersistenceError::Publication(super::PublicationError::from_host(error))
155    })?;
156    // Successful Host completion includes publication and the held-parent sync.
157    barrier(DurableWriteBarrier::AfterDirectorySync);
158    Ok(())
159}
160
161#[cfg(test)]
162pub(crate) fn write_json_durable_at_barriers<T>(
163    path: &Path,
164    value: &T,
165    barrier: impl FnMut(DurableWriteBarrier),
166) -> Result<(), PersistenceError>
167where
168    T: Serialize,
169{
170    let bytes = serde_json::to_vec_pretty(value)?;
171    publish_bytes_at_barriers(path, &bytes, PublicationMode::Replace, barrier)
172}
173
174#[cfg(test)]
175pub(crate) fn create_json_durable_at_barriers<T>(
176    path: &Path,
177    value: &T,
178    mut before_publication: impl FnMut(),
179    mut after_directory_sync: impl FnMut(),
180) -> Result<(), PersistenceError>
181where
182    T: Serialize,
183{
184    let bytes = serde_json::to_vec_pretty(value)?;
185    publish_bytes_at_barriers(
186        path,
187        &bytes,
188        PublicationMode::CreateNew,
189        |barrier| match barrier {
190            DurableWriteBarrier::BeforeRename => before_publication(),
191            DurableWriteBarrier::AfterDirectorySync => after_directory_sync(),
192        },
193    )
194}
195
196// -----------------------------------------------------------------------------
197// Tests
198// -----------------------------------------------------------------------------
199
200#[cfg(test)]
201mod regressions;
202#[cfg(test)]
203mod tests;