use std::path::Path;
use crate::atomic::write_atomic;
use crate::crypto::Sealer;
use crate::errors::{HaysonCipherError, HaysonSchemaError};
use super::parse::parse_catalog;
use super::serialize::{catalog_document, serialize_catalog};
use super::types::Catalog;
#[cfg(unix)]
const PERMISSION_BITS: u32 = 0o777;
pub fn assert_catalog_readable(path: &Path, catalog: &Catalog) -> Result<(), HaysonSchemaError> {
parse_catalog(&catalog_document(catalog)).map(|_| ()).map_err(|error| {
HaysonSchemaError(format!(
"refusing to write \"{}\": the catalog it would hold is one this build of hayson cannot read back — {}; nothing on disk was changed, and the declaration in the way has to go before the change can",
path.display(),
error.0,
))
})
}
#[cfg(unix)]
fn permissions_of(path: &Path) -> std::io::Result<Option<u32>> {
use std::os::unix::fs::PermissionsExt;
match std::fs::metadata(path) {
Ok(metadata) => Ok(Some(metadata.permissions().mode() & PERMISSION_BITS)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(error),
}
}
#[cfg(not(unix))]
fn permissions_of(_path: &Path) -> std::io::Result<Option<u32>> {
Ok(None)
}
pub fn write_catalog(path: &Path, catalog: &Catalog) -> Result<(), Box<dyn std::error::Error>> {
write_catalog_sealed(path, catalog, None)
}
pub fn write_catalog_sealed(
path: &Path,
catalog: &Catalog,
sealer: Option<&Sealer>,
) -> Result<(), Box<dyn std::error::Error>> {
assert_catalog_readable(path, catalog)?;
let mode = permissions_of(path)?;
let contents = serialize_catalog(catalog);
match sealer {
Some(sealer) => write_atomic(path, &sealer.seal_blob(CATALOG_SCOPE, contents.as_bytes()), mode)?,
None => write_atomic(path, contents.as_bytes(), mode)?,
}
Ok(())
}
pub const CATALOG_SCOPE: &str = "catalog";
pub fn read_catalog_text(path: &Path, sealer: Option<&Sealer>) -> Result<Option<String>, ReadCatalogError> {
let bytes = match std::fs::read(path) {
Ok(bytes) => bytes,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(ReadCatalogError::Io(error)),
};
let sealed = crate::crypto::is_sealed_blob(&bytes);
let plain = match (sealed, sealer) {
(true, Some(sealer)) => sealer
.open_blob(CATALOG_SCOPE, &bytes)
.map_err(ReadCatalogError::Cipher)?,
(true, None) => {
return Err(ReadCatalogError::Encrypted(
HaysonCipherError::new("", crate::crypto::STORAGE_FIELD, format!(
"\"{}\" is encrypted — open the database with its encryption key", path.display()
)),
))
}
(false, Some(_)) => {
return Err(ReadCatalogError::NotEncrypted(
HaysonCipherError::new("", crate::crypto::STORAGE_FIELD, format!(
"\"{}\" is not encrypted, but an encryption key was given — convert the database first, or open it without the key", path.display()
)),
))
}
(false, None) => bytes,
};
String::from_utf8(plain)
.map(Some)
.map_err(|error| ReadCatalogError::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, error)))
}
#[derive(Debug)]
pub enum ReadCatalogError {
Io(std::io::Error),
Cipher(HaysonCipherError),
Encrypted(HaysonCipherError),
NotEncrypted(HaysonCipherError),
}
impl ReadCatalogError {
pub fn problem_code(&self) -> &'static str {
match self {
ReadCatalogError::Io(_) => "catalog-unreadable",
ReadCatalogError::Cipher(_) => "catalog-key-rejected",
ReadCatalogError::Encrypted(_) => "catalog-encrypted",
ReadCatalogError::NotEncrypted(_) => "catalog-not-encrypted",
}
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use std::fs;
use super::*;
use crate::catalog::build::empty_catalog;
use crate::catalog::types::CatalogLayout;
#[test]
fn writes_the_catalog_atomically() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("hayson.json");
let catalog = empty_catalog(CatalogLayout::Flat, 1_000);
write_catalog(&path, &catalog).unwrap();
let on_disk = fs::read_to_string(&path).unwrap();
assert!(on_disk.contains("\"catalogVersion\": 2"));
assert!(!path.with_extension("json.tmp").exists());
let reparsed = parse_catalog(&serde_json::from_str(&on_disk).unwrap()).unwrap();
assert_eq!(reparsed.tables, BTreeMap::new());
}
#[test]
fn refuses_before_touching_disk_when_the_catalog_cannot_be_read_back() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("hayson.json");
fs::write(&path, "original").unwrap();
let mut catalog = empty_catalog(CatalogLayout::Flat, 1_000);
catalog.catalog_version = 3;
assert!(write_catalog(&path, &catalog).is_err());
assert_eq!(fs::read_to_string(&path).unwrap(), "original");
}
}