use crate::error::NsfError;
pub const NSF_LSIG_BYTE_0: u8 = 0x1A;
pub const NSF_LSIG_BYTE_1: u8 = 0x00;
pub const MIN_PLAUSIBLE_DB_HEADER_SIZE: u32 = 64;
pub const MAX_PLAUSIBLE_DB_HEADER_SIZE: u32 = 65_536;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FileKind {
Nsf {
db_header_size: u32,
},
NotNsf {
reason: String,
},
}
impl FileKind {
pub fn is_nsf(&self) -> bool {
matches!(self, Self::Nsf { .. })
}
}
pub fn identify_file(bytes: &[u8]) -> FileKind {
match identify_file_strict(bytes) {
Ok(kind) => kind,
Err(e) => FileKind::NotNsf {
reason: e.to_string(),
},
}
}
pub fn identify_file_strict(bytes: &[u8]) -> Result<FileKind, NsfError> {
if bytes.len() < 6 {
return Err(NsfError::TooShort {
actual: bytes.len(),
required: 6,
});
}
if bytes[0] != NSF_LSIG_BYTE_0 || bytes[1] != NSF_LSIG_BYTE_1 {
return Err(NsfError::BadFileSignature {
observed: [bytes[0], bytes[1]],
});
}
let db_header_size = u32::from_le_bytes([bytes[2], bytes[3], bytes[4], bytes[5]]);
if db_header_size < MIN_PLAUSIBLE_DB_HEADER_SIZE
|| db_header_size > MAX_PLAUSIBLE_DB_HEADER_SIZE
{
return Err(NsfError::BadHeaderSize {
size: db_header_size,
});
}
Ok(FileKind::Nsf { db_header_size })
}
#[cfg(test)]
mod tests {
use super::*;
fn ok_header() -> Vec<u8> {
let mut v = vec![0u8; 16];
v[0] = 0x1A;
v[1] = 0x00;
v[2] = 0x00;
v[3] = 0x01;
v[4] = 0x00;
v[5] = 0x00;
v
}
#[test]
fn identifies_valid_nsf_header() {
let h = ok_header();
let kind = identify_file(&h);
assert!(kind.is_nsf());
match kind {
FileKind::Nsf { db_header_size } => assert_eq!(db_header_size, 256),
_ => unreachable!(),
}
}
#[test]
fn rejects_too_short_file() {
let kind = identify_file(&[0x1A, 0x00, 0x00]);
assert!(!kind.is_nsf());
match kind {
FileKind::NotNsf { reason } => assert!(reason.contains("too short")),
_ => unreachable!(),
}
}
#[test]
fn rejects_bad_signature() {
let mut h = ok_header();
h[0] = 0x21; h[1] = 0x42;
let kind = identify_file(&h);
assert!(!kind.is_nsf());
match kind {
FileKind::NotNsf { reason } => {
assert!(reason.contains("21 42"));
assert!(reason.contains("1A 00"));
}
_ => unreachable!(),
}
}
#[test]
fn rejects_zero_header_size() {
let mut h = ok_header();
h[2] = 0x00;
h[3] = 0x00;
h[4] = 0x00;
h[5] = 0x00;
let kind = identify_file(&h);
assert!(!kind.is_nsf());
match kind {
FileKind::NotNsf { reason } => assert!(reason.contains("implausible")),
_ => unreachable!(),
}
}
#[test]
fn rejects_impossibly_large_header_size() {
let mut h = ok_header();
h[2] = 0xFF;
h[3] = 0xFF;
h[4] = 0xFF;
h[5] = 0xFF;
let kind = identify_file(&h);
assert!(!kind.is_nsf());
}
#[test]
fn strict_variant_returns_structured_error() {
let mut h = ok_header();
h[0] = 0xDE;
h[1] = 0xAD;
let err = identify_file_strict(&h).unwrap_err();
assert!(matches!(
err,
NsfError::BadFileSignature { observed: [0xDE, 0xAD] }
));
}
#[test]
fn accepts_extra_bytes_after_header() {
let mut h = ok_header();
h.extend_from_slice(&[0xAA; 100_000]);
let kind = identify_file(&h);
assert!(kind.is_nsf());
}
}