use std::fs;
use std::io::{Seek, SeekFrom, Write};
use std::path::Path;
use queue_file::{Error, QueueFile};
const V2_MAGIC: u32 = 0x5146_4D48;
const V2_SLOT_A_OFFSET: u64 = 0;
const V2_SLOT_B_OFFSET: u64 = 4096;
const V2_SLOT_LEN: usize = 56;
const V2_DATA_START: u64 = 8192;
const V2_INITIAL_LEN: u64 = 8192;
const V1_VERSIONED_HEADER: u32 = 0x8000_0001;
const V2_ELEM_HDR_LEN: usize = 28;
fn temp_path() -> auto_delete_path::AutoDeletePath {
auto_delete_path::AutoDeletePath::temp()
}
fn patch_file(path: impl AsRef<Path>, offset: u64, data: &[u8]) {
let mut f = fs::OpenOptions::new().write(true).open(path).unwrap();
f.seek(SeekFrom::Start(offset)).unwrap();
f.write_all(data).unwrap();
}
fn build_v2_slot(
file_len: u64,
elem_cnt: u32,
first_pos: u64,
last_pos: u64,
generation: u64,
next_seq: u64,
) -> [u8; V2_SLOT_LEN] {
let mut buf = [0u8; V2_SLOT_LEN];
buf[0..4].copy_from_slice(&V2_MAGIC.to_be_bytes());
buf[4] = 2; buf[8..16].copy_from_slice(&(file_len as i64).to_be_bytes());
buf[16..20].copy_from_slice(&(elem_cnt as i32).to_be_bytes());
buf[20..28].copy_from_slice(&(first_pos as i64).to_be_bytes());
buf[28..36].copy_from_slice(&(last_pos as i64).to_be_bytes());
buf[36..44].copy_from_slice(&(generation as i64).to_be_bytes());
buf[44..52].copy_from_slice(&(next_seq as i64).to_be_bytes());
let crc = crc32fast::hash(&buf[..52]);
buf[52..56].copy_from_slice(&crc.to_be_bytes());
buf
}
fn create_v2_file_with_slot_a(path: impl AsRef<Path>, slot: [u8; V2_SLOT_LEN]) {
let path = path.as_ref();
fs::write(path, vec![0u8; V2_INITIAL_LEN as usize]).unwrap();
patch_file(path, V2_SLOT_A_OFFSET, &slot);
}
fn build_v1_file(elements: &[&[u8]]) -> Vec<u8> {
let data_start: usize = 32;
let data_size: usize = elements.iter().map(|e| 4 + e.len()).sum();
let total = data_start + data_size;
let file_len = total.next_power_of_two().max(4096) as u64;
let mut data = vec![0u8; file_len as usize];
data[0..4].copy_from_slice(&V1_VERSIONED_HEADER.to_be_bytes());
data[4..12].copy_from_slice(&file_len.to_be_bytes()); data[12..16].copy_from_slice(&(elements.len() as i32).to_be_bytes());
if !elements.is_empty() {
let first_phys: u64 = data_start as u64;
let mut pos = data_start;
let mut last_phys = first_phys;
for elem in elements {
last_phys = pos as u64;
data[pos..pos + 4].copy_from_slice(&(elem.len() as u32).to_be_bytes());
data[pos + 4..pos + 4 + elem.len()].copy_from_slice(elem);
pos += 4 + elem.len();
}
data[16..24].copy_from_slice(&first_phys.to_be_bytes()); data[24..32].copy_from_slice(&last_phys.to_be_bytes()); }
data
}
#[test]
fn v1_empty_file_migrates_to_v2() {
let path = temp_path();
fs::write(&path, build_v1_file(&[])).unwrap();
let qf = QueueFile::open(&path).unwrap();
assert!(qf.is_empty());
}
#[test]
fn v1_file_with_elements_migrates_to_v2() {
let path = temp_path();
fs::write(&path, build_v1_file(&[b"hello", b"world"])).unwrap();
let mut qf = QueueFile::open(&path).unwrap();
assert_eq!(qf.size(), 2);
let first = qf.peek().unwrap().unwrap();
assert_eq!(first, b"hello");
qf.remove().unwrap();
let second = qf.peek().unwrap().unwrap();
assert_eq!(second, b"world");
}
#[test]
fn legacy_on_expansion_wrapped() {
let path = temp_path();
let mut qf = QueueFile::open_legacy(&path).unwrap();
let payload = vec![0u8; 100];
for _ in 0..39 {
qf.add(&payload).unwrap();
}
qf.remove_n(2).unwrap();
qf.add(&payload).unwrap(); qf.add(&payload).unwrap();
qf.add(&payload).unwrap();
assert_eq!(qf.size(), 40);
}
const FIRST_ELEM_PHYS: u64 = V2_DATA_START;
fn footer_phys(payload_len: u64) -> u64 {
FIRST_ELEM_PHYS + V2_ELEM_HDR_LEN as u64 + payload_len
}
#[test]
fn v2_element_header_magic_mismatch() {
let path = temp_path();
{
let mut qf = QueueFile::open(&path).unwrap();
qf.add(b"hello").unwrap();
}
patch_file(&path, FIRST_ELEM_PHYS, &[0u8; 4]);
let err = QueueFile::open(&path).unwrap_err();
assert!(
matches!(err, Error::CorruptedFile { .. }),
"expected CorruptedFile, got {err:?}"
);
}
#[test]
fn v2_element_header_negative_payload_len() {
let path = temp_path();
{
let mut qf = QueueFile::open(&path).unwrap();
qf.add(b"hello").unwrap();
}
patch_file(&path, FIRST_ELEM_PHYS + 20, &0xFFFF_FFFFu32.to_be_bytes());
let err = QueueFile::open(&path).unwrap_err();
assert!(
matches!(err, Error::CorruptedFile { .. }),
"expected CorruptedFile, got {err:?}"
);
}
#[test]
fn v2_element_header_seq_zero() {
let path = temp_path();
{
let mut qf = QueueFile::open(&path).unwrap();
qf.add(b"hello").unwrap();
}
patch_file(&path, FIRST_ELEM_PHYS + 4, &[0u8; 8]);
let err = QueueFile::open(&path).unwrap_err();
assert!(
matches!(err, Error::CorruptedFile { .. }),
"expected CorruptedFile, got {err:?}"
);
}
#[test]
fn v2_element_header_crc_mismatch() {
let path = temp_path();
{
let mut qf = QueueFile::open(&path).unwrap();
qf.add(b"hello").unwrap();
}
patch_file(&path, FIRST_ELEM_PHYS + 24, &[0u8; 4]);
let err = QueueFile::open(&path).unwrap_err();
assert!(
matches!(err, Error::CorruptedFile { .. }),
"expected CorruptedFile, got {err:?}"
);
}
#[test]
fn v2_element_footer_magic_mismatch() {
let path = temp_path();
{
let mut qf = QueueFile::open(&path).unwrap();
qf.add(b"hello").unwrap();
}
let ftr_offset = footer_phys(5);
patch_file(&path, ftr_offset, &[0u8; 4]);
let qf = QueueFile::open(&path).unwrap();
let err = qf.peek().unwrap_err();
assert!(
matches!(err, Error::CorruptedFile { .. }),
"expected CorruptedFile, got {err:?}"
);
}
#[test]
fn v2_element_footer_seq_mismatch() {
let path = temp_path();
{
let mut qf = QueueFile::open(&path).unwrap();
qf.add(b"hello").unwrap();
}
let ftr_offset = footer_phys(5);
patch_file(&path, ftr_offset + 4, &2u64.to_be_bytes());
let qf = QueueFile::open(&path).unwrap();
let err = qf.peek().unwrap_err();
assert!(
matches!(err, Error::CorruptedFile { .. }),
"expected CorruptedFile, got {err:?}"
);
}
#[test]
fn v2_element_footer_crc_mismatch() {
let path = temp_path();
{
let mut qf = QueueFile::open(&path).unwrap();
qf.add(b"hello").unwrap();
}
let ftr_offset = footer_phys(5);
patch_file(&path, ftr_offset + 12, &[0u8; 4]);
let qf = QueueFile::open(&path).unwrap();
let err = qf.peek().unwrap_err();
assert!(
matches!(err, Error::CorruptedFile { .. }),
"expected CorruptedFile, got {err:?}"
);
}
#[test]
fn versioned_header_unsupported_version() {
let path = temp_path();
let mut data = vec![0u8; 4096];
data[0..4].copy_from_slice(&0x8000_0002u32.to_be_bytes());
data[4..12].copy_from_slice(&4096u64.to_be_bytes()); fs::write(&path, &data).unwrap();
let err = QueueFile::open(&path).unwrap_err();
assert!(
matches!(err, Error::UnsupportedVersion { detected: 2, .. }),
"expected UnsupportedVersion(2), got {err:?}"
);
}
#[test]
fn legacy_header_file_len_overflow() {
let path = temp_path();
let mut data = vec![0u8; 64];
data[0..4].copy_from_slice(&0x8000_0000u32.to_be_bytes());
fs::write(&path, &data).unwrap();
let err = QueueFile::open_legacy(&path).unwrap_err();
assert!(
matches!(err, Error::CorruptedFile { .. }),
"expected CorruptedFile, got {err:?}"
);
}
#[test]
fn legacy_header_elem_cnt_overflow() {
let path = temp_path();
let mut data = vec![0u8; 64];
data[0..4].copy_from_slice(&64u32.to_be_bytes());
data[4..8].copy_from_slice(&0x8000_0000u32.to_be_bytes());
fs::write(&path, &data).unwrap();
let err = QueueFile::open_legacy(&path).unwrap_err();
assert!(
matches!(err, Error::CorruptedFile { .. }),
"expected CorruptedFile, got {err:?}"
);
}
#[test]
fn legacy_header_first_pos_overflow() {
let path = temp_path();
let mut data = vec![0u8; 64];
data[0..4].copy_from_slice(&64u32.to_be_bytes()); data[4..8].copy_from_slice(&1u32.to_be_bytes()); data[8..12].copy_from_slice(&0x8000_0000u32.to_be_bytes()); data[12..16].copy_from_slice(&16u32.to_be_bytes()); fs::write(&path, &data).unwrap();
let err = QueueFile::open_legacy(&path).unwrap_err();
assert!(
matches!(err, Error::CorruptedFile { .. }),
"expected CorruptedFile, got {err:?}"
);
}
#[test]
fn legacy_header_last_pos_overflow() {
let path = temp_path();
let mut data = vec![0u8; 64];
data[0..4].copy_from_slice(&64u32.to_be_bytes()); data[4..8].copy_from_slice(&1u32.to_be_bytes()); data[8..12].copy_from_slice(&16u32.to_be_bytes()); data[12..16].copy_from_slice(&0x8000_0000u32.to_be_bytes()); fs::write(&path, &data).unwrap();
let err = QueueFile::open_legacy(&path).unwrap_err();
assert!(
matches!(err, Error::CorruptedFile { .. }),
"expected CorruptedFile, got {err:?}"
);
}
#[test]
fn v2_both_slots_invalid_crc() {
let path = temp_path();
let mut data = vec![0u8; V2_INITIAL_LEN as usize];
data[0..4].copy_from_slice(&V2_MAGIC.to_be_bytes());
data[4] = 2;
data[V2_SLOT_B_OFFSET as usize..V2_SLOT_B_OFFSET as usize + 4]
.copy_from_slice(&V2_MAGIC.to_be_bytes());
data[V2_SLOT_B_OFFSET as usize + 4] = 2;
fs::write(&path, &data).unwrap();
let err = QueueFile::open(&path).unwrap_err();
assert!(
matches!(err, Error::CorruptedFile { .. }),
"expected CorruptedFile, got {err:?}"
);
}
#[test]
fn v2_slot_file_length_too_small() {
let path = temp_path();
let slot = build_v2_slot(100, 0, 0, 0, 1, 1);
create_v2_file_with_slot_a(&path, slot);
let err = QueueFile::open(&path).unwrap_err();
assert!(
matches!(err, Error::CorruptedFile { .. }),
"expected CorruptedFile, got {err:?}"
);
}
#[test]
fn v2_slot_file_length_truncated() {
let path = temp_path();
let slot = build_v2_slot(16384, 0, 0, 0, 1, 1);
create_v2_file_with_slot_a(&path, slot);
let err = QueueFile::open(&path).unwrap_err();
assert!(
matches!(err, Error::CorruptedFile { .. }),
"expected CorruptedFile, got {err:?}"
);
}
#[test]
fn v2_slot_next_seq_zero() {
let path = temp_path();
let slot = build_v2_slot(V2_INITIAL_LEN, 0, 0, 0, 1, 0 );
create_v2_file_with_slot_a(&path, slot);
let err = QueueFile::open(&path).unwrap_err();
assert!(
matches!(err, Error::CorruptedFile { .. }),
"expected CorruptedFile, got {err:?}"
);
}
#[test]
fn v2_slot_empty_queue_nonzero_pointers() {
let path = temp_path();
let slot = build_v2_slot(V2_INITIAL_LEN, 0, V2_DATA_START, V2_DATA_START, 1, 1);
create_v2_file_with_slot_a(&path, slot);
let err = QueueFile::open(&path).unwrap_err();
assert!(
matches!(err, Error::CorruptedFile { .. }),
"expected CorruptedFile, got {err:?}"
);
}
#[test]
fn v2_slot_nonempty_queue_zero_pointer() {
let path = temp_path();
let slot = build_v2_slot(V2_INITIAL_LEN, 1, 0, 0, 1, 2);
create_v2_file_with_slot_a(&path, slot);
let err = QueueFile::open(&path).unwrap_err();
assert!(
matches!(err, Error::CorruptedFile { .. }),
"expected CorruptedFile, got {err:?}"
);
}
#[test]
fn v2_slot_first_position_out_of_bounds() {
let path = temp_path();
let slot = build_v2_slot(V2_INITIAL_LEN, 1, 100, 100, 1, 2);
create_v2_file_with_slot_a(&path, slot);
let err = QueueFile::open(&path).unwrap_err();
assert!(
matches!(err, Error::CorruptedFile { .. }),
"expected CorruptedFile, got {err:?}"
);
}
#[test]
fn v2_slot_last_position_out_of_bounds() {
let path = temp_path();
let slot = build_v2_slot(V2_INITIAL_LEN, 1, V2_DATA_START, V2_INITIAL_LEN, 1, 2);
create_v2_file_with_slot_a(&path, slot);
let err = QueueFile::open(&path).unwrap_err();
assert!(
matches!(err, Error::CorruptedFile { .. }),
"expected CorruptedFile, got {err:?}"
);
}