use crate::core::document::Document;
use crate::format::detect::read_bytes_raw;
use crate::format::time;
use crate::format::types::*;

const TAR_BLOCK: u64 = 512;
const TAR_NAME_LEN: usize = 100;
const TAR_MODE_LEN: usize = 8;
const TAR_UID_LEN: usize = 8;
const TAR_GID_LEN: usize = 8;
const TAR_SIZE_LEN: usize = 12;
const TAR_MTIME_LEN: usize = 12;
const TAR_CHECKSUM_LEN: usize = 8;
const TAR_LINKNAME_LEN: usize = 100;
const TAR_MAGIC_LEN: usize = 6;
const TAR_VERSION_LEN: usize = 2;
const TAR_UNAME_LEN: usize = 32;
const TAR_GNAME_LEN: usize = 32;
const TAR_DEV_LEN: usize = 8;
const TAR_PREFIX_LEN: usize = 155;

pub fn detect(doc: &mut Document) -> Option<FormatDef> {
    detect_with_cap(doc, super::super::detect::DEFAULT_ENTRY_CAP)
}

pub fn detect_with_cap(doc: &mut Document, entry_cap: usize) -> Option<FormatDef> {
    if doc.len() < TAR_BLOCK {
        return None;
    }

    let first = read_header(doc, 0)?;
    if !is_recognizable_tar_header(&first) {
        return None;
    }

    let mut structs = Vec::new();
    let mut offset = 0_u64;
    let mut entry_idx = 0_usize;
    let mut more_remain = false;

    while offset.saturating_add(TAR_BLOCK) <= doc.len() {
        if entry_idx >= entry_cap.max(1) {
            if let Some(next) = read_header(doc, offset) {
                if !is_zero_block(&next) && is_recognizable_tar_header(&next) {
                    more_remain = true;
                }
            }
            break;
        }

        let Some(header) = read_header(doc, offset) else {
            break;
        };
        if is_zero_block(&header) {
            break;
        }
        if !is_recognizable_tar_header(&header) {
            break;
        }

        let mode_field = parse_octal_field(&header[100..108]);
        let size_field = parse_octal_field(&header[124..136]);
        let mtime_field = parse_octal_field(&header[136..148]);
        let size = size_field.unwrap_or(0);
        let checksum_ok = checksum_matches(&header);
        let typeflag = header[156];
        let name = entry_name(&header);
        let header_end = offset.saturating_add(TAR_BLOCK);
        let data_end = header_end.saturating_add(size);
        let truncated = data_end > doc.len();

        let mut fields = vec![
            FieldDef {
                name: "name".into(),
                offset: 0,
                field_type: FieldType::Utf8(TAR_NAME_LEN),
                description: "Entry path".into(),
                editable: true,
            },
            FieldDef {
                name: "mode".into(),
                offset: 100,
                field_type: FieldType::custom_display(
                    TAR_MODE_LEN,
                    tar_mode_display(mode_field),
                    encode_tar_mode,
                ),
                description: "File mode decoded from TAR octal text".into(),
                editable: true,
            },
            FieldDef {
                name: "uid".into(),
                offset: 108,
                field_type: FieldType::Utf8(TAR_UID_LEN),
                description: "Owner user ID (octal text)".into(),
                editable: true,
            },
            FieldDef {
                name: "gid".into(),
                offset: 116,
                field_type: FieldType::Utf8(TAR_GID_LEN),
                description: "Owner group ID (octal text)".into(),
                editable: true,
            },
            FieldDef {
                name: "size".into(),
                offset: 124,
                field_type: FieldType::custom_display(
                    TAR_SIZE_LEN,
                    tar_size_display(size_field),
                    encode_tar_size,
                ),
                description: "Entry size decoded from TAR octal text".into(),
                editable: true,
            },
            FieldDef {
                name: "mtime".into(),
                offset: 136,
                field_type: FieldType::custom_display(
                    TAR_MTIME_LEN,
                    tar_mtime_display(mtime_field),
                    encode_tar_mtime,
                ),
                description: "Modification time decoded from TAR octal Unix seconds".into(),
                editable: true,
            },
            FieldDef {
                name: "checksum".into(),
                offset: 148,
                field_type: FieldType::Utf8(TAR_CHECKSUM_LEN),
                description: "Header checksum (octal text)".into(),
                editable: true,
            },
            FieldDef {
                name: "typeflag".into(),
                offset: 156,
                field_type: FieldType::custom_enum(FieldType::U8, typeflag_variants()),
                description: "Entry type".into(),
                editable: true,
            },
            FieldDef {
                name: "linkname".into(),
                offset: 157,
                field_type: FieldType::Utf8(TAR_LINKNAME_LEN),
                description: "Target name for link entries".into(),
                editable: true,
            },
            FieldDef {
                name: "magic".into(),
                offset: 257,
                field_type: FieldType::Utf8(TAR_MAGIC_LEN),
                description: "USTAR magic".into(),
                editable: true,
            },
            FieldDef {
                name: "version".into(),
                offset: 263,
                field_type: FieldType::Utf8(TAR_VERSION_LEN),
                description: "USTAR version".into(),
                editable: true,
            },
            FieldDef {
                name: "uname".into(),
                offset: 265,
                field_type: FieldType::Utf8(TAR_UNAME_LEN),
                description: "Owner user name".into(),
                editable: true,
            },
            FieldDef {
                name: "gname".into(),
                offset: 297,
                field_type: FieldType::Utf8(TAR_GNAME_LEN),
                description: "Owner group name".into(),
                editable: true,
            },
            FieldDef {
                name: "devmajor".into(),
                offset: 329,
                field_type: FieldType::Utf8(TAR_DEV_LEN),
                description: "Major device number (octal text)".into(),
                editable: true,
            },
            FieldDef {
                name: "devminor".into(),
                offset: 337,
                field_type: FieldType::Utf8(TAR_DEV_LEN),
                description: "Minor device number (octal text)".into(),
                editable: true,
            },
            FieldDef {
                name: "prefix".into(),
                offset: 345,
                field_type: FieldType::Utf8(TAR_PREFIX_LEN),
                description: "Path prefix".into(),
                editable: true,
            },
        ];

        if !truncated && size > 0 && typeflag_has_data(typeflag) {
            fields.push(FieldDef {
                name: "file_data".into(),
                offset: TAR_BLOCK,
                field_type: FieldType::DataRange(size),
                description: "Entry payload bytes".into(),
                editable: false,
            });
        }

        structs.push(StructDef {
            name: if truncated {
                let label = format!(
                    "Entry {}: {} [{}] (truncated)",
                    entry_idx,
                    display_name(&name),
                    typeflag_label(typeflag)
                );
                if checksum_ok {
                    label
                } else {
                    format!("{label} [checksum mismatch]")
                }
            } else {
                let label = format!(
                    "Entry {}: {} [{}]",
                    entry_idx,
                    display_name(&name),
                    typeflag_label(typeflag)
                );
                if checksum_ok {
                    label
                } else {
                    format!("{label} [checksum mismatch]")
                }
            },
            base_offset: offset,
            fields,
            children: vec![],
        });

        entry_idx += 1;
        if truncated {
            break;
        }

        offset = header_end.saturating_add(pad_to_block(size));
    }

    if structs.is_empty() {
        return None;
    }

    if more_remain {
        structs.push(StructDef {
            name: format!(
                "… more entries beyond {} (use `:insp more` to load more)",
                entry_idx
            ),
            base_offset: offset,
            fields: vec![],
            children: vec![],
        });
    }

    Some(FormatDef {
        name: "TAR".to_string(),
        structs,
    })
}

fn read_header(doc: &mut Document, offset: u64) -> Option<Vec<u8>> {
    read_bytes_raw(doc, offset, TAR_BLOCK as usize)
}

fn is_recognizable_tar_header(header: &[u8]) -> bool {
    !is_zero_block(header) && has_ustar_magic(header)
}

fn has_ustar_magic(header: &[u8]) -> bool {
    header.len() >= TAR_BLOCK as usize
        && header.get(257..262).is_some_and(|magic| magic == b"ustar")
}

fn checksum_matches(header: &[u8]) -> bool {
    let Some(stored) = parse_octal_field(header.get(148..156).unwrap_or(&[])) else {
        return false;
    };
    compute_checksum(header) == stored
}

fn compute_checksum(header: &[u8]) -> u64 {
    header
        .iter()
        .enumerate()
        .map(|(idx, byte)| {
            if (148..156).contains(&idx) {
                b' ' as u64
            } else {
                *byte as u64
            }
        })
        .sum()
}

fn parse_octal_field(raw: &[u8]) -> Option<u64> {
    let end = raw.iter().position(|&byte| byte == 0).unwrap_or(raw.len());
    let text = std::str::from_utf8(&raw[..end]).ok()?.trim();
    if text.is_empty() {
        Some(0)
    } else {
        u64::from_str_radix(text, 8).ok()
    }
}

fn tar_mode_display(value: Option<u64>) -> String {
    match value {
        Some(value) => format!("0o{value:07o} ({})", symbolic_mode(value)),
        None => "invalid octal mode".into(),
    }
}

fn tar_size_display(value: Option<u64>) -> String {
    match value {
        Some(value) => format!("{value} bytes (0o{value:o})"),
        None => "invalid octal size".into(),
    }
}

fn tar_mtime_display(value: Option<u64>) -> String {
    match value {
        Some(value) if value <= u32::MAX as u64 => {
            format!("{} (0o{value:o})", time::format_unix_utc(value as u32))
        }
        Some(value) => format!("{value} seconds since Unix epoch (0o{value:o})"),
        None => "invalid octal mtime".into(),
    }
}

fn encode_tar_mode(input: &str) -> Result<Vec<u8>, String> {
    let input = input.trim();
    let value = parse_symbolic_mode(input).unwrap_or_else(|| parse_tar_integer(input, 8))?;
    encode_tar_octal_field(value, TAR_MODE_LEN)
}

fn encode_tar_size(input: &str) -> Result<Vec<u8>, String> {
    let value = parse_tar_integer(input, 10)?;
    encode_tar_octal_field(value, TAR_SIZE_LEN)
}

fn encode_tar_mtime(input: &str) -> Result<Vec<u8>, String> {
    let input = input.trim();
    let head = input
        .split_once(" (")
        .map(|(head, _)| head.trim_end())
        .unwrap_or(input);
    let value = if head.contains('-') || head.contains('T') || head.contains(':') {
        time::parse_unix_utc(head)? as u64
    } else {
        parse_tar_integer(head, 10)?
    };
    encode_tar_octal_field(value, TAR_MTIME_LEN)
}

fn encode_tar_octal_field(value: u64, width: usize) -> Result<Vec<u8>, String> {
    let digits = width.saturating_sub(1);
    let text = format!("{value:0digits$o}");
    if text.len() > digits {
        return Err(format!(
            "octal value does not fit in {width}-byte TAR field"
        ));
    }
    let mut bytes = text.into_bytes();
    bytes.push(0);
    Ok(bytes)
}

fn parse_tar_integer(input: &str, default_radix: u32) -> Result<u64, String> {
    let token = numeric_token(input);
    if token.is_empty() {
        return Err("value is empty".into());
    }
    if let Some(octal) = token
        .strip_prefix("0o")
        .or_else(|| token.strip_prefix("0O"))
    {
        u64::from_str_radix(octal, 8).map_err(|err| format!("invalid octal value: {err}"))
    } else if let Some(hex) = token
        .strip_prefix("0x")
        .or_else(|| token.strip_prefix("0X"))
    {
        u64::from_str_radix(hex, 16).map_err(|err| format!("invalid hex value: {err}"))
    } else {
        u64::from_str_radix(token, default_radix)
            .map_err(|err| format!("invalid numeric value: {err}"))
    }
}

fn numeric_token(input: &str) -> &str {
    let trimmed = input.trim();
    trimmed
        .split([' ', '\t', '(', '[', '{'])
        .next()
        .unwrap_or(trimmed)
}

fn symbolic_mode(value: u64) -> String {
    let bits = value & 0o7777;
    let mut text = String::with_capacity(9);
    text.push(if bits & 0o400 != 0 { 'r' } else { '-' });
    text.push(if bits & 0o200 != 0 { 'w' } else { '-' });
    text.push(special_exec(bits, 0o100, 0o4000, 's', 'S'));
    text.push(if bits & 0o040 != 0 { 'r' } else { '-' });
    text.push(if bits & 0o020 != 0 { 'w' } else { '-' });
    text.push(special_exec(bits, 0o010, 0o2000, 's', 'S'));
    text.push(if bits & 0o004 != 0 { 'r' } else { '-' });
    text.push(if bits & 0o002 != 0 { 'w' } else { '-' });
    text.push(special_exec(bits, 0o001, 0o1000, 't', 'T'));
    text
}

fn special_exec(bits: u64, exec: u64, special: u64, set_exec: char, set_no_exec: char) -> char {
    match (bits & exec != 0, bits & special != 0) {
        (true, true) => set_exec,
        (false, true) => set_no_exec,
        (true, false) => 'x',
        (false, false) => '-',
    }
}

fn parse_symbolic_mode(input: &str) -> Option<Result<u64, String>> {
    let text = input.trim();
    let text = match text.len() {
        9 => text,
        10 => &text[1..],
        _ => return None,
    };
    let bytes = text.as_bytes();
    if bytes.len() != 9 {
        return None;
    }

    Some((|| {
        let mut value = 0_u64;
        parse_perm(bytes[0], b'r', 0o400, &mut value, "user read")?;
        parse_perm(bytes[1], b'w', 0o200, &mut value, "user write")?;
        parse_exec(
            bytes[2],
            0o100,
            0o4000,
            b's',
            b'S',
            &mut value,
            "user execute",
        )?;
        parse_perm(bytes[3], b'r', 0o040, &mut value, "group read")?;
        parse_perm(bytes[4], b'w', 0o020, &mut value, "group write")?;
        parse_exec(
            bytes[5],
            0o010,
            0o2000,
            b's',
            b'S',
            &mut value,
            "group execute",
        )?;
        parse_perm(bytes[6], b'r', 0o004, &mut value, "other read")?;
        parse_perm(bytes[7], b'w', 0o002, &mut value, "other write")?;
        parse_exec(
            bytes[8],
            0o001,
            0o1000,
            b't',
            b'T',
            &mut value,
            "other execute",
        )?;
        Ok(value)
    })())
}

fn parse_perm(byte: u8, expected: u8, bit: u64, value: &mut u64, name: &str) -> Result<(), String> {
    match byte {
        b'-' => Ok(()),
        found if found == expected => {
            *value |= bit;
            Ok(())
        }
        _ => Err(format!("invalid symbolic mode bit for {name}")),
    }
}

fn parse_exec(
    byte: u8,
    exec_bit: u64,
    special_bit: u64,
    special_exec: u8,
    special_no_exec: u8,
    value: &mut u64,
    name: &str,
) -> Result<(), String> {
    match byte {
        b'-' => Ok(()),
        b'x' => {
            *value |= exec_bit;
            Ok(())
        }
        found if found == special_exec => {
            *value |= exec_bit | special_bit;
            Ok(())
        }
        found if found == special_no_exec => {
            *value |= special_bit;
            Ok(())
        }
        _ => Err(format!("invalid symbolic mode bit for {name}")),
    }
}

fn entry_name(header: &[u8]) -> String {
    let name = trim_tar_text(&header[..TAR_NAME_LEN]);
    let prefix = trim_tar_text(&header[345..500]);
    if prefix.is_empty() {
        name
    } else if name.is_empty() {
        prefix
    } else {
        format!("{prefix}/{name}")
    }
}

fn trim_tar_text(raw: &[u8]) -> String {
    let end = raw.iter().position(|&byte| byte == 0).unwrap_or(raw.len());
    String::from_utf8_lossy(&raw[..end])
        .trim_end_matches(' ')
        .to_string()
}

fn display_name(name: &str) -> &str {
    if name.is_empty() {
        "<unnamed>"
    } else {
        name
    }
}

fn typeflag_has_data(typeflag: u8) -> bool {
    !matches!(typeflag, b'1' | b'2' | b'3' | b'4' | b'5' | b'6')
}

fn typeflag_variants() -> Vec<(u64, String)> {
    vec![
        (0, "Regular file".into()),
        (b'0' as u64, "Regular file".into()),
        (b'1' as u64, "Hard link".into()),
        (b'2' as u64, "Symlink".into()),
        (b'3' as u64, "Character device".into()),
        (b'4' as u64, "Block device".into()),
        (b'5' as u64, "Directory".into()),
        (b'6' as u64, "FIFO".into()),
        (b'7' as u64, "Contiguous file".into()),
        (b'g' as u64, "PAX global header".into()),
        (b'x' as u64, "PAX extended header".into()),
        (b'L' as u64, "GNU long name".into()),
        (b'K' as u64, "GNU long link".into()),
    ]
}

fn typeflag_label(typeflag: u8) -> &'static str {
    match typeflag {
        0 | b'0' => "file",
        b'1' => "hardlink",
        b'2' => "symlink",
        b'3' => "chardev",
        b'4' => "blockdev",
        b'5' => "dir",
        b'6' => "fifo",
        b'7' => "contig",
        b'g' => "pax-global",
        b'x' => "pax",
        b'L' => "gnu-longname",
        b'K' => "gnu-longlink",
        _ => "other",
    }
}

fn pad_to_block(size: u64) -> u64 {
    let rem = size % TAR_BLOCK;
    if rem == 0 {
        size
    } else {
        size + (TAR_BLOCK - rem)
    }
}

fn is_zero_block(bytes: &[u8]) -> bool {
    bytes.iter().all(|&byte| byte == 0)
}

#[cfg(test)]
mod tests {
    use std::fs;

    use tempfile::tempdir;

    use super::{detect, detect_with_cap, TAR_BLOCK};
    use crate::config::Config;
    use crate::core::document::Document;
    use crate::format;
    use crate::format::edit::encode_value;
    use crate::format::types::{CustomCodec, FieldType};

    struct TarEntry<'a> {
        name: &'a str,
        typeflag: u8,
        data: &'a [u8],
        prefix: &'a str,
    }

    fn write_tar(path: &std::path::Path, bytes: &[u8]) -> Document {
        fs::write(path, bytes).unwrap();
        Document::open(path, &Config::default()).unwrap()
    }

    fn build_tar(entries: &[TarEntry<'_>]) -> Vec<u8> {
        let mut bytes = Vec::new();
        for entry in entries {
            let mut header = [0_u8; TAR_BLOCK as usize];
            write_bytes(&mut header[0..100], entry.name.as_bytes());
            write_octal(&mut header[100..108], 0o644);
            write_octal(&mut header[108..116], 0);
            write_octal(&mut header[116..124], 0);
            write_octal(&mut header[124..136], entry.data.len() as u64);
            write_octal(&mut header[136..148], 0);
            header[148..156].fill(b' ');
            header[156] = entry.typeflag;
            header[257..263].copy_from_slice(b"ustar\0");
            header[263..265].copy_from_slice(b"00");
            write_bytes(&mut header[265..297], b"root");
            write_bytes(&mut header[297..329], b"root");
            write_bytes(&mut header[345..500], entry.prefix.as_bytes());
            let checksum: u64 = header.iter().map(|&byte| byte as u64).sum();
            write_checksum(&mut header[148..156], checksum);
            bytes.extend_from_slice(&header);
            bytes.extend_from_slice(entry.data);
            let padded = super::pad_to_block(entry.data.len() as u64) as usize;
            bytes.resize(bytes.len() + (padded - entry.data.len()), 0);
        }
        bytes.resize(bytes.len() + (TAR_BLOCK as usize * 2), 0);
        bytes
    }

    fn write_bytes(dst: &mut [u8], src: &[u8]) {
        let len = src.len().min(dst.len());
        dst[..len].copy_from_slice(&src[..len]);
    }

    fn write_octal(dst: &mut [u8], value: u64) {
        dst.fill(0);
        let digits = dst.len().saturating_sub(1);
        let text = format!("{value:0digits$o}");
        dst[..digits].copy_from_slice(text.as_bytes());
        dst[digits] = 0;
    }

    fn write_checksum(dst: &mut [u8], checksum: u64) {
        let text = format!("{checksum:06o}\0 ");
        dst.copy_from_slice(text.as_bytes());
    }

    #[test]
    fn detects_entries_and_pagination_marker() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("sample.tar");
        let entries = [
            TarEntry {
                name: "bin/hello",
                typeflag: b'0',
                data: b"abc",
                prefix: "",
            },
            TarEntry {
                name: "notes.txt",
                typeflag: b'0',
                data: b"xyz",
                prefix: "",
            },
            TarEntry {
                name: "docs",
                typeflag: b'5',
                data: b"",
                prefix: "",
            },
        ];
        let mut doc = write_tar(&path, &build_tar(&entries));

        let def = detect_with_cap(&mut doc, 2).expect("tar detected");
        assert_eq!(def.name, "TAR");
        assert!(def.structs[0].name.contains("bin/hello"));
        assert!(def.structs[1].name.contains("notes.txt"));
        assert!(def
            .structs
            .last()
            .is_some_and(|last| last.name.contains("more entries")));
    }

    #[test]
    fn file_data_range_points_to_payload_bytes() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("payload.tar");
        let entries = [TarEntry {
            name: "payload.bin",
            typeflag: b'0',
            data: b"hello",
            prefix: "",
        }];
        let mut doc = write_tar(&path, &build_tar(&entries));

        let def = detect(&mut doc).expect("tar detected");
        let structs = format::parse::parse_format(&def, &mut doc).expect("parse succeeds");
        let entry = structs
            .iter()
            .find(|structure| structure.name.contains("payload.bin"))
            .expect("entry exists");
        let field = entry
            .fields
            .iter()
            .find(|field| field.def.name == "file_data")
            .expect("file_data field");
        assert_eq!(field.abs_offset, TAR_BLOCK);
        assert_eq!(field.size, 5);
    }

    #[test]
    fn tar_octal_fields_are_readable_and_editable() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("readable.tar");
        let entries = [TarEntry {
            name: "payload.bin",
            typeflag: b'0',
            data: b"hello",
            prefix: "",
        }];
        let mut doc = write_tar(&path, &build_tar(&entries));

        let def = detect(&mut doc).expect("tar detected");
        let entry = &def.structs[0];
        let mode = entry
            .fields
            .iter()
            .find(|field| field.name == "mode")
            .expect("mode field");
        let size = entry
            .fields
            .iter()
            .find(|field| field.name == "size")
            .expect("size field");
        let mtime = entry
            .fields
            .iter()
            .find(|field| field.name == "mtime")
            .expect("mtime field");

        let FieldType::Custom(custom) = &mode.field_type else {
            panic!("mode should use custom display");
        };
        let CustomCodec::Display { display, .. } = &custom.codec else {
            panic!("mode should use display codec");
        };
        assert_eq!(display, "0o0000644 (rw-r--r--)");
        assert_eq!(
            encode_value(&mode.field_type, display).unwrap(),
            super::encode_tar_octal_field(0o644, super::TAR_MODE_LEN).unwrap()
        );
        assert_eq!(
            encode_value(&mode.field_type, "rwxr-xr-x").unwrap(),
            super::encode_tar_octal_field(0o755, super::TAR_MODE_LEN).unwrap()
        );

        let FieldType::Custom(custom) = &size.field_type else {
            panic!("size should use custom display");
        };
        let CustomCodec::Display { display, .. } = &custom.codec else {
            panic!("size should use display codec");
        };
        assert_eq!(display, "5 bytes (0o5)");
        assert_eq!(
            encode_value(&size.field_type, display).unwrap(),
            super::encode_tar_octal_field(5, super::TAR_SIZE_LEN).unwrap()
        );

        let FieldType::Custom(custom) = &mtime.field_type else {
            panic!("mtime should use custom display");
        };
        let CustomCodec::Display { display, .. } = &custom.codec else {
            panic!("mtime should use display codec");
        };
        assert_eq!(display, "1970-01-01T00:00:00Z (0o0)");
        assert_eq!(
            encode_value(&mtime.field_type, display).unwrap(),
            super::encode_tar_octal_field(0, super::TAR_MTIME_LEN).unwrap()
        );
    }

    #[test]
    fn keeps_detecting_tar_when_checksum_is_stale() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("edited.tar");
        let entries = [TarEntry {
            name: "payload.bin",
            typeflag: b'0',
            data: b"hello",
            prefix: "",
        }];
        let mut doc = write_tar(&path, &build_tar(&entries));
        doc.replace_display_byte(0, b'q').unwrap();

        let def = detect(&mut doc).expect("tar should remain detectable");
        assert!(def.structs[0].name.contains("checksum mismatch"));
    }

    #[test]
    fn rejects_missing_ustar_magic() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("bad.tar");
        let entries = [TarEntry {
            name: "payload.bin",
            typeflag: b'0',
            data: b"hello",
            prefix: "",
        }];
        let mut bytes = build_tar(&entries);
        bytes[257..262].copy_from_slice(b"xxxxx");
        let mut doc = write_tar(&path, &bytes);
        assert!(detect(&mut doc).is_none());
    }
}
