//! Helper for writing PE files.
use alloc::string::String;
use alloc::vec::Vec;
use core::mem;

use crate::endian::{LittleEndian as LE, *};
use crate::pe;
use crate::write::util;
use crate::write::{Error, Result, WritableBuffer};

/// A helper for writing PE files.
///
/// Writing uses a two phase approach. The first phase reserves file ranges and virtual
/// address ranges for everything in the order that they will be written.
///
/// The second phase writes everything out in order. Thus the caller must ensure writing
/// is in the same order that file ranges were reserved.
#[allow(missing_debug_implementations)]
pub struct Writer<'a> {
    is_64: bool,
    section_alignment: u32,
    file_alignment: u32,

    buffer: &'a mut dyn WritableBuffer,
    len: u32,
    virtual_len: u32,
    headers_len: u32,

    code_address: u32,
    data_address: u32,
    code_len: u32,
    data_len: u32,
    bss_len: u32,

    nt_headers_offset: u32,
    data_directories: Vec<DataDirectory>,
    section_header_num: u16,
    sections: Vec<Section>,

    symbol_offset: u32,
    symbol_num: u32,

    reloc_blocks: Vec<RelocBlock>,
    relocs: Vec<U16<LE>>,
    reloc_offset: u32,
}

impl<'a> Writer<'a> {
    /// Create a new `Writer`.
    pub fn new(
        is_64: bool,
        section_alignment: u32,
        file_alignment: u32,
        buffer: &'a mut dyn WritableBuffer,
    ) -> Self {
        Writer {
            is_64,
            section_alignment,
            file_alignment,

            buffer,
            len: 0,
            virtual_len: 0,
            headers_len: 0,

            code_address: 0,
            data_address: 0,
            code_len: 0,
            data_len: 0,
            bss_len: 0,

            nt_headers_offset: 0,
            data_directories: Vec::new(),
            section_header_num: 0,
            sections: Vec::new(),

            symbol_offset: 0,
            symbol_num: 0,

            reloc_blocks: Vec::new(),
            relocs: Vec::new(),
            reloc_offset: 0,
        }
    }

    /// Return the current virtual address size that has been reserved.
    ///
    /// This is only valid after section headers have been reserved.
    pub fn virtual_len(&self) -> u32 {
        self.virtual_len
    }

    /// Reserve a virtual address range with the given size.
    ///
    /// The reserved length will be increased to match the section alignment.
    ///
    /// Returns the aligned offset of the start of the range.
    pub fn reserve_virtual(&mut self, len: u32) -> u32 {
        let offset = self.virtual_len;
        self.virtual_len += len;
        self.virtual_len = util::align_u32(self.virtual_len, self.section_alignment);
        offset
    }

    /// Reserve up to the given virtual address.
    ///
    /// The reserved length will be increased to match the section alignment.
    pub fn reserve_virtual_until(&mut self, address: u32) {
        debug_assert!(self.virtual_len <= address);
        self.virtual_len = util::align_u32(address, self.section_alignment);
    }

    /// Return the current file length that has been reserved.
    pub fn reserved_len(&self) -> u32 {
        self.len
    }

    /// Return the current file length that has been written.
    #[allow(clippy::len_without_is_empty)]
    pub fn len(&self) -> usize {
        self.buffer.len()
    }

    /// Reserve a file range with the given size and starting alignment.
    ///
    /// Returns the aligned offset of the start of the range.
    pub fn reserve(&mut self, len: u32, align_start: u32) -> u32 {
        if len == 0 {
            return self.len;
        }
        self.reserve_align(align_start);
        let offset = self.len;
        self.len += len;
        offset
    }

    /// Reserve a file range with the given size and using the file alignment.
    ///
    /// Returns the aligned offset of the start of the range.
    pub fn reserve_file(&mut self, len: u32) -> u32 {
        self.reserve(len, self.file_alignment)
    }

    /// Write data.
    pub fn write(&mut self, data: &[u8]) {
        self.buffer.write_bytes(data);
    }

    /// Reserve alignment padding bytes.
    pub fn reserve_align(&mut self, align_start: u32) {
        self.len = util::align_u32(self.len, align_start);
    }

    /// Write alignment padding bytes.
    pub fn write_align(&mut self, align_start: u32) {
        util::write_align(self.buffer, align_start as usize);
    }

    /// Write padding up to the next multiple of file alignment.
    pub fn write_file_align(&mut self) {
        self.write_align(self.file_alignment);
    }

    /// Reserve the file range up to the given file offset.
    pub fn reserve_until(&mut self, offset: u32) {
        debug_assert!(self.len <= offset);
        self.len = offset;
    }

    /// Write padding up to the given file offset.
    pub fn pad_until(&mut self, offset: u32) {
        debug_assert!(self.buffer.len() <= offset as usize);
        self.buffer.resize(offset as usize);
    }

    /// Reserve the range for the DOS header.
    ///
    /// This must be at the start of the file.
    ///
    /// When writing, you may use `write_custom_dos_header` or `write_empty_dos_header`.
    pub fn reserve_dos_header(&mut self) {
        debug_assert_eq!(self.len, 0);
        self.reserve(mem::size_of::<pe::ImageDosHeader>() as u32, 1);
    }

    /// Write a custom DOS header.
    ///
    /// This must be at the start of the file.
    pub fn write_custom_dos_header(&mut self, dos_header: &pe::ImageDosHeader) -> Result<()> {
        debug_assert_eq!(self.buffer.len(), 0);

        // Start writing.
        self.buffer
            .reserve(self.len as usize)
            .map_err(|_| Error(String::from("Cannot allocate buffer")))?;

        self.buffer.write(dos_header);
        Ok(())
    }

    /// Write the DOS header for a file without a stub.
    ///
    /// This must be at the start of the file.
    ///
    /// Uses default values for all fields.
    pub fn write_empty_dos_header(&mut self) -> Result<()> {
        self.write_custom_dos_header(&pe::ImageDosHeader {
            e_magic: U16::new(LE, pe::IMAGE_DOS_SIGNATURE),
            e_cblp: U16::new(LE, 0),
            e_cp: U16::new(LE, 0),
            e_crlc: U16::new(LE, 0),
            e_cparhdr: U16::new(LE, 0),
            e_minalloc: U16::new(LE, 0),
            e_maxalloc: U16::new(LE, 0),
            e_ss: U16::new(LE, 0),
            e_sp: U16::new(LE, 0),
            e_csum: U16::new(LE, 0),
            e_ip: U16::new(LE, 0),
            e_cs: U16::new(LE, 0),
            e_lfarlc: U16::new(LE, 0),
            e_ovno: U16::new(LE, 0),
            e_res: [U16::new(LE, 0); 4],
            e_oemid: U16::new(LE, 0),
            e_oeminfo: U16::new(LE, 0),
            e_res2: [U16::new(LE, 0); 10],
            e_lfanew: U32::new(LE, self.nt_headers_offset),
        })
    }

    /// Reserve a fixed DOS header and stub.
    ///
    /// Use `reserve_dos_header` and `reserve` if you need a custom stub.
    pub fn reserve_dos_header_and_stub(&mut self) {
        self.reserve_dos_header();
        self.reserve(64, 1);
    }

    /// Write a fixed DOS header and stub.
    ///
    /// Use `write_custom_dos_header` and `write` if you need a custom stub.
    pub fn write_dos_header_and_stub(&mut self) -> Result<()> {
        self.write_custom_dos_header(&pe::ImageDosHeader {
            e_magic: U16::new(LE, pe::IMAGE_DOS_SIGNATURE),
            e_cblp: U16::new(LE, 0x90),
            e_cp: U16::new(LE, 3),
            e_crlc: U16::new(LE, 0),
            e_cparhdr: U16::new(LE, 4),
            e_minalloc: U16::new(LE, 0),
            e_maxalloc: U16::new(LE, 0xffff),
            e_ss: U16::new(LE, 0),
            e_sp: U16::new(LE, 0xb8),
            e_csum: U16::new(LE, 0),
            e_ip: U16::new(LE, 0),
            e_cs: U16::new(LE, 0),
            e_lfarlc: U16::new(LE, 0x40),
            e_ovno: U16::new(LE, 0),
            e_res: [U16::new(LE, 0); 4],
            e_oemid: U16::new(LE, 0),
            e_oeminfo: U16::new(LE, 0),
            e_res2: [U16::new(LE, 0); 10],
            e_lfanew: U32::new(LE, self.nt_headers_offset),
        })?;

        #[rustfmt::skip]
        self.buffer.write_bytes(&[
            0x0e, 0x1f, 0xba, 0x0e, 0x00, 0xb4, 0x09, 0xcd,
            0x21, 0xb8, 0x01, 0x4c, 0xcd, 0x21, 0x54, 0x68,
            0x69, 0x73, 0x20, 0x70, 0x72, 0x6f, 0x67, 0x72,
            0x61, 0x6d, 0x20, 0x63, 0x61, 0x6e, 0x6e, 0x6f,
            0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x75, 0x6e,
            0x20, 0x69, 0x6e, 0x20, 0x44, 0x4f, 0x53, 0x20,
            0x6d, 0x6f, 0x64, 0x65, 0x2e, 0x0d, 0x0d, 0x0a,
            0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        ]);

        Ok(())
    }

    fn nt_headers_size(&self) -> u32 {
        if self.is_64 {
            mem::size_of::<pe::ImageNtHeaders64>() as u32
        } else {
            mem::size_of::<pe::ImageNtHeaders32>() as u32
        }
    }

    fn optional_header_size(&self) -> u32 {
        let size = if self.is_64 {
            mem::size_of::<pe::ImageOptionalHeader64>() as u32
        } else {
            mem::size_of::<pe::ImageOptionalHeader32>() as u32
        };
        size + self.data_directories.len() as u32 * mem::size_of::<pe::ImageDataDirectory>() as u32
    }

    /// Return the offset of the NT headers, if reserved.
    pub fn nt_headers_offset(&self) -> u32 {
        self.nt_headers_offset
    }

    /// Reserve the range for the NT headers.
    pub fn reserve_nt_headers(&mut self, data_directory_num: usize) {
        debug_assert_eq!(self.nt_headers_offset, 0);
        self.nt_headers_offset = self.reserve(self.nt_headers_size(), 8);
        self.data_directories = vec![DataDirectory::default(); data_directory_num];
        self.reserve(
            data_directory_num as u32 * mem::size_of::<pe::ImageDataDirectory>() as u32,
            1,
        );
    }

    /// Set the virtual address and size of a data directory.
    pub fn set_data_directory(&mut self, index: usize, virtual_address: u32, size: u32) {
        self.data_directories[index] = DataDirectory {
            virtual_address,
            size,
        }
    }

    /// Write the NT headers.
    pub fn write_nt_headers(&mut self, nt_headers: NtHeaders) {
        self.pad_until(self.nt_headers_offset);
        self.buffer.write(&U32::new(LE, pe::IMAGE_NT_SIGNATURE));
        let file_header = pe::ImageFileHeader {
            machine: U16::new(LE, nt_headers.machine),
            number_of_sections: U16::new(LE, self.section_header_num),
            time_date_stamp: U32::new(LE, nt_headers.time_date_stamp),
            pointer_to_symbol_table: U32::new(LE, self.symbol_offset),
            number_of_symbols: U32::new(LE, self.symbol_num),
            size_of_optional_header: U16::new(LE, self.optional_header_size() as u16),
            characteristics: U16::new(LE, nt_headers.characteristics),
        };
        self.buffer.write(&file_header);
        if self.is_64 {
            let optional_header = pe::ImageOptionalHeader64 {
                magic: U16::new(LE, pe::IMAGE_NT_OPTIONAL_HDR64_MAGIC),
                major_linker_version: nt_headers.major_linker_version,
                minor_linker_version: nt_headers.minor_linker_version,
                size_of_code: U32::new(LE, self.code_len),
                size_of_initialized_data: U32::new(LE, self.data_len),
                size_of_uninitialized_data: U32::new(LE, self.bss_len),
                address_of_entry_point: U32::new(LE, nt_headers.address_of_entry_point),
                base_of_code: U32::new(LE, self.code_address),
                image_base: U64::new(LE, nt_headers.image_base),
                section_alignment: U32::new(LE, self.section_alignment),
                file_alignment: U32::new(LE, self.file_alignment),
                major_operating_system_version: U16::new(
                    LE,
                    nt_headers.major_operating_system_version,
                ),
                minor_operating_system_version: U16::new(
                    LE,
                    nt_headers.minor_operating_system_version,
                ),
                major_image_version: U16::new(LE, nt_headers.major_image_version),
                minor_image_version: U16::new(LE, nt_headers.minor_image_version),
                major_subsystem_version: U16::new(LE, nt_headers.major_subsystem_version),
                minor_subsystem_version: U16::new(LE, nt_headers.minor_subsystem_version),
                win32_version_value: U32::new(LE, 0),
                size_of_image: U32::new(LE, self.virtual_len),
                size_of_headers: U32::new(LE, self.headers_len),
                check_sum: U32::new(LE, 0),
                subsystem: U16::new(LE, nt_headers.subsystem),
                dll_characteristics: U16::new(LE, nt_headers.dll_characteristics),
                size_of_stack_reserve: U64::new(LE, nt_headers.size_of_stack_reserve),
                size_of_stack_commit: U64::new(LE, nt_headers.size_of_stack_commit),
                size_of_heap_reserve: U64::new(LE, nt_headers.size_of_heap_reserve),
                size_of_heap_commit: U64::new(LE, nt_headers.size_of_heap_commit),
                loader_flags: U32::new(LE, 0),
                number_of_rva_and_sizes: U32::new(LE, self.data_directories.len() as u32),
            };
            self.buffer.write(&optional_header);
        } else {
            let optional_header = pe::ImageOptionalHeader32 {
                magic: U16::new(LE, pe::IMAGE_NT_OPTIONAL_HDR32_MAGIC),
                major_linker_version: nt_headers.major_linker_version,
                minor_linker_version: nt_headers.minor_linker_version,
                size_of_code: U32::new(LE, self.code_len),
                size_of_initialized_data: U32::new(LE, self.data_len),
                size_of_uninitialized_data: U32::new(LE, self.bss_len),
                address_of_entry_point: U32::new(LE, nt_headers.address_of_entry_point),
                base_of_code: U32::new(LE, self.code_address),
                base_of_data: U32::new(LE, self.data_address),
                image_base: U32::new(LE, nt_headers.image_base as u32),
                section_alignment: U32::new(LE, self.section_alignment),
                file_alignment: U32::new(LE, self.file_alignment),
                major_operating_system_version: U16::new(
                    LE,
                    nt_headers.major_operating_system_version,
                ),
                minor_operating_system_version: U16::new(
                    LE,
                    nt_headers.minor_operating_system_version,
                ),
                major_image_version: U16::new(LE, nt_headers.major_image_version),
                minor_image_version: U16::new(LE, nt_headers.minor_image_version),
                major_subsystem_version: U16::new(LE, nt_headers.major_subsystem_version),
                minor_subsystem_version: U16::new(LE, nt_headers.minor_subsystem_version),
                win32_version_value: U32::new(LE, 0),
                size_of_image: U32::new(LE, self.virtual_len),
                size_of_headers: U32::new(LE, self.headers_len),
                check_sum: U32::new(LE, 0),
                subsystem: U16::new(LE, nt_headers.subsystem),
                dll_characteristics: U16::new(LE, nt_headers.dll_characteristics),
                size_of_stack_reserve: U32::new(LE, nt_headers.size_of_stack_reserve as u32),
                size_of_stack_commit: U32::new(LE, nt_headers.size_of_stack_commit as u32),
                size_of_heap_reserve: U32::new(LE, nt_headers.size_of_heap_reserve as u32),
                size_of_heap_commit: U32::new(LE, nt_headers.size_of_heap_commit as u32),
                loader_flags: U32::new(LE, 0),
                number_of_rva_and_sizes: U32::new(LE, self.data_directories.len() as u32),
            };
            self.buffer.write(&optional_header);
        }

        for dir in &self.data_directories {
            self.buffer.write(&pe::ImageDataDirectory {
                virtual_address: U32::new(LE, dir.virtual_address),
                size: U32::new(LE, dir.size),
            })
        }
    }

    /// Reserve the section headers.
    ///
    /// The number of reserved section headers must be the same as the number of sections that
    /// are later reserved.
    // TODO: change this to a maximum number of sections?
    pub fn reserve_section_headers(&mut self, section_header_num: u16) {
        debug_assert_eq!(self.section_header_num, 0);
        self.section_header_num = section_header_num;
        self.reserve(
            u32::from(section_header_num) * mem::size_of::<pe::ImageSectionHeader>() as u32,
            1,
        );
        // Padding before sections must be included in headers_len.
        self.reserve_align(self.file_alignment);
        self.headers_len = self.len;
        self.reserve_virtual(self.len);
    }

    /// Write the section headers.
    ///
    /// This uses information that was recorded when the sections were reserved.
    pub fn write_section_headers(&mut self) {
        debug_assert_eq!(self.section_header_num as usize, self.sections.len());
        for section in &self.sections {
            let section_header = pe::ImageSectionHeader {
                name: section.name,
                virtual_size: U32::new(LE, section.range.virtual_size),
                virtual_address: U32::new(LE, section.range.virtual_address),
                size_of_raw_data: U32::new(LE, section.range.file_size),
                pointer_to_raw_data: U32::new(LE, section.range.file_offset),
                pointer_to_relocations: U32::new(LE, 0),
                pointer_to_linenumbers: U32::new(LE, 0),
                number_of_relocations: U16::new(LE, 0),
                number_of_linenumbers: U16::new(LE, 0),
                characteristics: U32::new(LE, section.characteristics),
            };
            self.buffer.write(&section_header);
        }
    }

    /// Reserve a section.
    ///
    /// Returns the file range and virtual address range that are reserved
    /// for the section.
    pub fn reserve_section(
        &mut self,
        name: [u8; 8],
        characteristics: u32,
        virtual_size: u32,
        data_size: u32,
    ) -> SectionRange {
        let virtual_address = self.reserve_virtual(virtual_size);

        // Padding after section must be included in section file size.
        let file_size = util::align_u32(data_size, self.file_alignment);
        let file_offset = if file_size != 0 {
            self.reserve(file_size, self.file_alignment)
        } else {
            0
        };

        // Sizes in optional header use the virtual size with the file alignment.
        let aligned_virtual_size = util::align_u32(virtual_size, self.file_alignment);
        if characteristics & pe::IMAGE_SCN_CNT_CODE != 0 {
            if self.code_address == 0 {
                self.code_address = virtual_address;
            }
            self.code_len += aligned_virtual_size;
        } else if characteristics & pe::IMAGE_SCN_CNT_INITIALIZED_DATA != 0 {
            if self.data_address == 0 {
                self.data_address = virtual_address;
            }
            self.data_len += aligned_virtual_size;
        } else if characteristics & pe::IMAGE_SCN_CNT_UNINITIALIZED_DATA != 0 {
            if self.data_address == 0 {
                self.data_address = virtual_address;
            }
            self.bss_len += aligned_virtual_size;
        }

        let range = SectionRange {
            virtual_address,
            virtual_size,
            file_offset,
            file_size,
        };
        self.sections.push(Section {
            name,
            characteristics,
            range,
        });
        range
    }

    /// Write the data for a section.
    pub fn write_section(&mut self, offset: u32, data: &[u8]) {
        if data.is_empty() {
            return;
        }
        self.pad_until(offset);
        self.write(data);
        self.write_align(self.file_alignment);
    }

    /// Reserve a `.text` section.
    ///
    /// Contains executable code.
    pub fn reserve_text_section(&mut self, size: u32) -> SectionRange {
        self.reserve_section(
            *b".text\0\0\0",
            pe::IMAGE_SCN_CNT_CODE | pe::IMAGE_SCN_MEM_EXECUTE | pe::IMAGE_SCN_MEM_READ,
            size,
            size,
        )
    }

    /// Reserve a `.data` section.
    ///
    /// Contains initialized data.
    ///
    /// May also contain uninitialized data if `virtual_size` is greater than `data_size`.
    pub fn reserve_data_section(&mut self, virtual_size: u32, data_size: u32) -> SectionRange {
        self.reserve_section(
            *b".data\0\0\0",
            pe::IMAGE_SCN_CNT_INITIALIZED_DATA | pe::IMAGE_SCN_MEM_READ | pe::IMAGE_SCN_MEM_WRITE,
            virtual_size,
            data_size,
        )
    }

    /// Reserve a `.rdata` section.
    ///
    /// Contains read-only initialized data.
    pub fn reserve_rdata_section(&mut self, size: u32) -> SectionRange {
        self.reserve_section(
            *b".rdata\0\0",
            pe::IMAGE_SCN_CNT_INITIALIZED_DATA | pe::IMAGE_SCN_MEM_READ,
            size,
            size,
        )
    }

    /// Reserve a `.bss` section.
    ///
    /// Contains uninitialized data.
    pub fn reserve_bss_section(&mut self, size: u32) -> SectionRange {
        self.reserve_section(
            *b".bss\0\0\0\0",
            pe::IMAGE_SCN_CNT_UNINITIALIZED_DATA | pe::IMAGE_SCN_MEM_READ | pe::IMAGE_SCN_MEM_WRITE,
            size,
            0,
        )
    }

    /// Reserve an `.idata` section.
    ///
    /// Contains import tables. Note that it is permissible to store import tables in a different
    /// section.
    ///
    /// This also sets the `pe::IMAGE_DIRECTORY_ENTRY_IMPORT` data directory.
    pub fn reserve_idata_section(&mut self, size: u32) -> SectionRange {
        let range = self.reserve_section(
            *b".idata\0\0",
            pe::IMAGE_SCN_CNT_INITIALIZED_DATA | pe::IMAGE_SCN_MEM_READ | pe::IMAGE_SCN_MEM_WRITE,
            size,
            size,
        );
        let dir = &mut self.data_directories[pe::IMAGE_DIRECTORY_ENTRY_IMPORT];
        debug_assert_eq!(dir.virtual_address, 0);
        *dir = DataDirectory {
            virtual_address: range.virtual_address,
            size,
        };
        range
    }

    /// Reserve an `.edata` section.
    ///
    /// Contains export tables.
    ///
    /// This also sets the `pe::IMAGE_DIRECTORY_ENTRY_EXPORT` data directory.
    pub fn reserve_edata_section(&mut self, size: u32) -> SectionRange {
        let range = self.reserve_section(
            *b".edata\0\0",
            pe::IMAGE_SCN_CNT_INITIALIZED_DATA | pe::IMAGE_SCN_MEM_READ,
            size,
            size,
        );
        let dir = &mut self.data_directories[pe::IMAGE_DIRECTORY_ENTRY_EXPORT];
        debug_assert_eq!(dir.virtual_address, 0);
        *dir = DataDirectory {
            virtual_address: range.virtual_address,
            size,
        };
        range
    }

    /// Reserve a `.pdata` section.
    ///
    /// Contains exception information.
    ///
    /// This also sets the `pe::IMAGE_DIRECTORY_ENTRY_EXCEPTION` data directory.
    pub fn reserve_pdata_section(&mut self, size: u32) -> SectionRange {
        let range = self.reserve_section(
            *b".pdata\0\0",
            pe::IMAGE_SCN_CNT_INITIALIZED_DATA | pe::IMAGE_SCN_MEM_READ,
            size,
            size,
        );
        let dir = &mut self.data_directories[pe::IMAGE_DIRECTORY_ENTRY_EXCEPTION];
        debug_assert_eq!(dir.virtual_address, 0);
        *dir = DataDirectory {
            virtual_address: range.virtual_address,
            size,
        };
        range
    }

    /// Reserve a `.xdata` section.
    ///
    /// Contains exception information.
    pub fn reserve_xdata_section(&mut self, size: u32) -> SectionRange {
        self.reserve_section(
            *b".xdata\0\0",
            pe::IMAGE_SCN_CNT_INITIALIZED_DATA | pe::IMAGE_SCN_MEM_READ,
            size,
            size,
        )
    }

    /// Reserve a `.rsrc` section.
    ///
    /// Contains the resource directory.
    ///
    /// This also sets the `pe::IMAGE_DIRECTORY_ENTRY_RESOURCE` data directory.
    pub fn reserve_rsrc_section(&mut self, size: u32) -> SectionRange {
        let range = self.reserve_section(
            *b".rsrc\0\0\0",
            pe::IMAGE_SCN_CNT_INITIALIZED_DATA | pe::IMAGE_SCN_MEM_READ,
            size,
            size,
        );
        let dir = &mut self.data_directories[pe::IMAGE_DIRECTORY_ENTRY_RESOURCE];
        debug_assert_eq!(dir.virtual_address, 0);
        *dir = DataDirectory {
            virtual_address: range.virtual_address,
            size,
        };
        range
    }

    /// Add a base relocation.
    ///
    /// `typ` must be one of the `IMAGE_REL_BASED_*` constants.
    pub fn add_reloc(&mut self, mut virtual_address: u32, typ: u16) {
        let reloc = U16::new(LE, typ << 12 | (virtual_address & 0xfff) as u16);
        virtual_address &= !0xfff;
        if let Some(block) = self.reloc_blocks.last_mut() {
            if block.virtual_address == virtual_address {
                self.relocs.push(reloc);
                block.count += 1;
                return;
            }
            // Blocks must have an even number of relocations.
            if block.count & 1 != 0 {
                self.relocs.push(U16::new(LE, 0));
                block.count += 1;
            }
            debug_assert!(block.virtual_address < virtual_address);
        }
        self.relocs.push(reloc);
        self.reloc_blocks.push(RelocBlock {
            virtual_address,
            count: 1,
        });
    }

    /// Return true if a base relocation has been added.
    pub fn has_relocs(&mut self) -> bool {
        !self.relocs.is_empty()
    }

    /// Reserve a `.reloc` section.
    ///
    /// This contains the base relocations that were added with `add_reloc`.
    ///
    /// This also sets the `pe::IMAGE_DIRECTORY_ENTRY_BASERELOC` data directory.
    pub fn reserve_reloc_section(&mut self) -> SectionRange {
        if let Some(block) = self.reloc_blocks.last_mut() {
            // Blocks must have an even number of relocations.
            if block.count & 1 != 0 {
                self.relocs.push(U16::new(LE, 0));
                block.count += 1;
            }
        }
        let size = self.reloc_blocks.iter().map(RelocBlock::size).sum();
        let range = self.reserve_section(
            *b".reloc\0\0",
            pe::IMAGE_SCN_CNT_INITIALIZED_DATA
                | pe::IMAGE_SCN_MEM_READ
                | pe::IMAGE_SCN_MEM_DISCARDABLE,
            size,
            size,
        );
        let dir = &mut self.data_directories[pe::IMAGE_DIRECTORY_ENTRY_BASERELOC];
        debug_assert_eq!(dir.virtual_address, 0);
        *dir = DataDirectory {
            virtual_address: range.virtual_address,
            size,
        };
        self.reloc_offset = range.file_offset;
        range
    }

    /// Write a `.reloc` section.
    ///
    /// This contains the base relocations that were added with `add_reloc`.
    pub fn write_reloc_section(&mut self) {
        if self.reloc_offset == 0 {
            return;
        }
        self.pad_until(self.reloc_offset);

        let mut total = 0;
        for block in &self.reloc_blocks {
            self.buffer.write(&pe::ImageBaseRelocation {
                virtual_address: U32::new(LE, block.virtual_address),
                size_of_block: U32::new(LE, block.size()),
            });
            self.buffer
                .write_slice(&self.relocs[total..][..block.count as usize]);
            total += block.count as usize;
        }
        debug_assert_eq!(total, self.relocs.len());

        self.write_align(self.file_alignment);
    }

    /// Reserve the certificate table.
    ///
    /// This also sets the `pe::IMAGE_DIRECTORY_ENTRY_SECURITY` data directory.
    // TODO: reserve individual certificates
    pub fn reserve_certificate_table(&mut self, size: u32) {
        let size = util::align_u32(size, 8);
        let offset = self.reserve(size, 8);
        let dir = &mut self.data_directories[pe::IMAGE_DIRECTORY_ENTRY_SECURITY];
        debug_assert_eq!(dir.virtual_address, 0);
        *dir = DataDirectory {
            virtual_address: offset,
            size,
        };
    }

    /// Write the certificate table.
    // TODO: write individual certificates
    pub fn write_certificate_table(&mut self, data: &[u8]) {
        let dir = self.data_directories[pe::IMAGE_DIRECTORY_ENTRY_SECURITY];
        self.pad_until(dir.virtual_address);
        self.write(data);
        self.pad_until(dir.virtual_address + dir.size);
    }
}

/// Information required for writing [`pe::ImageNtHeaders32`] or [`pe::ImageNtHeaders64`].
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct NtHeaders {
    // ImageFileHeader
    pub machine: u16,
    pub time_date_stamp: u32,
    pub characteristics: u16,
    // ImageOptionalHeader
    pub major_linker_version: u8,
    pub minor_linker_version: u8,
    pub address_of_entry_point: u32,
    pub image_base: u64,
    pub major_operating_system_version: u16,
    pub minor_operating_system_version: u16,
    pub major_image_version: u16,
    pub minor_image_version: u16,
    pub major_subsystem_version: u16,
    pub minor_subsystem_version: u16,
    pub subsystem: u16,
    pub dll_characteristics: u16,
    pub size_of_stack_reserve: u64,
    pub size_of_stack_commit: u64,
    pub size_of_heap_reserve: u64,
    pub size_of_heap_commit: u64,
}

#[derive(Default, Clone, Copy)]
struct DataDirectory {
    virtual_address: u32,
    size: u32,
}

/// Information required for writing [`pe::ImageSectionHeader`].
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct Section {
    pub name: [u8; pe::IMAGE_SIZEOF_SHORT_NAME],
    pub characteristics: u32,
    pub range: SectionRange,
}

/// The file range and virtual address range for a section.
#[allow(missing_docs)]
#[derive(Debug, Default, Clone, Copy)]
pub struct SectionRange {
    pub virtual_address: u32,
    pub virtual_size: u32,
    pub file_offset: u32,
    pub file_size: u32,
}

struct RelocBlock {
    virtual_address: u32,
    count: u32,
}

impl RelocBlock {
    fn size(&self) -> u32 {
        mem::size_of::<pe::ImageBaseRelocation>() as u32 + self.count * mem::size_of::<u16>() as u32
    }
}
