prns-core 0.3.4

Pure Reticulum engine and wire contract for Personal Reticulum
mod document;
mod file;
mod manual_configuration;
#[cfg(test)]
mod tests;

use std::collections::BTreeMap;
use std::fs;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};

use crate::interface_discovery::{DiscoveredInterfaceId, DiscoveryCatalog, DiscoveryRecord};

use self::document::{ArchiveDocument, ArchiveDocumentRef, ArchivedRecord};
use self::file::replace_archive_file;

pub use self::file::{
    ArchiveFileOperation, ArchiveRecordError, DiscoveryArchiveError, HexDecodeError,
};
pub use self::manual_configuration::manual_configuration as discovered_interface_configuration;

pub const DISCOVERED_INTERFACES_FILE: &str = "discovered_interfaces.json";
const FORMAT_VERSION: u32 = 1;
const MAX_ARCHIVE_BYTES: usize = 8 * 1024 * 1024;
const CONFIGURATION_NOTE: &str =
    "Copy a configuration_entry beneath [interfaces] in the Reticulum config, then fill any blank hardware-specific fields.";

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiscoveryArchiveFileState {
    Missing,
    Present,
}

pub struct LoadedDiscoveryArchive {
    pub archive: DiscoveryArchive,
    pub catalog: DiscoveryCatalog,
    pub file_state: DiscoveryArchiveFileState,
}

pub struct DiscoveryArchive {
    path: PathBuf,
    interfaces: BTreeMap<String, ArchivedRecord>,
}

pub struct DiscoveryArchiveRecord {
    id: String,
    operation: DiscoveryArchiveOperation,
}

enum DiscoveryArchiveOperation {
    Upsert(Box<ArchivedRecord>),
    Remove,
}

impl From<&DiscoveryRecord> for DiscoveryArchiveRecord {
    fn from(record: &DiscoveryRecord) -> Self {
        Self {
            id: file::encode_hex(record.id().as_bytes()),
            operation: DiscoveryArchiveOperation::Upsert(Box::new(ArchivedRecord::from_record(
                record,
            ))),
        }
    }
}

impl DiscoveryArchiveRecord {
    pub fn remove(id: DiscoveredInterfaceId) -> Self {
        Self {
            id: file::encode_hex(id.as_bytes()),
            operation: DiscoveryArchiveOperation::Remove,
        }
    }
}

impl DiscoveryArchive {
    pub fn load(path: PathBuf) -> Result<LoadedDiscoveryArchive, DiscoveryArchiveError> {
        let (mut document, file_state) = match fs::read(&path) {
            Ok(bytes) => {
                if bytes.len() > MAX_ARCHIVE_BYTES {
                    return Err(DiscoveryArchiveError::TooLarge {
                        path,
                        bytes: bytes.len(),
                        maximum: MAX_ARCHIVE_BYTES,
                    });
                }
                let document =
                    serde_json::from_slice::<ArchiveDocument>(&bytes).map_err(|source| {
                        DiscoveryArchiveError::Decode {
                            path: path.clone(),
                            source,
                        }
                    })?;
                (document, DiscoveryArchiveFileState::Present)
            }
            Err(source) if source.kind() == ErrorKind::NotFound => {
                (ArchiveDocument::empty(), DiscoveryArchiveFileState::Missing)
            }
            Err(source) => {
                return Err(DiscoveryArchiveError::read(path, source));
            }
        };
        if document.format_version != FORMAT_VERSION {
            return Err(DiscoveryArchiveError::UnsupportedFormat {
                path,
                found: document.format_version,
                supported: FORMAT_VERSION,
            });
        }

        let mut catalog = DiscoveryCatalog::new();
        for (id, record) in &mut document.interfaces {
            let seed =
                record
                    .to_seed(id)
                    .map_err(|source| DiscoveryArchiveError::InvalidRecord {
                        path: path.clone(),
                        id: id.clone(),
                        source,
                    })?;
            record.refresh_manual_configuration(&seed.interface);
            catalog
                .restore(seed)
                .map_err(|source| DiscoveryArchiveError::InvalidRecord {
                    path: path.clone(),
                    id: id.clone(),
                    source: ArchiveRecordError::Catalog(source),
                })?;
        }

        Ok(LoadedDiscoveryArchive {
            archive: Self {
                path,
                interfaces: document.interfaces,
            },
            catalog,
            file_state,
        })
    }

    pub fn path(&self) -> &Path {
        &self.path
    }

    pub fn len(&self) -> usize {
        self.interfaces.len()
    }

    pub fn is_empty(&self) -> bool {
        self.interfaces.is_empty()
    }

    pub fn record(
        &mut self,
        record: impl Into<DiscoveryArchiveRecord>,
    ) -> Result<(), DiscoveryArchiveError> {
        let DiscoveryArchiveRecord { id, operation } = record.into();
        let mut updated = match operation {
            DiscoveryArchiveOperation::Upsert(updated) => *updated,
            DiscoveryArchiveOperation::Remove => {
                let previous = self.interfaces.remove(&id);
                if let Err(error) = self.persist() {
                    if let Some(previous) = previous {
                        self.interfaces.insert(id, previous);
                    }
                    return Err(error);
                }
                return Ok(());
            }
        };
        if let Some(previous) = self.interfaces.get(&id) {
            if !updated.merge_history_from(previous) {
                return Ok(());
            }
        }
        let previous = self.interfaces.insert(id.clone(), updated);
        if let Err(error) = self.persist() {
            match previous {
                Some(previous) => {
                    self.interfaces.insert(id, previous);
                }
                None => {
                    self.interfaces.remove(&id);
                }
            }
            return Err(error);
        }
        Ok(())
    }

    pub fn persist(&self) -> Result<(), DiscoveryArchiveError> {
        let document = ArchiveDocumentRef {
            format_version: FORMAT_VERSION,
            configuration_note: CONFIGURATION_NOTE,
            interfaces: &self.interfaces,
        };
        let mut bytes = serde_json::to_vec_pretty(&document).map_err(|source| {
            DiscoveryArchiveError::Encode {
                path: self.path.clone(),
                source,
            }
        })?;
        bytes.push(b'\n');
        if bytes.len() > MAX_ARCHIVE_BYTES {
            return Err(DiscoveryArchiveError::TooLarge {
                path: self.path.clone(),
                bytes: bytes.len(),
                maximum: MAX_ARCHIVE_BYTES,
            });
        }
        replace_archive_file(&self.path, &bytes)
    }
}