// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    fuchsia_archive::{ChunkType, DIR_CHUNK_TYPE, DIR_NAMES_CHUNK_TYPE, MAGIC_INDEX_VALUE},
    std::{
        fs::{create_dir_all, File},
        io::Write,
        path::PathBuf,
    },
};

/// Top-level command.
#[derive(argh::FromArgs, Debug)]
struct Opt {
    /// directory to write the invalid FARs.
    #[argh(option)]
    output_dir: PathBuf,
}

fn main() {
    let opt: Opt = argh::from_env();
    create_dir_all(&opt.output_dir).unwrap();

    // Make a file and serialize a sequence of `Serialize` expressions to it.
    macro_rules! make_test_file {
        ( $filename:literal, $( $serializable:expr, )+ ) => {
            let mut file = File::create(opt.output_dir.join($filename)).unwrap();
            $( $serializable.serialize(&mut file); )+
        }
    }

    // The Directory and Directory Names chunks are required, but the chunks themselves can be
    // empty, so the smallest valid FAR is an Index chunk with entries for zero length Directory and
    // Directory Names chunks.
    make_test_file!(
        "invalid-magic-bytes.far",
        0u64,      // invalid magic bytes
        2 * 24u64, // length of index entries in bytes
        IndexEntry { chunk_type: DIR_CHUNK_TYPE, offset: 64, len: 0 },
        IndexEntry { chunk_type: DIR_NAMES_CHUNK_TYPE, offset: 64, len: 0 },
    );

    make_test_file!(
        "index-entries-length-not-a-multiple-of-24-bytes.far",
        MAGIC_INDEX_VALUE,
        2 * 24 + 1u64, // length of index entries in bytes, should be 48
        IndexEntry { chunk_type: DIR_CHUNK_TYPE, offset: 64, len: 0 },
        IndexEntry { chunk_type: DIR_NAMES_CHUNK_TYPE, offset: 64, len: 0 },
    );

    make_test_file!(
        "directory-names-index-entry-before-directory-index-entry.far",
        Index {
            entries: &[
                IndexEntry { chunk_type: DIR_NAMES_CHUNK_TYPE, offset: 64, len: 0 },
                IndexEntry { chunk_type: DIR_CHUNK_TYPE, offset: 64, len: 0 },
            ]
        },
    );

    make_test_file!(
        "two-directory-index-entries.far",
        Index {
            entries: &[
                IndexEntry { chunk_type: DIR_CHUNK_TYPE, offset: 88, len: 0 },
                IndexEntry { chunk_type: DIR_CHUNK_TYPE, offset: 88, len: 0 },
                IndexEntry { chunk_type: DIR_NAMES_CHUNK_TYPE, offset: 88, len: 0 },
            ]
        },
    );

    make_test_file!(
        "two-directory-names-index-entries.far",
        Index {
            entries: &[
                IndexEntry { chunk_type: DIR_CHUNK_TYPE, offset: 88, len: 0 },
                IndexEntry { chunk_type: DIR_NAMES_CHUNK_TYPE, offset: 88, len: 0 },
                IndexEntry { chunk_type: DIR_NAMES_CHUNK_TYPE, offset: 88, len: 0 },
            ]
        },
    );

    make_test_file!(
        "no-directory-index-entry.far",
        Index { entries: &[IndexEntry { chunk_type: DIR_NAMES_CHUNK_TYPE, offset: 40, len: 0 },] },
    );

    make_test_file!(
        "no-directory-names-index-entry.far",
        Index { entries: &[IndexEntry { chunk_type: DIR_CHUNK_TYPE, offset: 40, len: 0 },] },
    );

    make_test_file!(
        "directory-chunk-length-not-a-multiple-of-32-bytes.far",
        Index {
            entries: &[
                IndexEntry {
                    chunk_type: DIR_CHUNK_TYPE,
                    offset: 64,
                    len: 40 /*should be 32*/
                },
                IndexEntry { chunk_type: DIR_NAMES_CHUNK_TYPE, offset: 104, len: 8 },
            ]
        },
        DirectoryEntry { name: (0, 1), content: (4096, 0) },
        PadTo(104), // padding for incorrect directory chunk length
        PathData("a"),
        PadTo(4096),
    );

    make_test_file!(
        "directory-chunk-not-tightly-packed.far",
        Index {
            entries: &[
                IndexEntry {
                    chunk_type: DIR_CHUNK_TYPE,
                    offset: 72, /*should be 64*/
                    len: 32
                },
                IndexEntry { chunk_type: DIR_NAMES_CHUNK_TYPE, offset: 104, len: 8 },
            ]
        },
        PadTo(72), // padding for incorrect directory chunk offset
        DirectoryEntry { name: (0, 1), content: (4096, 0) },
        PathData("a"),
        PadTo(4096),
    );

    make_test_file!(
        "directory-names-chunk-not-tightly-packed.far",
        Index {
            entries: &[
                IndexEntry { chunk_type: DIR_CHUNK_TYPE, offset: 64, len: 32 },
                IndexEntry {
                    chunk_type: DIR_NAMES_CHUNK_TYPE,
                    offset: 104, /*should be 96*/
                    len: 8
                },
            ]
        },
        DirectoryEntry { name: (0, 1), content: (4096, 0) },
        PadTo(104), // padding for incorrect directory names chunk offset
        PathData("a"),
        PadTo(4096),
    );

    make_test_file!(
        "path-data-offset-too-large.far",
        Index {
            entries: &[
                IndexEntry { chunk_type: DIR_CHUNK_TYPE, offset: 64, len: 32 },
                IndexEntry { chunk_type: DIR_NAMES_CHUNK_TYPE, offset: 96, len: 8 },
            ]
        },
        DirectoryEntry { name: (9 /*should be 0*/, 1), content: (4096, 0) },
        PathData("a"),
        PadTo(4096),
    );

    make_test_file!(
        "path-data-length-too-large.far",
        Index {
            entries: &[
                IndexEntry { chunk_type: DIR_CHUNK_TYPE, offset: 64, len: 32 },
                IndexEntry { chunk_type: DIR_NAMES_CHUNK_TYPE, offset: 96, len: 8 },
            ]
        },
        DirectoryEntry { name: (0, 9 /*should be 1*/), content: (4096, 0) },
        PathData("a"),
        PadTo(4096),
    );

    make_test_file!(
        "directory-entries-not-sorted.far",
        Index {
            entries: &[
                IndexEntry { chunk_type: DIR_CHUNK_TYPE, offset: 64, len: 64 },
                IndexEntry { chunk_type: DIR_NAMES_CHUNK_TYPE, offset: 128, len: 8 },
            ]
        },
        DirectoryEntry { name: (0, 1), content: (4096, 0) },
        DirectoryEntry { name: (1, 1), content: (4096, 0) },
        PathData("ba"), // b > a
        PadTo(4096),
    );

    make_test_file!(
        "directory-entries-with-same-name.far",
        Index {
            entries: &[
                IndexEntry { chunk_type: DIR_CHUNK_TYPE, offset: 64, len: 64 },
                IndexEntry { chunk_type: DIR_NAMES_CHUNK_TYPE, offset: 128, len: 8 },
            ]
        },
        DirectoryEntry { name: (0, 1), content: (4096, 0) },
        DirectoryEntry { name: (1, 1), content: (4096, 0) },
        PathData("aa"), // both names are "a"
        PadTo(4096),
    );

    make_test_file!(
        "directory-names-chunk-length-not-a-multiple-of-8-bytes.far",
        Index {
            entries: &[
                IndexEntry { chunk_type: DIR_CHUNK_TYPE, offset: 64, len: 32 },
                IndexEntry {
                    chunk_type: DIR_NAMES_CHUNK_TYPE,
                    offset: 96,
                    len: 1 /*should be 8*/
                },
            ]
        },
        DirectoryEntry { name: (0, 1), content: (4096, 0) },
        PathData("a"),
        PadTo(4096),
    );

    make_test_file!(
        "directory-names-chunk-before-directory-chunk.far",
        Index {
            entries: &[
                IndexEntry {
                    chunk_type: DIR_CHUNK_TYPE,
                    offset: 72, /*should be 64*/
                    len: 32
                },
                IndexEntry {
                    chunk_type: DIR_NAMES_CHUNK_TYPE,
                    offset: 64, /*should be 96*/
                    len: 8
                },
            ]
        },
        PathData("a"),
        DirectoryEntry { name: (0, 1), content: (4096, 0) },
        PadTo(4096),
    );

    make_test_file!(
        "directory-names-chunk-overlaps-directory-chunk.far",
        Index {
            entries: &[
                IndexEntry { chunk_type: DIR_CHUNK_TYPE, offset: 64, len: 32 },
                IndexEntry {
                    chunk_type: DIR_NAMES_CHUNK_TYPE,
                    offset: 88, /*should be 96*/
                    len: 8
                },
            ]
        },
        DirectoryEntry { name: (0, 1), content: (4096, 0) },
        PathData("a"),
        PadTo(4096),
    );

    make_test_file!(
        "zero-length-name.far",
        Index {
            entries: &[
                IndexEntry { chunk_type: DIR_CHUNK_TYPE, offset: 64, len: 32 },
                IndexEntry { chunk_type: DIR_NAMES_CHUNK_TYPE, offset: 96, len: 8 },
            ]
        },
        DirectoryEntry { name: (0, 0 /*zero length name*/), content: (4096, 0) },
        PathData(""),
        PadTo(4096),
    );

    make_test_file!(
        "name-with-null-character.far",
        Index {
            entries: &[
                IndexEntry { chunk_type: DIR_CHUNK_TYPE, offset: 64, len: 32 },
                IndexEntry { chunk_type: DIR_NAMES_CHUNK_TYPE, offset: 96, len: 8 },
            ]
        },
        DirectoryEntry { name: (0, 1), content: (4096, 0) },
        PathData("\0"), // null character in name
        PadTo(4096),
    );

    make_test_file!(
        "name-with-leading-slash.far",
        Index {
            entries: &[
                IndexEntry { chunk_type: DIR_CHUNK_TYPE, offset: 64, len: 32 },
                IndexEntry { chunk_type: DIR_NAMES_CHUNK_TYPE, offset: 96, len: 8 },
            ]
        },
        DirectoryEntry { name: (0, 2), content: (4096, 0) },
        PathData("/a"), // name starts with slash
        PadTo(4096),
    );

    make_test_file!(
        "name-with-trailing-slash.far",
        Index {
            entries: &[
                IndexEntry { chunk_type: DIR_CHUNK_TYPE, offset: 64, len: 32 },
                IndexEntry { chunk_type: DIR_NAMES_CHUNK_TYPE, offset: 96, len: 8 },
            ]
        },
        DirectoryEntry { name: (0, 2), content: (4096, 0) },
        PathData("a/"), // name ends with slash
        PadTo(4096),
    );

    make_test_file!(
        "name-with-empty-segment.far",
        Index {
            entries: &[
                IndexEntry { chunk_type: DIR_CHUNK_TYPE, offset: 64, len: 32 },
                IndexEntry { chunk_type: DIR_NAMES_CHUNK_TYPE, offset: 96, len: 8 },
            ]
        },
        DirectoryEntry { name: (0, 4), content: (4096, 0) },
        PathData("a//a"), // name with empty segment
        PadTo(4096),
    );

    make_test_file!(
        "name-with-dot-segment.far",
        Index {
            entries: &[
                IndexEntry { chunk_type: DIR_CHUNK_TYPE, offset: 64, len: 32 },
                IndexEntry { chunk_type: DIR_NAMES_CHUNK_TYPE, offset: 96, len: 8 },
            ]
        },
        DirectoryEntry { name: (0, 5), content: (4096, 0) },
        PathData("a/./a"), // name with dot segment
        PadTo(4096),
    );

    make_test_file!(
        "name-with-dot-dot-segment.far",
        Index {
            entries: &[
                IndexEntry { chunk_type: DIR_CHUNK_TYPE, offset: 64, len: 32 },
                IndexEntry { chunk_type: DIR_NAMES_CHUNK_TYPE, offset: 96, len: 8 },
            ]
        },
        DirectoryEntry { name: (0, 6), content: (4096, 0) },
        PathData("a/../a"), // name with dot segment
        PadTo(4096),
    );

    make_test_file!(
        "content-chunk-starts-early.far",
        Index {
            entries: &[
                IndexEntry { chunk_type: DIR_CHUNK_TYPE, offset: 64, len: 32 },
                IndexEntry { chunk_type: DIR_NAMES_CHUNK_TYPE, offset: 96, len: 8 },
            ]
        },
        DirectoryEntry { name: (0, 1), content: (4095 /*should be 4096*/, 1) },
        PathData("a"),
        PadTo(4095),
        "a",
    );

    make_test_file!(
        "content-chunk-starts-late.far",
        Index {
            entries: &[
                IndexEntry { chunk_type: DIR_CHUNK_TYPE, offset: 64, len: 32 },
                IndexEntry { chunk_type: DIR_NAMES_CHUNK_TYPE, offset: 96, len: 8 },
            ]
        },
        DirectoryEntry { name: (0, 1), content: (4097 /*should be 4096*/, 1) },
        PathData("a"),
        PadTo(4097),
        "a",
        PadTo(8192),
    );

    make_test_file!(
        "second-content-chunk-starts-early.far",
        Index {
            entries: &[
                IndexEntry { chunk_type: DIR_CHUNK_TYPE, offset: 64, len: 64 },
                IndexEntry { chunk_type: DIR_NAMES_CHUNK_TYPE, offset: 128, len: 8 },
            ]
        },
        DirectoryEntry { name: (0, 1), content: (4096, 1) },
        DirectoryEntry { name: (1, 1), content: (2 * 4096 - 1 /*should be 2*4096*/, 1) },
        PathData("ab"),
        PadTo(4096 * 2),
    );

    make_test_file!(
        "second-content-chunk-starts-late.far",
        Index {
            entries: &[
                IndexEntry { chunk_type: DIR_CHUNK_TYPE, offset: 64, len: 64 },
                IndexEntry { chunk_type: DIR_NAMES_CHUNK_TYPE, offset: 128, len: 8 },
            ]
        },
        DirectoryEntry { name: (0, 1), content: (4096, 1) },
        DirectoryEntry { name: (1, 1), content: (2 * 4096 + 1 /*should be 2*4096*/, 1) },
        PathData("ab"),
        PadTo(4096 * 3),
    );

    make_test_file!(
        "content-chunk-not-zero-padded.far",
        Index {
            entries: &[
                IndexEntry { chunk_type: DIR_CHUNK_TYPE, offset: 64, len: 32 },
                IndexEntry { chunk_type: DIR_NAMES_CHUNK_TYPE, offset: 96, len: 8 },
            ]
        },
        DirectoryEntry { name: (0, 1), content: (4096, 1) },
        PathData("a"),
        PadTo(4096),
        "a", // Note content chunk not padded to 4192 alignment
    );

    make_test_file!(
        "content-chunk-overlap.far",
        Index {
            entries: &[
                IndexEntry { chunk_type: DIR_CHUNK_TYPE, offset: 64, len: 64 },
                IndexEntry { chunk_type: DIR_NAMES_CHUNK_TYPE, offset: 128, len: 8 },
            ]
        },
        DirectoryEntry { name: (0, 1), content: (4096, 4097) },
        DirectoryEntry { name: (1, 1), content: (2 * 4096 /*should be 3*4096*/, 1) },
        PathData("ab"),
        PadTo(4096 * 3),
    );

    make_test_file!(
        "content-chunk-not-tightly-packed.far",
        Index {
            entries: &[
                IndexEntry { chunk_type: DIR_CHUNK_TYPE, offset: 64, len: 32 },
                IndexEntry { chunk_type: DIR_NAMES_CHUNK_TYPE, offset: 96, len: 8 },
            ]
        },
        DirectoryEntry { name: (0, 1), content: (4096 * 2 /*should be 4096*/, 1) },
        PathData("a"),
        PadTo(4096 * 3),
    );

    make_test_file!(
        "content-chunk-offset-past-end-of-file.far",
        Index {
            entries: &[
                IndexEntry { chunk_type: DIR_CHUNK_TYPE, offset: 64, len: 32 },
                IndexEntry { chunk_type: DIR_NAMES_CHUNK_TYPE, offset: 96, len: 8 },
            ]
        },
        DirectoryEntry { name: (0, 1), content: (4096, 0) },
        PathData("a"),
        // Note no padding
    );
}

trait Serialize {
    fn serialize(&self, f: &mut File);
}

impl Serialize for u64 {
    fn serialize(&self, f: &mut File) {
        f.write_all(&self.to_le_bytes()).unwrap()
    }
}

impl Serialize for u32 {
    fn serialize(&self, f: &mut File) {
        f.write_all(&self.to_le_bytes()).unwrap()
    }
}

impl Serialize for u16 {
    fn serialize(&self, f: &mut File) {
        f.write_all(&self.to_le_bytes()).unwrap()
    }
}

impl Serialize for [u8; 8] {
    fn serialize(&self, f: &mut File) {
        f.write_all(&self[..]).unwrap()
    }
}

impl Serialize for &str {
    fn serialize(&self, f: &mut File) {
        f.write_all(self.as_bytes()).unwrap()
    }
}

// Pad the file with zeroes until it is size self.0, panics if file size already >= self.0
struct PadTo(u64);
impl Serialize for PadTo {
    fn serialize(&self, f: &mut File) {
        let n = f.metadata().unwrap().len();
        assert!(
            self.0 >= n,
            "unnecessary padding requested, asked to pad to {} but file size is already {}",
            self.0,
            n
        );
        let zeroes = vec![0u8; (self.0 - n) as usize];
        f.write_all(&zeroes).unwrap();
    }
}

impl Serialize for Vec<u8> {
    fn serialize(&self, f: &mut File) {
        f.write_all(self).unwrap();
    }
}

struct IndexEntry {
    chunk_type: ChunkType,
    offset: u64,
    len: u64,
}
impl Serialize for IndexEntry {
    fn serialize(&self, f: &mut File) {
        self.chunk_type.serialize(f);
        self.offset.serialize(f);
        self.len.serialize(f);
    }
}

struct Index {
    entries: &'static [IndexEntry],
}
impl Serialize for Index {
    fn serialize(&self, f: &mut File) {
        MAGIC_INDEX_VALUE.serialize(f);
        ((self.entries.len() * 24) as u64).serialize(f);
        for entry in self.entries {
            entry.serialize(f);
        }
    }
}

struct DirectoryEntry {
    // (offset, length)
    name: (u32, u16),
    // (offset, length)
    content: (u64, u64),
}
impl Serialize for DirectoryEntry {
    fn serialize(&self, f: &mut File) {
        self.name.0.serialize(f);
        self.name.1.serialize(f);
        0u16.serialize(f); // padding
        self.content.0.serialize(f);
        self.content.1.serialize(f);
        0u64.serialize(f); // padding
    }
}

// Zero pads to 8 byte alignment
struct PathData(&'static str);
impl Serialize for PathData {
    fn serialize(&self, f: &mut File) {
        self.0.serialize(f);
        let padding = vec![0u8; 8 - (self.0.len() % 8)];
        padding.serialize(f);
    }
}
