use super::io::{decode_puz_string, read_bytes, read_u16, read_u8, skip_bytes};
use crate::error::PuzError;
use std::io::{BufReader, Read};
#[derive(Debug)]
pub(crate) struct Header {
pub width: u8,
pub height: u8,
pub num_clues: u16,
pub version: String,
#[allow(dead_code)]
pub bitmask: u16,
pub is_scrambled: bool,
}
pub(crate) fn parse_header<R: Read>(reader: &mut BufReader<R>) -> Result<Header, PuzError> {
skip_bytes(reader, 10)?;
let version_bytes = read_bytes(reader, 4)?;
let version = decode_puz_string(&version_bytes)?;
skip_bytes(reader, 16)?;
let width = read_u8(reader)?;
let height = read_u8(reader)?;
let num_clues = read_u16(reader)?;
let bitmask = read_u16(reader)?;
let scrambled_tag = read_u16(reader)?;
if width == 0 || height == 0 {
return Err(PuzError::InvalidDimensions { width, height });
}
let is_scrambled = scrambled_tag != 0;
Ok(Header {
width,
height,
num_clues,
version: version.trim_end_matches('\0').to_string(),
bitmask,
is_scrambled,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn create_header_data(
width: u8,
height: u8,
num_clues: u16,
version: &[u8; 4],
bitmask: u16,
scrambled_tag: u16,
) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(&[0; 10]);
data.extend_from_slice(version);
data.extend_from_slice(&[0; 16]);
data.push(width);
data.push(height);
data.extend_from_slice(&num_clues.to_le_bytes());
data.extend_from_slice(&bitmask.to_le_bytes());
data.extend_from_slice(&scrambled_tag.to_le_bytes());
data
}
#[test]
fn test_parse_header_valid() {
let data = create_header_data(
15, 15, 76, b"1.3\0", 0x0000, 0x0000, );
let mut reader = BufReader::new(Cursor::new(data));
let header = parse_header(&mut reader).unwrap();
assert_eq!(header.width, 15);
assert_eq!(header.height, 15);
assert_eq!(header.num_clues, 76);
assert_eq!(header.version, "1.3");
assert_eq!(header.bitmask, 0x0000);
assert!(!header.is_scrambled);
}
#[test]
fn test_parse_header_scrambled() {
let data = create_header_data(
21, 21, 140, b"1.2c", 0x0004, 0x0004, );
let mut reader = BufReader::new(Cursor::new(data));
let header = parse_header(&mut reader).unwrap();
assert_eq!(header.width, 21);
assert_eq!(header.height, 21);
assert_eq!(header.num_clues, 140);
assert_eq!(header.version, "1.2c");
assert_eq!(header.bitmask, 0x0004);
assert!(header.is_scrambled);
}
#[test]
fn test_parse_header_version_formats() {
let data1 = create_header_data(15, 15, 76, b"1.4\0", 0x0000, 0x0000);
let mut reader1 = BufReader::new(Cursor::new(data1));
let header1 = parse_header(&mut reader1).unwrap();
assert_eq!(header1.version, "1.4");
let data2 = create_header_data(15, 15, 76, b"2.0a", 0x0000, 0x0000);
let mut reader2 = BufReader::new(Cursor::new(data2));
let header2 = parse_header(&mut reader2).unwrap();
assert_eq!(header2.version, "2.0a");
let data3 = create_header_data(15, 15, 76, b"1\0\0\0", 0x0000, 0x0000);
let mut reader3 = BufReader::new(Cursor::new(data3));
let header3 = parse_header(&mut reader3).unwrap();
assert_eq!(header3.version, "1");
}
#[test]
fn test_parse_header_invalid_dimensions_zero_width() {
let data = create_header_data(0, 15, 76, b"1.3\0", 0x0000, 0x0000);
let mut reader = BufReader::new(Cursor::new(data));
let result = parse_header(&mut reader);
assert!(result.is_err());
if let Err(PuzError::InvalidDimensions { width, height }) = result {
assert_eq!(width, 0);
assert_eq!(height, 15);
} else {
panic!("Expected InvalidDimensions error");
}
}
#[test]
fn test_parse_header_invalid_dimensions_zero_height() {
let data = create_header_data(15, 0, 76, b"1.3\0", 0x0000, 0x0000);
let mut reader = BufReader::new(Cursor::new(data));
let result = parse_header(&mut reader);
assert!(result.is_err());
if let Err(PuzError::InvalidDimensions { width, height }) = result {
assert_eq!(width, 15);
assert_eq!(height, 0);
} else {
panic!("Expected InvalidDimensions error");
}
}
#[test]
fn test_parse_header_invalid_dimensions_both_zero() {
let data = create_header_data(0, 0, 0, b"1.3\0", 0x0000, 0x0000);
let mut reader = BufReader::new(Cursor::new(data));
let result = parse_header(&mut reader);
assert!(result.is_err());
if let Err(PuzError::InvalidDimensions { width, height }) = result {
assert_eq!(width, 0);
assert_eq!(height, 0);
} else {
panic!("Expected InvalidDimensions error");
}
}
#[test]
fn test_parse_header_large_dimensions() {
let data = create_header_data(255, 255, 30000, b"1.3\0", 0x0000, 0x0000);
let mut reader = BufReader::new(Cursor::new(data));
let header = parse_header(&mut reader).unwrap();
assert_eq!(header.width, 255);
assert_eq!(header.height, 255);
assert_eq!(header.num_clues, 30000);
}
#[test]
fn test_parse_header_minimal_dimensions() {
let data = create_header_data(1, 1, 2, b"1.3\0", 0x0000, 0x0000);
let mut reader = BufReader::new(Cursor::new(data));
let header = parse_header(&mut reader).unwrap();
assert_eq!(header.width, 1);
assert_eq!(header.height, 1);
assert_eq!(header.num_clues, 2);
}
#[test]
fn test_parse_header_scrambling_detection() {
let scramble_values = [0x0001, 0x0004, 0x0008, 0xFFFF];
for &scramble_tag in &scramble_values {
let data = create_header_data(15, 15, 76, b"1.3\0", 0x0000, scramble_tag);
let mut reader = BufReader::new(Cursor::new(data));
let header = parse_header(&mut reader).unwrap();
assert!(
header.is_scrambled,
"Failed to detect scrambling for tag 0x{scramble_tag:04X}"
);
}
let data = create_header_data(15, 15, 76, b"1.3\0", 0x0000, 0x0000);
let mut reader = BufReader::new(Cursor::new(data));
let header = parse_header(&mut reader).unwrap();
assert!(!header.is_scrambled);
}
#[test]
fn test_parse_header_bitmask_values() {
let bitmask_values = [0x0000, 0x0001, 0x0080, 0x8000, 0xFFFF];
for &bitmask in &bitmask_values {
let data = create_header_data(15, 15, 76, b"1.3\0", bitmask, 0x0000);
let mut reader = BufReader::new(Cursor::new(data));
let header = parse_header(&mut reader).unwrap();
assert_eq!(
header.bitmask, bitmask,
"Bitmask not preserved: expected 0x{:04X}, got 0x{:04X}",
bitmask, header.bitmask
);
}
}
#[test]
fn test_parse_header_truncated_data() {
let mut data = Vec::new();
data.extend_from_slice(&[0; 10]); data.extend_from_slice(b"1.3\0"); data.extend_from_slice(&[0; 16]); data.push(15); data.push(15);
let mut reader = BufReader::new(Cursor::new(data));
let result = parse_header(&mut reader);
assert!(result.is_err());
matches!(result.unwrap_err(), PuzError::IoError { .. });
}
#[test]
fn test_parse_header_version_encoding() {
let version_bytes = [b'v', 0x97, b'1', 0x00]; let data = create_header_data(15, 15, 76, &version_bytes, 0x0000, 0x0000);
let mut reader = BufReader::new(Cursor::new(data));
let header = parse_header(&mut reader).unwrap();
assert!(header.version.contains('ā'));
assert!(header.version.starts_with('v'));
assert!(header.version.ends_with('1'));
}
}