use crate::{Error, Result};
const MARKER_MAGIC: &[u8; 8] = b"Q2FAMRK1";
const PENDING_MAGIC: &[u8; 8] = b"Q2FAPND1";
pub const STATE_FORMAT_VERSION: u8 = 1;
pub const MARKER_LEN: usize = 19;
pub const PENDING_LEN: usize = 18;
const FLAG_PASSWORD: u8 = 0x01;
const FLAG_BIOMETRIC: u8 = 0x02;
const KNOWN_FLAGS: u8 = FLAG_PASSWORD | FLAG_BIOMETRIC;
mod marker_offset {
pub const MAGIC: usize = 0;
pub const VERSION: usize = 8;
pub const FLAGS: usize = 9;
pub const RESERVED: usize = 10;
pub const MIGRATED_AT: usize = 11;
}
mod pending_offset {
pub const MAGIC: usize = 0;
pub const VERSION: usize = 8;
pub const RESERVED: usize = 9;
pub const STARTED_AT: usize = 10;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LockMarker {
password: bool,
biometric: bool,
migrated_at: u64,
}
impl LockMarker {
pub fn new(password: bool, biometric: bool, migrated_at: u64) -> Result<Self> {
if !password && !biometric {
return Err(Error::LockFileMalformed {
reason: "a lock marker must name at least one wrap",
});
}
if migrated_at == 0 {
return Err(Error::TimestampZero);
}
Ok(Self {
password,
biometric,
migrated_at,
})
}
pub fn parse(bytes: &[u8]) -> Result<Self> {
if bytes.len() != MARKER_LEN {
return Err(Error::LockFileMalformed {
reason: "the lock marker does not have the fixed length",
});
}
if &bytes[marker_offset::MAGIC..marker_offset::VERSION] != MARKER_MAGIC {
return Err(Error::LockFileMalformed {
reason: "magic mismatch: this is not a lock marker",
});
}
if bytes[marker_offset::VERSION] != STATE_FORMAT_VERSION {
return Err(Error::UnsupportedFormatVersion {
version: bytes[marker_offset::VERSION],
});
}
let flags = bytes[marker_offset::FLAGS];
if flags & !KNOWN_FLAGS != 0 {
return Err(Error::LockFileMalformed {
reason: "the lock marker carries flag bits this build does not understand",
});
}
if bytes[marker_offset::RESERVED] != 0 {
return Err(Error::LockFileMalformed {
reason: "the lock marker reserved byte must be 0",
});
}
let migrated_at = u64::from_be_bytes(
bytes[marker_offset::MIGRATED_AT..MARKER_LEN]
.try_into()
.map_err(|_| Error::LockFileMalformed {
reason: "failed to read the migration time",
})?,
);
Self::new(
flags & FLAG_PASSWORD != 0,
flags & FLAG_BIOMETRIC != 0,
migrated_at,
)
}
#[must_use]
pub fn encode(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(MARKER_LEN);
bytes.extend_from_slice(MARKER_MAGIC);
bytes.push(STATE_FORMAT_VERSION);
let mut flags = 0_u8;
if self.password {
flags |= FLAG_PASSWORD;
}
if self.biometric {
flags |= FLAG_BIOMETRIC;
}
bytes.push(flags);
bytes.push(0);
bytes.extend_from_slice(&self.migrated_at.to_be_bytes());
debug_assert_eq!(bytes.len(), MARKER_LEN);
bytes
}
#[must_use]
pub const fn has_password(&self) -> bool {
self.password
}
#[must_use]
pub const fn has_biometric(&self) -> bool {
self.biometric
}
#[must_use]
pub const fn migrated_at(&self) -> u64 {
self.migrated_at
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LockPending {
started_at: u64,
}
impl LockPending {
pub fn new(started_at: u64) -> Result<Self> {
if started_at == 0 {
return Err(Error::TimestampZero);
}
Ok(Self { started_at })
}
pub fn parse(bytes: &[u8]) -> Result<Self> {
if bytes.len() != PENDING_LEN {
return Err(Error::LockFileMalformed {
reason: "the pending marker does not have the fixed length",
});
}
if &bytes[pending_offset::MAGIC..pending_offset::VERSION] != PENDING_MAGIC {
return Err(Error::LockFileMalformed {
reason: "magic mismatch: this is not a pending lock marker",
});
}
if bytes[pending_offset::VERSION] != STATE_FORMAT_VERSION {
return Err(Error::UnsupportedFormatVersion {
version: bytes[pending_offset::VERSION],
});
}
if bytes[pending_offset::RESERVED] != 0 {
return Err(Error::LockFileMalformed {
reason: "the pending marker reserved byte must be 0",
});
}
let started_at = u64::from_be_bytes(
bytes[pending_offset::STARTED_AT..PENDING_LEN]
.try_into()
.map_err(|_| Error::LockFileMalformed {
reason: "failed to read the start time",
})?,
);
Self::new(started_at)
}
#[must_use]
pub fn encode(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(PENDING_LEN);
bytes.extend_from_slice(PENDING_MAGIC);
bytes.push(STATE_FORMAT_VERSION);
bytes.push(0);
bytes.extend_from_slice(&self.started_at.to_be_bytes());
debug_assert_eq!(bytes.len(), PENDING_LEN);
bytes
}
#[must_use]
pub const fn started_at(&self) -> u64 {
self.started_at
}
}
#[cfg(test)]
mod tests {
use super::*;
fn marker(password: bool, biometric: bool) -> LockMarker {
LockMarker::new(password, biometric, 1_700_000_000).expect("valid marker")
}
#[test]
fn marker_roundtrip() {
for (password, biometric) in [(true, false), (false, true), (true, true)] {
let marker = marker(password, biometric);
let encoded = marker.encode();
assert_eq!(encoded.len(), MARKER_LEN);
let parsed = LockMarker::parse(&encoded).expect("valid marker");
assert_eq!(parsed, marker);
assert_eq!(parsed.has_password(), password);
assert_eq!(parsed.has_biometric(), biometric);
assert_eq!(parsed.migrated_at(), 1_700_000_000);
}
}
#[test]
fn marker_without_a_wrap_is_rejected() {
assert!(matches!(
LockMarker::new(false, false, 1),
Err(Error::LockFileMalformed { .. })
));
}
#[test]
fn zero_time_is_rejected() {
assert!(matches!(
LockMarker::new(true, false, 0),
Err(Error::TimestampZero)
));
assert!(matches!(LockPending::new(0), Err(Error::TimestampZero)));
}
#[test]
fn marker_unknown_flags_are_rejected() {
let mut encoded = marker(true, false).encode();
encoded[marker_offset::FLAGS] = 0x80;
assert!(matches!(
LockMarker::parse(&encoded),
Err(Error::LockFileMalformed { .. })
));
}
#[test]
fn marker_reserved_byte_must_be_zero() {
let mut encoded = marker(true, false).encode();
encoded[marker_offset::RESERVED] = 1;
assert!(matches!(
LockMarker::parse(&encoded),
Err(Error::LockFileMalformed { .. })
));
}
#[test]
fn marker_version_is_checked() {
let mut encoded = marker(true, false).encode();
encoded[marker_offset::VERSION] = 2;
assert!(matches!(
LockMarker::parse(&encoded),
Err(Error::UnsupportedFormatVersion { version: 2 })
));
}
#[test]
fn marker_exact_length_is_required() {
let encoded = marker(true, false).encode();
for cut in [0_usize, 1, MARKER_LEN - 1] {
assert!(LockMarker::parse(&encoded[..cut]).is_err());
}
let mut longer = encoded;
longer.push(0);
assert!(LockMarker::parse(&longer).is_err());
}
#[test]
fn marker_layout_is_stable() {
assert_eq!(MARKER_LEN, 19);
assert_eq!(marker_offset::MAGIC, 0);
assert_eq!(marker_offset::VERSION, 8);
assert_eq!(marker_offset::FLAGS, 9);
assert_eq!(marker_offset::RESERVED, 10);
assert_eq!(marker_offset::MIGRATED_AT, 11);
}
#[test]
fn pending_roundtrip() {
let pending = LockPending::new(1_700_000_000).expect("valid pending");
let encoded = pending.encode();
assert_eq!(encoded.len(), PENDING_LEN);
let parsed = LockPending::parse(&encoded).expect("valid pending");
assert_eq!(parsed, pending);
assert_eq!(parsed.started_at(), 1_700_000_000);
}
#[test]
fn pending_structure_is_checked() {
let mut encoded = LockPending::new(1).expect("valid pending").encode();
encoded[pending_offset::RESERVED] = 1;
assert!(matches!(
LockPending::parse(&encoded),
Err(Error::LockFileMalformed { .. })
));
let encoded = LockPending::new(1).expect("valid pending").encode();
assert!(LockPending::parse(&encoded[..PENDING_LEN - 1]).is_err());
let mut longer = encoded;
longer.push(0);
assert!(LockPending::parse(&longer).is_err());
}
#[test]
fn pending_layout_is_stable() {
assert_eq!(PENDING_LEN, 18);
assert_eq!(pending_offset::MAGIC, 0);
assert_eq!(pending_offset::VERSION, 8);
assert_eq!(pending_offset::RESERVED, 9);
assert_eq!(pending_offset::STARTED_AT, 10);
}
#[test]
fn the_two_files_have_different_magics() {
let marker = marker(true, false).encode();
let pending = LockPending::new(1).expect("valid pending").encode();
assert!(LockMarker::parse(&pending).is_err());
assert!(LockPending::parse(&marker).is_err());
}
}