use std::collections::{BTreeMap, BTreeSet};
use std::io::Write;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use super::{Store, StoreError, format, owner_file, sync_directory};
use crate::certificate::Certificate;
use crate::lotw;
use crate::options::SigningOptions;
use crate::qso::Qso;
use crate::signed_log::{LogError, sign_log, sign_resolved, tq8};
use crate::signing::TqslKey;
use crate::station::StationLocation;
use crate::tbk::Backup;
pub const MAX_SIGNED_QSOS: usize = 100_000;
pub const MAX_SIGNED_STATIONS: usize = 4096;
pub const MAX_SIGNED_KEY_LEN: usize = 1024;
pub const MAX_SIGNED_STATION_LEN: usize = 2048;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SignedQso {
pub key: String,
pub station: String,
pub signed_at: Option<u64>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DuplicateChoice {
NewOnly,
SignAgain,
Cancel,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PreviouslySigned {
pub index: usize,
pub previous: SignedQso,
}
#[derive(Clone, Debug)]
pub struct SigningPlan {
qsos: Vec<Qso>,
snapshot: BTreeMap<String, Option<SignedQso>>,
station: StationLocation,
locations: Option<Vec<StationLocation>>,
new: Vec<usize>,
signed_before: Vec<PreviouslySigned>,
}
impl SigningPlan {
pub fn new_indices(&self) -> &[usize] {
&self.new
}
pub fn signed_before(&self) -> &[PreviouslySigned] {
&self.signed_before
}
pub fn choose(&self, choice: DuplicateChoice) -> Option<SigningSelection> {
let qsos = match choice {
DuplicateChoice::Cancel => return None,
DuplicateChoice::SignAgain => self.qsos.clone(),
DuplicateChoice::NewOnly => self.new.iter().map(|&i| self.qsos[i].clone()).collect(),
};
Some(SigningSelection {
qsos,
snapshot: (choice == DuplicateChoice::NewOnly).then(|| self.snapshot.clone()),
station: self.station.clone(),
locations: self.locations.as_ref().map(|locations| match choice {
DuplicateChoice::SignAgain => locations.clone(),
DuplicateChoice::NewOnly => {
self.new.iter().map(|&i| locations[i].clone()).collect()
}
DuplicateChoice::Cancel => unreachable!("cancelled above"),
}),
})
}
}
pub struct SigningSelection {
qsos: Vec<Qso>,
snapshot: Option<BTreeMap<String, Option<SignedQso>>>,
station: StationLocation,
locations: Option<Vec<StationLocation>>,
}
impl SigningSelection {
pub fn qsos(&self) -> &[Qso] {
&self.qsos
}
pub fn sign(
&self,
cert: &Certificate,
key: &TqslKey,
now: SystemTime,
) -> Result<SignedLog, LogError> {
let bytes = tq8(&match &self.locations {
Some(locations) => sign_resolved(cert, key, &self.qsos, locations, now)?,
None => sign_log(cert, key, &self.station, &self.qsos, now)?,
});
let signed_at = now.duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs());
let records = self
.qsos
.iter()
.enumerate()
.map(|(index, q)| {
let station = self.locations.as_ref().map_or(&self.station, |s| &s[index]);
SignedQso {
key: duplicate_key(q, station),
station: station_details(station),
signed_at,
}
})
.collect();
Ok(SignedLog {
bytes,
records,
snapshot: self.snapshot.clone(),
})
}
}
pub struct SignedLog {
bytes: Vec<u8>,
records: Vec<SignedQso>,
snapshot: Option<BTreeMap<String, Option<SignedQso>>>,
}
impl SignedLog {
pub fn tq8(&self) -> &[u8] {
&self.bytes
}
pub fn write_to(&self, store: &Store, path: &Path) -> Result<(), StoreError> {
self.check_output_path(store, path)?;
let _lock = store.lock()?;
let mut data = store.read()?;
if let Some(snapshot) = &self.snapshot
&& self.records.iter().any(|r| {
data.signed_qsos.get(&r.key) != snapshot.get(&r.key).and_then(Option::as_ref)
})
{
return Err(StoreError::SignedMeanwhile);
}
merge(&mut data.signed_qsos, &self.records, true)?;
let xml = format::write(&data)?;
let mut output = owner_file().create_new(true).open(path)?;
output.write_all(&self.bytes)?;
output.sync_all()?;
drop(output);
sync_directory(
path.parent()
.filter(|p| !p.as_os_str().is_empty())
.unwrap_or_else(|| Path::new(".")),
)?;
store.replace(&xml)
}
pub fn check_output_path(&self, store: &Store, path: &Path) -> Result<(), StoreError> {
if path
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| {
["store.xml", "store.xml.partial", "store.lock"]
.iter()
.any(|reserved| name.eq_ignore_ascii_case(reserved))
})
{
return Err(StoreError::Invalid("reserved store filename"));
}
let parent = path
.parent()
.filter(|p| !p.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));
#[cfg(unix)]
let in_store = {
use std::os::unix::fs::MetadataExt;
let output_dir = std::fs::metadata(parent)?;
let store_dir = std::fs::metadata(&store.dir)?;
(output_dir.dev(), output_dir.ino()) == (store_dir.dev(), store_dir.ino())
};
#[cfg(not(unix))]
let in_store = parent.canonicalize()? == store.dir.canonicalize()?;
if in_store {
return Err(StoreError::Invalid("output in store directory"));
}
Ok(())
}
}
pub fn duplicate_key(qso: &Qso, station: &StationLocation) -> String {
let mut key = format!("{}{}", station.call(), qso.sign_data());
key.truncate(127);
key
}
fn station_details(station: &StationLocation) -> String {
lotw::SIGN_STATION
.iter()
.filter_map(|name| {
station
.get(name)
.filter(|v| !v.is_empty())
.map(|value| format!("{name}: {value}"))
})
.collect::<Vec<_>>()
.join(", ")
}
fn signed_before(previous: &str, current: &str) -> bool {
if previous.eq_ignore_ascii_case(current) {
return true;
}
let old = previous.replace(' ', "");
let new = current.replace(' ', "");
old.split(',').any(|entry| {
let key_end = entry.find(':').map_or(0, |i| i + 1);
let key = &entry[..key_end];
new.split(',')
.find(|v| &v[..v.find(':').map_or(0, |i| i + 1)] == key)
.is_some_and(|value| entry != key && entry != value)
})
}
pub(super) fn validate(records: &BTreeMap<String, SignedQso>) -> Result<(), StoreError> {
if records.len() > MAX_SIGNED_QSOS {
return Err(StoreError::Limit("signed QSOs"));
}
let mut stations = BTreeSet::new();
for (key, record) in records {
check_record(record)?;
if key != &record.key {
return Err(StoreError::Invalid("signed QSO key"));
}
stations.insert(&record.station);
if stations.len() > MAX_SIGNED_STATIONS {
return Err(StoreError::Limit("signed stations"));
}
}
Ok(())
}
pub(super) fn check_record(record: &SignedQso) -> Result<(), StoreError> {
if record.key.len() > MAX_SIGNED_KEY_LEN || record.station.len() > MAX_SIGNED_STATION_LEN {
return Err(StoreError::Limit("signed QSO bytes"));
}
if record.key.is_empty()
|| record
.key
.chars()
.any(|c| c.is_control() || matches!(c, '\u{fffe}' | '\u{ffff}'))
{
return Err(StoreError::Invalid("signed QSO key"));
}
if record.station.chars().any(|c| {
(c < ' ' && !matches!(c, '\t' | '\n' | '\r')) || matches!(c, '\u{fffe}' | '\u{ffff}')
}) {
return Err(StoreError::Invalid("signed station character"));
}
Ok(())
}
pub(super) fn merge(
target: &mut BTreeMap<String, SignedQso>,
entries: &[SignedQso],
overwrite: bool,
) -> Result<usize, StoreError> {
if entries.len() > MAX_SIGNED_QSOS {
return Err(StoreError::Limit("signed QSOs"));
}
let before = target.len();
for entry in entries {
check_record(entry)?;
if overwrite || !target.contains_key(&entry.key) {
target.insert(entry.key.clone(), entry.clone());
}
if target.len() > MAX_SIGNED_QSOS {
return Err(StoreError::Limit("signed QSOs"));
}
}
validate(target)?;
Ok(target.len() - before)
}
impl Store {
pub fn signed_qsos(&self) -> Result<Vec<SignedQso>, StoreError> {
let _lock = self.lock()?;
Ok(self.read()?.signed_qsos.into_values().collect())
}
pub fn signed_qso(&self, key: &str) -> Result<Option<SignedQso>, StoreError> {
let _lock = self.lock()?;
Ok(self.read()?.signed_qsos.remove(key))
}
pub fn add_signed_qsos(&self, entries: &[SignedQso]) -> Result<usize, StoreError> {
let _lock = self.lock()?;
let mut data = self.read()?;
let count = merge(&mut data.signed_qsos, entries, false)?;
if count != 0 {
self.replace(&format::write(&data)?)?;
}
Ok(count)
}
pub fn import_signed_qsos(&self, backup: &Backup) -> Result<usize, StoreError> {
if backup.signed_qsos.len() > MAX_SIGNED_QSOS {
return Err(StoreError::Limit("signed QSOs"));
}
self.add_signed_qsos(
&backup
.signed_qsos
.iter()
.map(|q| SignedQso {
key: q.key.clone(),
station: q.station.clone(),
signed_at: None,
})
.collect::<Vec<_>>(),
)
}
pub fn wipe_signed_qsos(&self) -> Result<usize, StoreError> {
let _lock = self.lock()?;
let mut data = self.read()?;
let count = data.signed_qsos.len();
data.signed_qsos.clear();
self.replace(&format::write(&data)?)?;
Ok(count)
}
pub fn prepare_signing(
&self,
qsos: &[Qso],
station: &StationLocation,
) -> Result<SigningPlan, StoreError> {
self.prepare(qsos, station, None)
}
pub fn prepare_signing_with_options(
&self,
qsos: &[Qso],
station: &StationLocation,
options: &SigningOptions,
) -> Result<SigningPlan, StoreError> {
self.prepare(qsos, station, Some(options))
}
fn prepare(
&self,
qsos: &[Qso],
station: &StationLocation,
options: Option<&SigningOptions>,
) -> Result<SigningPlan, StoreError> {
if qsos.len() > MAX_SIGNED_QSOS {
return Err(StoreError::Limit("signing QSOs"));
}
let mut seen = {
let _lock = self.lock()?;
self.read()?.signed_qsos
};
let locations = options
.map(|o| o.locations(qsos, station))
.transpose()
.map_err(StoreError::Log)?;
let qsos = match (options, &locations) {
(Some(o), Some(locations)) => o.qsos(qsos, locations).map_err(StoreError::Log)?,
_ => qsos.to_vec(),
};
let mut plan = SigningPlan {
qsos: qsos.to_vec(),
snapshot: BTreeMap::new(),
station: station.clone(),
locations,
new: Vec::new(),
signed_before: Vec::new(),
};
for (index, qso) in qsos.iter().enumerate() {
let station = plan.locations.as_ref().map_or(station, |s| &s[index]);
let details = station_details(station);
let key = duplicate_key(qso, station);
if !plan.snapshot.contains_key(&key) {
plan.snapshot.insert(key.clone(), seen.get(&key).cloned());
}
if let Some(previous) = seen
.get(&key)
.filter(|p| signed_before(&p.station, &details))
{
plan.signed_before.push(PreviouslySigned {
index,
previous: previous.clone(),
});
} else {
seen.insert(
key.clone(),
SignedQso {
key,
station: details.clone(),
signed_at: None,
},
);
plan.new.push(index);
}
}
Ok(plan)
}
}