/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

use std::{
    alloc::{self, Layout},
    cmp::min,
    ffi::{c_char, c_void, CStr},
    ptr::{copy_nonoverlapping, null_mut},
    sync::Mutex,
};

#[cfg(any(target_os = "linux", target_os = "android"))]
use std::arch::global_asm;

pub const ANNOTATION_CONTENTS_EMPTY: u32 = 0;
pub const ANNOTATION_CONTENTS_NSCSTRINGPOINTER: u32 = 1;
pub const ANNOTATION_CONTENTS_CSTRINGPOINTER: u32 = 2;
pub const ANNOTATION_CONTENTS_CSTRING: u32 = 3;
pub const ANNOTATION_CONTENTS_BYTEBUFFER: u32 = 4;
pub const ANNOTATION_CONTENTS_OWNEDBYTEBUFFER: u32 = 5;

pub fn is_annotation_contents(value: u32) -> bool {
    matches!(
        value,
        ANNOTATION_CONTENTS_EMPTY
            | ANNOTATION_CONTENTS_NSCSTRINGPOINTER
            | ANNOTATION_CONTENTS_CSTRINGPOINTER
            | ANNOTATION_CONTENTS_CSTRING
            | ANNOTATION_CONTENTS_BYTEBUFFER
            | ANNOTATION_CONTENTS_OWNEDBYTEBUFFER
    )
}

#[repr(C)]
pub struct Annotation {
    pub id: u32,
    pub contents: u32,
    pub len: usize,
    pub address: usize,
}

impl Drop for Annotation {
    fn drop(&mut self) {
        match self.contents {
            ANNOTATION_CONTENTS_OWNEDBYTEBUFFER if (self.address != 0) && (self.len > 0) => {
                let align = min(usize::next_power_of_two(self.len), 32);
                unsafe {
                    let layout = Layout::from_size_align_unchecked(self.len, align);
                    alloc::dealloc(self.address as *mut u8, layout);
                }
            }
            _ => {
                // Nothing to do
            }
        };
    }
}

pub struct AnnotationTable {
    data: Vec<Annotation>,
    magic_number: u32,
}

impl AnnotationTable {
    const fn new() -> AnnotationTable {
        AnnotationTable {
            data: Vec::new(),
            magic_number: ANNOTATION_TYPE,
        }
    }

    pub const fn verify(&self) -> bool {
        self.magic_number == ANNOTATION_TYPE
    }

    pub fn get_ptr(&self) -> *const Annotation {
        self.data.as_ptr()
    }

    pub fn len(&self) -> usize {
        self.data.len()
    }

    pub fn is_empty(&self) -> bool {
        self.data.is_empty()
    }
}

pub type AnnotationMutex = Mutex<AnnotationTable>;

#[cfg(target_os = "windows")]
#[link_section = "mozannot"]
static MOZANNOTATIONS: AnnotationMutex = Mutex::new(AnnotationTable::new());
#[cfg(any(target_os = "linux", target_os = "android"))]
static MOZANNOTATIONS: AnnotationMutex = Mutex::new(AnnotationTable::new());
#[cfg(target_os = "macos")]
#[link_section = "__DATA,mozannotation"]
static MOZANNOTATIONS: AnnotationMutex = Mutex::new(AnnotationTable::new());

#[no_mangle]
unsafe fn mozannotation_get() -> *const AnnotationMutex {
    &MOZANNOTATIONS as _
}

#[cfg(any(target_os = "linux", target_os = "android"))]
extern "C" {
    pub static MOZANNOTATION_NOTE_REFERENCE: &'static u32;
    pub static __ehdr_start: [u8; 0];
}

#[cfg(target_os = "windows")]
pub const ANNOTATION_SECTION: &[u8; 8] = b"mozannot";

#[cfg(target_os = "macos")]
pub const ANNOTATION_SECTION: &[u8; 16] = b"mozannotation\0\0\0";

// TODO: Use the following constants in the assembly below when constant
// expressions are stabilized: https://github.com/rust-lang/rust/issues/93332
#[cfg(any(target_os = "linux", target_os = "android"))]
const _ANNOTATION_NOTE_ALIGNMENT: u32 = 4;
#[cfg(any(target_os = "linux", target_os = "android"))]
pub const ANNOTATION_NOTE_NAME: &str = "mozannotation";
pub const ANNOTATION_TYPE: u32 = u32::from_le_bytes(*b"MOZA");

// We use the crashpad crash info trick here. We create a program note which
// we'll use to find the location of the MOZANNOTATIONS static. Since program
// headers are always available we'll always be able to find this note in the
// memory of the crashed program, even if it's stripped or the backing file on
// disk has been deleted.
//
// We'll set the note type and name so we can easily recognize it (see the
// constants above). In the note's desc field we'll have the linker store the
// offset between the address of the MOZANNOTATIONS static and the desc field
// itself.
//
// At runtime we'll localize the note in the target process' memory, find the
// address of the `desc` field, load its contents (that is the offset we stored
// at link time) and add them together. The resulting address is the location of
// the MOZANNOTATIONS static in memory.
//
// When elfhack is used, the note might be moved after the aforementioned offset
// is calculated, without it being updated. To compensate for this we store the
// offset between the `ehdr` field and the ELF header. At runtime we can
// use this offset to adjust for the shift of the `desc` field.
#[cfg(all(
    target_pointer_width = "64",
    any(target_os = "linux", target_os = "android")
))]
global_asm!(
    // The section holding the note will be called '.note.moz.annotation'. We
    // create a program note that's allocated ('a' option) in the target binary
    // so that it's loaded into memory.
    "  .section .note.moz.annotation,\"a\",%note",
    // Note alignment must be 4 bytes because that's the default alignment for
    // that section. If a different alignment is chosen the note will end up in
    // its own section which we don't want.
    "  .balign 4", // TODO: _ANNOTATION_NOTE_ALIGNMENT
    "MOZANNOTATION_NOTE:",
    "  .long name_end - name", // size in bytes of the note's name
    "  .long desc_end - desc", // size in bytes of the note's desc field
    "  .long 0x415a4f4d",      // TODO: _ANNOTATION_TYPE, MOZA in reverse
    "name:",
    "  .asciz \"mozannotation\"", // TODO: _ANNOTATION_NOTE_NAME
    "name_end:",
    "  .balign 4", // TODO: _ANNOTATION_NOTE_ALIGNMENT
    "desc:",
    "  .quad {mozannotation_symbol} - desc",
    "ehdr:",
    "  .quad {__ehdr_start} - ehdr",
    "desc_end:",
    "  .size MOZANNOTATION_NOTE, .-MOZANNOTATION_NOTE",
    mozannotation_symbol = sym MOZANNOTATIONS,
    __ehdr_start = sym __ehdr_start
);

// The following global_asm!() expressions for other targets because Rust's
// support for putting statements within expressions is still experimental-only.
// Once https://github.com/rust-lang/rust/issues/15701 is fixed this can be
// folded in the `global_asm!()` statement above.

#[cfg(all(
    target_pointer_width = "32",
    any(target_os = "linux", target_os = "android")
))]
global_asm!(
    "  .section .note.moz.annotation,\"a\",%note",
    "  .balign 4",
    "MOZANNOTATION_NOTE:",
    "  .long name_end - name",
    "  .long desc_end - desc",
    "  .long 0x415a4f4d",
    "name:",
    "  .asciz \"mozannotation\"",
    "name_end:",
    "  .balign 4",
    "desc:",
    "  .long {mozannotation_symbol} - desc",
    "ehdr:",
    "  .long {__ehdr_start} - ehdr",
    "desc_end:",
    "  .size MOZANNOTATION_NOTE, .-MOZANNOTATION_NOTE",
    mozannotation_symbol = sym MOZANNOTATIONS,
    __ehdr_start = sym __ehdr_start
);

#[cfg(all(
    any(target_os = "linux", target_os = "android"),
    not(target_arch = "arm")
))]
global_asm!(
    // MOZANNOTATION_NOTE can't be referenced directly because the relocation
    // used to make the reference may require that the address be 8-byte aligned
    // and notes must have 4-byte alignment.
    "  .section .rodata,\"a\",%progbits",
    "  .balign 8",
    // .globl indicates that it's available to link against other .o files.
    // .hidden indicates that it will not appear in the executable's symbol
    // table.
    "  .globl",
    "  .hidden MOZANNOTATION_NOTE_REFERENCE",
    "  .type MOZANNOTATION_NOTE_REFERENCE, %object",
    "MOZANNOTATION_NOTE_REFERENCE:",
    // The value of this quad isn't important. It exists to reference
    // MOZANNOTATION_NOTE, causing the linker to include the note into the
    // binary linking the mozannotation_client crate. The subtraction from name
    // is a convenience to allow the value to be computed statically.
    "  .quad name - MOZANNOTATION_NOTE",
);

// In theory we could have used the statement above for ARM targets but for some
// reason the current rust compiler rejects the .quad directive. As with the other
// duplicate code above we could replace this with a single conditional line once
// once https://github.com/rust-lang/rust/issues/15701 is fixed.

#[cfg(all(any(target_os = "linux", target_os = "android"), target_arch = "arm"))]
global_asm!(
    "  .section .rodata,\"a\",%progbits",
    "  .balign 8",
    "  .globl",
    "  .hidden MOZANNOTATION_NOTE_REFERENCE",
    "  .type MOZANNOTATION_NOTE_REFERENCE, %object",
    "MOZANNOTATION_NOTE_REFERENCE:",
    "  .long name - MOZANNOTATION_NOTE",
);

/// This structure mirrors the contents of the note declared above in the
/// assembly blocks. It is used to copy the contents of the note out of the
/// target process.
#[cfg(any(target_os = "linux", target_os = "android"))]
#[allow(dead_code)]
#[repr(C, packed(4))]
pub struct MozAnnotationNote {
    pub namesz: u32,
    pub descsz: u32,
    pub note_type: u32,
    pub name: [u8; 16], // "mozannotation" plus padding to next 4-bytes boundary
    pub desc: usize,
    pub ehdr: isize,
}

fn store_annotation<T>(id: u32, contents: u32, len: usize, address: *const T) -> *const T {
    debug_assert!(is_annotation_contents(contents));

    let address = match contents {
        ANNOTATION_CONTENTS_OWNEDBYTEBUFFER => {
            if !address.is_null() && (len > 0) {
                // Copy the contents of this annotation, we'll own the copy
                let align = min(usize::next_power_of_two(len), 32);
                unsafe {
                    let layout = Layout::from_size_align_unchecked(len, align);
                    let src = address as *mut u8;
                    let dst = alloc::alloc(layout);
                    copy_nonoverlapping(src, dst, len);
                    dst
                }
            } else {
                null_mut()
            }
        }
        _ => address as *mut u8,
    };

    let annotations = &mut MOZANNOTATIONS.lock().unwrap().data;
    let old = if let Some(existing) = annotations.iter_mut().find(|e| e.id == id) {
        let old = match existing.contents {
            ANNOTATION_CONTENTS_OWNEDBYTEBUFFER => {
                // If we owned the previous value of this annotation we must free it.
                if (existing.address != 0) && (len > 0) {
                    let align = min(usize::next_power_of_two(len), 32);
                    unsafe {
                        let layout = Layout::from_size_align_unchecked(len, align);
                        alloc::dealloc(existing.address as *mut u8, layout);
                    }
                }
                null_mut::<T>()
            }
            _ => existing.address as *mut T,
        };

        existing.contents = contents;
        existing.len = len;
        existing.address = address as usize;
        old
    } else {
        annotations.push(Annotation {
            id,
            contents,
            len,
            address: address as usize,
        });
        null_mut::<T>()
    };

    old
}

// Dummy definition so that we don't have to import the nsstring crate.
#[allow(non_camel_case_types)]
pub struct nsCString {}

/// Register a pointer to an nsCString string.
///
/// Returns the value of the previously registered annotation or null.
///
/// This function will be exposed to C++
#[no_mangle]
pub extern "C" fn mozannotation_register_nscstring(
    id: u32,
    address: *const nsCString,
) -> *const nsCString {
    store_annotation(
        id,
        ANNOTATION_CONTENTS_NSCSTRINGPOINTER,
        /* len */ 0,
        address,
    )
}

/// Create a copy of the provided string with a specified size that will be
/// owned by the crate, and register a pointer to it.
///
/// This function will be exposed to C++
#[no_mangle]
pub extern "C" fn mozannotation_record_nscstring_from_raw_parts(
    id: u32,
    address: *const u8,
    size: usize,
) {
    store_annotation(id, ANNOTATION_CONTENTS_OWNEDBYTEBUFFER, size, address);
}

/// Register a pointer to a pointer to a nul-terminated string.
///
/// Returns the value of the previously registered annotation or null.
///
/// This function will be exposed to C++
#[no_mangle]
pub extern "C" fn mozannotation_register_cstring_ptr(
    id: u32,
    address: *const *const c_char,
) -> *const *const c_char {
    store_annotation(
        id,
        ANNOTATION_CONTENTS_CSTRINGPOINTER,
        /* len */ 0,
        address,
    )
}

/// Register a pointer to a nul-terminated string.
///
/// Returns the value of the previously registered annotation or null.
///
/// This function will be exposed to C++
#[no_mangle]
pub extern "C" fn mozannotation_register_cstring(id: u32, address: *const c_char) -> *const c_char {
    store_annotation(id, ANNOTATION_CONTENTS_CSTRING, /* len */ 0, address)
}

/// Create a copy of the provided nul-terminated string which will be owned by
/// the crate, and register a pointer to it.
///
/// This function will be exposed to C++
///
/// # Safety
///
/// `address` must point to a valid nul-terminated C string.
#[no_mangle]
pub unsafe extern "C" fn mozannotation_record_cstring(id: u32, address: *const c_char) {
    let len = unsafe { CStr::from_ptr(address).to_bytes().len() };
    store_annotation(id, ANNOTATION_CONTENTS_OWNEDBYTEBUFFER, len, address);
}

/// Register a pointer to a fixed size buffer.
///
/// Returns the value of the previously registered annotation or null.
///
/// This function will be exposed to C++
#[no_mangle]
pub extern "C" fn mozannotation_register_bytebuffer(
    id: u32,
    address: *const c_void,
    size: usize,
) -> *const c_void {
    store_annotation(id, ANNOTATION_CONTENTS_BYTEBUFFER, size, address)
}

/// Create a copy of the provided buffer which will be owned by the crate, and
/// register a pointer to it.
///
/// This function will be exposed to C++
#[no_mangle]
pub extern "C" fn mozannotation_record_bytebuffer(id: u32, address: *const c_void, size: usize) {
    store_annotation(id, ANNOTATION_CONTENTS_OWNEDBYTEBUFFER, size, address);
}

/// Unregister a crash annotation. Returns the previously registered pointer or
/// null if none was present. Return null also if the crate owned the
/// annotations' buffer.
///
/// This function will be exposed to C++
#[no_mangle]
pub extern "C" fn mozannotation_unregister(id: u32) -> *const c_void {
    store_annotation(id, ANNOTATION_CONTENTS_EMPTY, /* len */ 0, null_mut())
}

/// Returns the raw address of an annotation if it has been registered or NULL
/// if it hasn't.
///
/// This function will be exposed to C++
///
/// # Safety
///
/// `contents` must point to a u32-sized integer, and `len` to a usize-sized
/// integer.
#[no_mangle]
pub unsafe extern "C" fn mozannotation_get_contents(
    id: u32,
    contents: *mut u32,
    len: *mut usize,
) -> usize {
    let annotations = &MOZANNOTATIONS.lock().unwrap().data;
    if let Some(annotation) = annotations.iter().find(|e| e.id == id) {
        if annotation.contents == ANNOTATION_CONTENTS_EMPTY {
            return 0;
        }

        unsafe {
            *contents = annotation.contents;
            *len = annotation.len;
        }

        return annotation.address;
    }

    0
}

#[no_mangle]
pub extern "C" fn mozannotation_clear_all() {
    let annotations = &mut MOZANNOTATIONS.lock().unwrap().data;
    annotations.clear();
}
