/ core / src / storage.rs
storage.rs
  1  use std::fs;
  2  use std::fs::OpenOptions;
  3  use std::io::Write;
  4  use std::path::Path;
  5  
  6  use automerge::AutoCommit;
  7  
  8  const STORAGE_MAGIC: [u8; 8] = *b"PALUN\0\0\0";
  9  pub const STORAGE_VERSION: u16 = 1;
 10  const HEADER_LEN: usize = 8 + 2 + 2 + 4;
 11  
 12  #[derive(Debug)]
 13  pub enum StorageError {
 14      Io(std::io::Error),
 15      Automerge(automerge::AutomergeError),
 16      InvalidHeader(String),
 17      UnsupportedVersion(u16),
 18  }
 19  
 20  impl std::fmt::Display for StorageError {
 21      fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 22          match self {
 23              StorageError::Io(err) => write!(f, "io error: {err}"),
 24              StorageError::Automerge(err) => write!(f, "automerge error: {err}"),
 25              StorageError::InvalidHeader(message) => write!(f, "invalid header: {message}"),
 26              StorageError::UnsupportedVersion(version) => {
 27                  write!(f, "unsupported storage version {version}")
 28              }
 29          }
 30      }
 31  }
 32  
 33  impl std::error::Error for StorageError {
 34      fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
 35          match self {
 36              StorageError::Io(err) => Some(err),
 37              StorageError::Automerge(err) => Some(err),
 38              _ => None,
 39          }
 40      }
 41  }
 42  
 43  impl From<std::io::Error> for StorageError {
 44      fn from(err: std::io::Error) -> Self {
 45          StorageError::Io(err)
 46      }
 47  }
 48  
 49  impl From<automerge::AutomergeError> for StorageError {
 50      fn from(err: automerge::AutomergeError) -> Self {
 51          StorageError::Automerge(err)
 52      }
 53  }
 54  
 55  /// Save the Automerge document to disk with a versioned header.
 56  pub fn save(doc: &mut AutoCommit, path: impl AsRef<Path>) -> Result<(), StorageError> {
 57      let document = doc.save();
 58      if document.len() > u32::MAX as usize {
 59          return Err(StorageError::InvalidHeader(
 60              "document too large".to_string(),
 61          ));
 62      }
 63      let payload = encode_document(&document);
 64      atomic_write(path.as_ref(), &payload)?;
 65      Ok(())
 66  }
 67  
 68  /// Load an Automerge document from disk.
 69  pub fn load(path: impl AsRef<Path>) -> Result<AutoCommit, StorageError> {
 70      let bytes = fs::read(path)?;
 71      let document = decode_document(&bytes)?;
 72      Ok(AutoCommit::load(document)?)
 73  }
 74  
 75  fn encode_document(document: &[u8]) -> Vec<u8> {
 76      let mut buffer = Vec::with_capacity(HEADER_LEN + document.len());
 77      buffer.extend_from_slice(&STORAGE_MAGIC);
 78      buffer.extend_from_slice(&STORAGE_VERSION.to_le_bytes());
 79      buffer.extend_from_slice(&0u16.to_le_bytes());
 80      buffer.extend_from_slice(&(document.len() as u32).to_le_bytes());
 81      buffer.extend_from_slice(document);
 82      buffer
 83  }
 84  
 85  fn decode_document(bytes: &[u8]) -> Result<&[u8], StorageError> {
 86      if bytes.len() < HEADER_LEN {
 87          return Err(StorageError::InvalidHeader("file too small".to_string()));
 88      }
 89      if bytes[0..8] != STORAGE_MAGIC {
 90          return Err(StorageError::InvalidHeader("magic mismatch".to_string()));
 91      }
 92      let version = u16::from_le_bytes([bytes[8], bytes[9]]);
 93      if version != STORAGE_VERSION {
 94          return Err(StorageError::UnsupportedVersion(version));
 95      }
 96      let length = u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]) as usize;
 97      let payload = &bytes[HEADER_LEN..];
 98      if payload.len() != length {
 99          return Err(StorageError::InvalidHeader("length mismatch".to_string()));
100      }
101      Ok(payload)
102  }
103  
104  fn atomic_write(path: &Path, data: &[u8]) -> Result<(), StorageError> {
105      if let Some(parent) = path.parent() {
106          if !parent.as_os_str().is_empty() {
107              fs::create_dir_all(parent)?;
108          }
109      }
110  
111      let file_name = path
112          .file_name()
113          .and_then(|name| name.to_str())
114          .unwrap_or("workspace");
115      let tmp_name = format!(".{file_name}.{}.tmp", uuid::Uuid::new_v4());
116      let tmp_path = path.with_file_name(tmp_name);
117  
118      {
119          let mut file = OpenOptions::new()
120              .write(true)
121              .create_new(true)
122              .open(&tmp_path)?;
123          file.write_all(data)?;
124          file.sync_all()?;
125      }
126  
127      if let Err(err) = fs::rename(&tmp_path, path) {
128          if err.kind() == std::io::ErrorKind::AlreadyExists {
129              fs::remove_file(path)?;
130              fs::rename(&tmp_path, path)?;
131          } else {
132              return Err(StorageError::Io(err));
133          }
134      }
135  
136      Ok(())
137  }