From a66be66c22bc31526ac35c1cffdb28992a392ccf Mon Sep 17 00:00:00 2001 From: iximeow Date: Sat, 22 Jun 2024 11:03:43 -0700 Subject: move DisplaySink code out from yaxpeax-x86 it was built in-place around yaxpeax-x86, hoisted out once it seemed suitable and could be generalized. yay! also include a Makefile in yaxpeax-arch now to test that various crate feature flag combinations.. work. --- Cargo.toml | 6 +- Makefile | 10 + src/display.rs | 33 ++ src/display/display_sink.rs | 1232 +++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 9 + src/safer_unchecked.rs | 30 ++ src/testkit.rs | 2 + src/testkit/display.rs | 166 ++++++ tests/display.rs | 71 +++ 9 files changed, 1557 insertions(+), 2 deletions(-) create mode 100644 Makefile create mode 100644 src/display/display_sink.rs create mode 100644 src/safer_unchecked.rs create mode 100644 src/testkit.rs create mode 100644 src/testkit/display.rs create mode 100644 tests/display.rs diff --git a/Cargo.toml b/Cargo.toml index c6d3cbf..e1d3357 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,9 +23,11 @@ thiserror = "1.0.26" lto = true [features] -default = ["std", "use-serde", "colors", "address-parse"] +default = ["std", "alloc", "use-serde", "colors", "address-parse"] -std = [] +std = ["alloc"] + +alloc = [] # enables the (optional) use of Serde for bounds on # Arch and Arch::Address diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..aa38b87 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +test: test-std test-no-std test-serde-no-std test-alloc-no-std + +test-std: + cargo test +test-no-std: + cargo test --no-default-features +test-serde-no-std: + cargo test --no-default-features --features "serde" +test-alloc-no-std: + cargo test --no-default-features --features "alloc" diff --git a/src/display.rs b/src/display.rs index 789919e..77f6ba9 100644 --- a/src/display.rs +++ b/src/display.rs @@ -1,9 +1,32 @@ +// allow use of deprecated items in this module since some functions using `SignedHexDisplay` still +// exist here +#![allow(deprecated)] + use crate::YaxColors; use core::fmt; use core::num::Wrapping; use core::ops::Neg; +mod display_sink; + +pub use display_sink::{DisplaySink, FmtSink, InstructionTextSink}; + +/// translate a byte in range `[0, 15]` to a lowercase base-16 digit. +/// +/// if `c` is in range, the output is always valid as the sole byte in a utf-8 string. if `c` is out +/// of range, the returned character might not be a valid single-byte utf-8 codepoint. +fn u8_to_hex(c: u8) -> u8 { + // this conditional branch is faster than a lookup for... most architectures (especially x86 + // with cmov) + if c < 10 { + b'0' + c + } else { + b'a' + c - 10 + } +} + +#[deprecated(since="0.3.0", note="format_number_i32 does not optimize as expected and will be removed in the future. see DisplaySink instead.")] pub enum NumberStyleHint { Signed, HexSigned, @@ -17,6 +40,7 @@ pub enum NumberStyleHint { HexUnsignedWithSign } +#[deprecated(since="0.3.0", note="format_number_i32 is both slow and incorrect: YaxColors may not result in correct styling when writing anywhere other than a terminal, and both stylin and formatting does not inline as well as initially expected. see DisplaySink instead.")] pub fn format_number_i32(colors: &Y, f: &mut W, i: i32, hint: NumberStyleHint) -> fmt::Result { match hint { NumberStyleHint::Signed => { @@ -64,6 +88,7 @@ pub fn format_number_i32(colors: &Y, f: &mut W, i: } } +#[deprecated(since="0.3.0", note="SignedHexDisplay does not optimize like expected and will be removed in the future. see DisplaySink instead.")] pub struct SignedHexDisplay { value: T, negative: bool @@ -79,6 +104,7 @@ impl fmt::Display for SignedHexDisplay where W } } +#[deprecated(since="0.3.0", note="u8_hex does not optimize like expected and will be removed in the future. see DisplaySink instead.")] pub fn u8_hex(value: u8) -> SignedHexDisplay { SignedHexDisplay { value: value as i8, @@ -86,6 +112,7 @@ pub fn u8_hex(value: u8) -> SignedHexDisplay { } } +#[deprecated(since="0.3.0", note="signed_i8_hex does not optimize like expected and will be removed in the future. see DisplaySink instead.")] pub fn signed_i8_hex(imm: i8) -> SignedHexDisplay { SignedHexDisplay { value: imm, @@ -93,6 +120,7 @@ pub fn signed_i8_hex(imm: i8) -> SignedHexDisplay { } } +#[deprecated(since="0.3.0", note="u16_hex does not optimize like expected and will be removed in the future. see DisplaySink instead.")] pub fn u16_hex(value: u16) -> SignedHexDisplay { SignedHexDisplay { value: value as i16, @@ -100,6 +128,7 @@ pub fn u16_hex(value: u16) -> SignedHexDisplay { } } +#[deprecated(since="0.3.0", note="signed_i16_hex does not optimize like expected and will be removed in the future. see DisplaySink instead.")] pub fn signed_i16_hex(imm: i16) -> SignedHexDisplay { SignedHexDisplay { value: imm, @@ -107,6 +136,7 @@ pub fn signed_i16_hex(imm: i16) -> SignedHexDisplay { } } +#[deprecated(since="0.3.0", note="u32_hex does not optimize like expected and will be removed in the future. see DisplaySink instead.")] pub fn u32_hex(value: u32) -> SignedHexDisplay { SignedHexDisplay { value: value as i32, @@ -114,6 +144,7 @@ pub fn u32_hex(value: u32) -> SignedHexDisplay { } } +#[deprecated(since="0.3.0", note="signed_i32_hex does not optimize like expected and will be removed in the future. see DisplaySink instead.")] pub fn signed_i32_hex(imm: i32) -> SignedHexDisplay { SignedHexDisplay { value: imm, @@ -121,6 +152,7 @@ pub fn signed_i32_hex(imm: i32) -> SignedHexDisplay { } } +#[deprecated(since="0.3.0", note="u64_hex does not optimize like expected and will be removed in the future. see DisplaySink instead.")] pub fn u64_hex(value: u64) -> SignedHexDisplay { SignedHexDisplay { value: value as i64, @@ -128,6 +160,7 @@ pub fn u64_hex(value: u64) -> SignedHexDisplay { } } +#[deprecated(since="0.3.0", note="signed_i64_hex does not optimize like expected and will be removed in the future. see DisplaySink instead.")] pub fn signed_i64_hex(imm: i64) -> SignedHexDisplay { SignedHexDisplay { value: imm, diff --git a/src/display/display_sink.rs b/src/display/display_sink.rs new file mode 100644 index 0000000..418b6aa --- /dev/null +++ b/src/display/display_sink.rs @@ -0,0 +1,1232 @@ +use core::fmt; + +use crate::display::u8_to_hex; + +use crate::safer_unchecked::unreachable_kinda_unchecked; + +/// `DisplaySink` allows client code to collect output and minimal markup. this is currently used +/// in formatting instructions for two reasons: +/// * `DisplaySink` implementations have the opportunity to collect starts and ends of tokens at +/// the same time as collecting output itself. +/// * `DisplaySink` implementations provide specialized functions for writing strings in +/// circumstances where a simple "use `core::fmt`" might incur unwanted overhead. +/// +/// ## spans +/// +/// spans are out-of-band indicators for the meaning of data written to this sink. when a +/// `span_start_` function is called, data written until a matching `span_end_` can be +/// considered the text corresponding to ``. +/// +/// spans are entered and exited in a FILO manner. implementations of `DisplaySink` are explicitly +/// allowed to depend on this fact. functions writing to a `DisplaySink` must exit spans in reverse +/// order to when they are entered. a function that has a call sequence like +/// ```text +/// sink.span_start_operand(); +/// sink.span_start_immediate(); +/// sink.span_end_operand(); +/// ``` +/// is in error. +/// +/// spans are reported through the `span_start_*` and `span_end_*` families of functions to avoid +/// constraining implementations into tracking current output offset (which may not be knowable) or +/// span size (which may be knowable, but incur additional overhead to compute or track). if the +/// task for a span is to simply emit VT100 color codes, for example, implementations avoid the +/// overhead of tracking offsets. +/// +/// default implementations of the `span_start_*` and `span_end_*` functions are to do nothing. a +/// no-op `span_start_*` or `span_end_*` allows rustc to elimiate such calls at compile time for +/// `DisplaySink` that are uninterested in the corresponding span type. +/// +/// # write helpers (`write_*`) +/// +/// the `write_*` helpers on `DisplaySink` may be able to take advantage of contraints described in +/// documentation here to better support writing some kinds of inputs than a fully-general solution +/// (such as `core::fmt`) might be able to yield. +/// +/// currently there are two motivating factors for `write_*` helpers: +/// +/// instruction formatting often involves writing small but variable-size strings, such as register +/// names, which is something of a pathological case for string appending as Rust currently exists: +/// this often becomes `memcpy` and specifically a call to the platform's `memcpy` (rather than an +/// inlined `rep movsb`) just to move 3-5 bytes. one relevant Rust issue for reference: +/// +/// +/// there are similar papercuts around formatting integers as base-16 numbers, such as +/// . in isolation and in most applications these are +/// not a significant source of overhead. but for programs bounded on decoding and printing +/// instructions, these can add up to significant overhead - on the order of 10-20% of total +/// runtime. +/// +/// ## example +/// +/// a simple call sequence to `DisplaySink` might look something like: +/// ```compile_fail +/// sink.span_start_operand() +/// sink.write_char('[') +/// sink.span_start_register() +/// sink.write_fixed_size("rbp") +/// sink.span_end_register() +/// sink.write_char(']') +/// sink.span_end_operand() +/// ``` +/// which writes the text `[rbp]`, telling sinks that the operand begins at `[`, ends after `]`, +/// and `rbp` is a register in that operand. +/// +/// ## extensibility +/// +/// additional `span_{start,end}_*` helpers may be added over time - in the above example, one +/// future addition might be to add a new `effective_address` span that is started before +/// `register` and ended after `register. for an operand like `\[rbp\]` the effective address span +/// would exactly match a corresponding register span, but in more complicated scenarios like +/// `[rsp + rdi * 4 + 0x50]` the effective address would be all of `rsp + rdi * 4 + 0x50`. +/// +/// additional spans are expected to be added as needed. it is not immediately clear how to add +/// support for more architecture-specific concepts (such as itanium predicate registers) would be +/// supported yet, and so architecture-specific concepts may be expressed on `DisplaySink` if the +/// need arises. +/// +/// new `span_{start,end}_*` helpers will be defaulted as no-op. additions to this trait will be +/// minor version bumps, so users should take care to not add custom functions starting with +/// `span_start_` or `span_end_` to structs implementing `DisplaySink`. +pub trait DisplaySink: fmt::Write { + #[inline(always)] + fn write_fixed_size(&mut self, s: &str) -> Result<(), core::fmt::Error> { + self.write_str(s) + } + + /// write a string to this sink that is less than 32 bytes. this is provided for optimization + /// opportunities when writing a variable-length string with known max size. + /// + /// SAFETY: the provided `s` must be less than 32 bytes. if the provided string is longer than + /// 31 bytes, implementations may only copy part of a multi-byte codepoint while writing to a + /// utf-8 string. this may corrupt Rust strings. + unsafe fn write_lt_32(&mut self, s: &str) -> Result<(), core::fmt::Error> { + self.write_str(s) + } + /// write a string to this sink that is less than 16 bytes. this is provided for optimization + /// opportunities when writing a variable-length string with known max size. + /// + /// SAFETY: the provided `s` must be less than 16 bytes. if the provided string is longer than + /// 15 bytes, implementations may only copy part of a multi-byte codepoint while writing to a + /// utf-8 string. this may corrupt Rust strings. + unsafe fn write_lt_16(&mut self, s: &str) -> Result<(), core::fmt::Error> { + self.write_str(s) + } + /// write a string to this sink that is less than 8 bytes. this is provided for optimization + /// opportunities when writing a variable-length string with known max size. + /// + /// SAFETY: the provided `s` must be less than 8 bytes. if the provided string is longer than + /// 7 bytes, implementations may only copy part of a multi-byte codepoint while writing to a + /// utf-8 string. this may corrupt Rust strings. + unsafe fn write_lt_8(&mut self, s: &str) -> Result<(), core::fmt::Error> { + self.write_str(s) + } + + /// write a u8 to the output as a base-16 integer. + /// + /// this corresponds to the Rust format specifier `{:x}` - see [`std::fmt::LowerHex`] for more. + /// + /// this is provided for optimization opportunities when the formatted integer can be written + /// directly to the sink (rather than formatted to an intermediate buffer and output as a + /// followup step) + fn write_u8(&mut self, v: u8) -> Result<(), core::fmt::Error> { + write!(self, "{:x}", v) + } + /// write a u8 to the output as a base-16 integer with leading `0x`. + /// + /// this corresponds to the Rust format specifier `{#:x}` - see [`std::fmt::LowerHex`] for more. + /// + /// this is provided for optimization opportunities when the formatted integer can be written + /// directly to the sink (rather than formatted to an intermediate buffer and output as a + /// followup step) + fn write_prefixed_u8(&mut self, v: u8) -> Result<(), core::fmt::Error> { + self.write_fixed_size("0x")?; + self.write_u8(v) + } + /// write an i8 to the output as a base-16 integer with leading `0x`, and leading `-` if the + /// value is negative. + /// + /// there is no matching `std` formatter, so some examples here: + /// ```text + /// sink.write_prefixed_i8(-0x60); // writes `-0x60` to the sink + /// sink.write_prefixed_i8(127); // writes `0x7f` to the sink + /// sink.write_prefixed_i8(-128); // writes `-0x80` to the sink + /// ``` + /// + /// this is provided for optimization opportunities when the formatted integer can be written + /// directly to the sink (rather than formatted to an intermediate buffer and output as a + /// followup step) + fn write_prefixed_i8(&mut self, v: i8) -> Result<(), core::fmt::Error> { + let v = if v < 0 { + self.write_char('-')?; + v.unsigned_abs() + } else { + v as u8 + }; + self.write_prefixed_u8(v) + } + /// write a u16 to the output as a base-16 integer. + /// + /// this is provided for optimization opportunities when the formatted integer can be written + /// directly to the sink (rather than formatted to an intermediate buffer and output as a + /// followup step) + fn write_u16(&mut self, v: u16) -> Result<(), core::fmt::Error> { + write!(self, "{:x}", v) + } + /// write a u16 to the output as a base-16 integer with leading `0x`. + /// + /// this corresponds to the Rust format specifier `{#:x}` - see [`std::fmt::LowerHex`] for more. + /// + /// this is provided for optimization opportunities when the formatted integer can be written + /// directly to the sink (rather than formatted to an intermediate buffer and output as a + /// followup step) + fn write_prefixed_u16(&mut self, v: u16) -> Result<(), core::fmt::Error> { + self.write_fixed_size("0x")?; + self.write_u16(v) + } + /// write an i16 to the output as a base-16 integer with leading `0x`, and leading `-` if the + /// value is negative. + /// + /// there is no matching `std` formatter, so some examples here: + /// ```text + /// sink.write_prefixed_i16(-0x60); // writes `-0x60` to the sink + /// sink.write_prefixed_i16(127); // writes `0x7f` to the sink + /// sink.write_prefixed_i16(-128); // writes `-0x80` to the sink + /// ``` + /// + /// this is provided for optimization opportunities when the formatted integer can be written + /// directly to the sink (rather than formatted to an intermediate buffer and output as a + /// followup step) + fn write_prefixed_i16(&mut self, v: i16) -> Result<(), core::fmt::Error> { + let v = if v < 0 { + self.write_char('-')?; + v.unsigned_abs() + } else { + v as u16 + }; + self.write_prefixed_u16(v) + } + /// write a u32 to the output as a base-16 integer. + /// + /// this is provided for optimization opportunities when the formatted integer can be written + /// directly to the sink (rather than formatted to an intermediate buffer and output as a + /// followup step) + fn write_u32(&mut self, v: u32) -> Result<(), core::fmt::Error> { + write!(self, "{:x}", v) + } + /// write a u32 to the output as a base-16 integer with leading `0x`. + /// + /// this corresponds to the Rust format specifier `{#:x}` - see [`std::fmt::LowerHex`] for more. + /// + /// this is provided for optimization opportunities when the formatted integer can be written + /// directly to the sink (rather than formatted to an intermediate buffer and output as a + /// followup step) + fn write_prefixed_u32(&mut self, v: u32) -> Result<(), core::fmt::Error> { + self.write_fixed_size("0x")?; + self.write_u32(v) + } + /// write an i32 to the output as a base-32 integer with leading `0x`, and leading `-` if the + /// value is negative. + /// + /// there is no matching `std` formatter, so some examples here: + /// ```text + /// sink.write_prefixed_i32(-0x60); // writes `-0x60` to the sink + /// sink.write_prefixed_i32(127); // writes `0x7f` to the sink + /// sink.write_prefixed_i32(-128); // writes `-0x80` to the sink + /// ``` + /// + /// this is provided for optimization opportunities when the formatted integer can be written + /// directly to the sink (rather than formatted to an intermediate buffer and output as a + /// followup step) + fn write_prefixed_i32(&mut self, v: i32) -> Result<(), core::fmt::Error> { + let v = if v < 0 { + self.write_char('-')?; + v.unsigned_abs() + } else { + v as u32 + }; + self.write_prefixed_u32(v) + } + /// write a u64 to the output as a base-16 integer. + /// + /// this is provided for optimization opportunities when the formatted integer can be written + /// directly to the sink (rather than formatted to an intermediate buffer and output as a + /// followup step) + fn write_u64(&mut self, v: u64) -> Result<(), core::fmt::Error> { + write!(self, "{:x}", v) + } + /// write a u64 to the output as a base-16 integer with leading `0x`. + /// + /// this corresponds to the Rust format specifier `{#:x}` - see [`std::fmt::LowerHex`] for more. + /// + /// this is provided for optimization opportunities when the formatted integer can be written + /// directly to the sink (rather than formatted to an intermediate buffer and output as a + /// followup step) + fn write_prefixed_u64(&mut self, v: u64) -> Result<(), core::fmt::Error> { + self.write_fixed_size("0x")?; + self.write_u64(v) + } + /// write an i64 to the output as a base-64 integer with leading `0x`, and leading `-` if the + /// value is negative. + /// + /// there is no matching `std` formatter, so some examples here: + /// ```text + /// sink.write_prefixed_i64(-0x60); // writes `-0x60` to the sink + /// sink.write_prefixed_i64(127); // writes `0x7f` to the sink + /// sink.write_prefixed_i64(-128); // writes `-0x80` to the sink + /// ``` + /// + /// this is provided for optimization opportunities when the formatted integer can be written + /// directly to the sink (rather than formatted to an intermediate buffer and output as a + /// followup step) + fn write_prefixed_i64(&mut self, v: i64) -> Result<(), core::fmt::Error> { + let v = if v < 0 { + self.write_char('-')?; + v.unsigned_abs() + } else { + v as u64 + }; + self.write_prefixed_u64(v) + } + + /// enter a region inside which output corresponds to an immediate. + fn span_start_immediate(&mut self) { } + /// end a region where an immediate was written. see docs on [`DisplaySink`] for more. + fn span_end_immediate(&mut self) { } + + /// enter a region inside which output corresponds to a register. + fn span_start_register(&mut self) { } + /// end a region where a register was written. see docs on [`DisplaySink`] for more. + fn span_end_register(&mut self) { } + + /// enter a region inside which output corresponds to an opcode. + fn span_start_opcode(&mut self) { } + /// end a region where an opcode was written. see docs on [`DisplaySink`] for more. + fn span_end_opcode(&mut self) { } + + /// enter a region inside which output corresponds to the program counter. + fn span_start_program_counter(&mut self) { } + /// end a region where the program counter was written. see docs on [`DisplaySink`] for more. + fn span_end_program_counter(&mut self) { } + + /// enter a region inside which output corresponds to a number, such as a memory offset or + /// immediate. + fn span_start_number(&mut self) { } + /// end a region where a number was written. see docs on [`DisplaySink`] for more. + fn span_end_number(&mut self) { } + + /// enter a region inside which output corresponds to an address. this is a best guess; + /// instructions like x86's `lea` may involve an "address" that is not, and arithmetic + /// instructions may operate on addresses held in registers. + /// + /// where possible, the presence of this span will be informed by ISA semantics - if an + /// instruction has a memory operand, the effective address calculation of that operand should + /// be in an address span. + fn span_start_address(&mut self) { } + /// end a region where an address was written. the specifics of an "address" are ambiguous and + /// best-effort; see [`DisplaySink::span_start_address`] for more about this. otherwise, see + /// docs on [`DisplaySink`] for more about spans. + fn span_end_address(&mut self) { } + + /// enter a region inside which output corresponds to a function address, or expression + /// evaluating to a function address. this is a best guess; instructions like `call` may call + /// to a non-function address, `jmp` may jump to a function (as with tail calls), function + /// addresses may be computed via table lookup without semantic hints. + /// + /// where possible, the presence of this span will be informed by ISA semantics - if an + /// instruction is like a "call", an address operand should be a `function` span. if other + /// instructions can be expected to handle subroutine starting addresses purely from ISA + /// semantics, address operand(s) should be in a `function` span. + fn span_start_function_expr(&mut self) { } + /// end a region where function address expression was written. the specifics of a "function + /// address" are ambiguous and best-effort; see [`DisplaySink::span_start_function_expr`] for more + /// about this. otherwise, see docs on [`DisplaySink`] for more about spans. + fn span_end_function_expr(&mut self) { } +} + +/// `FmtSink` can be used to adapt any `fmt::Write`-implementing type into a `DisplaySink` to +/// format an instruction while discarding all span information at zero cost. +pub struct FmtSink<'a, T: fmt::Write> { + out: &'a mut T, +} + +impl<'a, T: fmt::Write> FmtSink<'a, T> { + pub fn new(f: &'a mut T) -> Self { + Self { out: f } + } +} + +/// blanket impl that discards all span information, forwards writes to the underlying `fmt::Write` +/// type. +impl<'a, T: fmt::Write> DisplaySink for FmtSink<'a, T> { } + +impl<'a, T: fmt::Write> fmt::Write for FmtSink<'a, T> { + fn write_str(&mut self, s: &str) -> Result<(), core::fmt::Error> { + self.out.write_str(s) + } + fn write_char(&mut self, c: char) -> Result<(), core::fmt::Error> { + self.out.write_char(c) + } + fn write_fmt(&mut self, f: fmt::Arguments) -> Result<(), core::fmt::Error> { + self.out.write_fmt(f) + } +} + +/// this is an implementation detail of yaxpeax-arch and related crates. if you are a user of the +/// disassemblers, do not use this struct. do not depend on this struct existing. this struct is +/// not stable. this struct is not safe for general use. if you use this struct you and your +/// program will be eaten by gremlins. +/// +/// if you are implementing an instruction formatter for the yaxpeax family of crates: this struct +/// is guaranteed to contain a string that is long enough to hold a fully-formatted instruction. +/// because the buffer is guaranteed to be long enough, writes through `InstructionTextSink` are +/// not bounds-checked, and the buffer is never grown. +/// +/// this is wildly dangerous in general use. the public constructor of `InstructionTextSink` is +/// unsafe as a result. as used in `InstructionFormatter`, the buffer is guaranteed to be +/// `clear()`ed before use, `InstructionFormatter` ensures the buffer is large enough, *and* +/// `InstructionFormatter` never allows `InstructionTextSink` to exist in a context where it would +/// be written to without being rewound first. +/// +/// because this opens a very large hole through which `fmt::Write` can become unsafe, incorrect +/// uses of this struct will be hard to debug in general. `InstructionFormatter` is probably at the +/// limit of easily-reasoned-about lifecycle of the buffer, which "only" leaves the problem of +/// ensuring that instruction formatting impls this buffer is passed to are appropriately sized. +/// +/// this is intended to be hidden in docs. if you see this in docs, it's a bug. +#[doc(hidden)] +pub struct InstructionTextSink<'buf> { + buf: &'buf mut alloc::string::String +} + +impl<'buf> InstructionTextSink<'buf> { + // TODO: safety + pub unsafe fn new(buf: &'buf mut alloc::string::String) -> Self { + Self { buf } + } +} + +impl<'buf> fmt::Write for InstructionTextSink<'buf> { + fn write_str(&mut self, s: &str) -> Result<(), core::fmt::Error> { + self.buf.write_str(s) + } + fn write_char(&mut self, c: char) -> Result<(), core::fmt::Error> { + if cfg!(debug_assertions) { + if self.buf.capacity() < self.buf.len() + 1 { + panic!("InstructionTextSink::write_char would overflow output"); + } + } + // SAFETY: `buf` is assumed to be long enough to hold all input, `buf` at `underlying.len()` + // is valid for writing, but may be uninitialized. + // + // this function is essentially equivalent to `Vec::push` specialized for the case that + // `len < buf.capacity()`: + // https://github.com/rust-lang/rust/blob/be9e27e/library/alloc/src/vec/mod.rs#L1993-L2006 + unsafe { + let underlying = self.buf.as_mut_vec(); + // `InstructionTextSink::write_char` is only used by yaxpeax-x86, and is only used to + // write single ASCII characters. this is wrong in the general case, but `write_char` + // here is not going to be used in the general case. + if cfg!(debug_asertions) { + panic!("InstructionTextSink::write_char would truncate output"); + } + let to_push = c as u8; + // `ptr::write` here because `underlying.add(underlying.len())` may not point to an + // initialized value, which would mean that turning that pointer into a `&mut u8` to + // store through would be UB. `ptr::write` avoids taking the mut ref. + underlying.as_mut_ptr().offset(underlying.len() as isize).write(to_push); + // we have initialized all (one) bytes that `set_len` is increasing the length to + // include. + underlying.set_len(underlying.len() + 1); + } + Ok(()) + } +} + +/// this [`DisplaySink`] impl exists to support somewhat more performant buffering of the kinds of +/// strings `yaxpeax-x86` uses in formatting instructions. +/// +/// span information is discarded at zero cost. +impl DisplaySink for alloc::string::String { + #[inline(always)] + fn write_fixed_size(&mut self, s: &str) -> Result<(), core::fmt::Error> { + self.reserve(s.len()); + let buf = unsafe { self.as_mut_vec() }; + let new_bytes = s.as_bytes(); + + if new_bytes.len() == 0 { + unsafe { unreachable_kinda_unchecked() } + } + + if new_bytes.len() >= 16 { + unsafe { unreachable_kinda_unchecked() } + } + + unsafe { + let dest = buf.as_mut_ptr().offset(buf.len() as isize); + + // this used to be enough to bamboozle llvm away from + // https://github.com/rust-lang/rust/issues/92993#issuecomment-2028915232 + // if `s` is not fixed size. somewhere between Rust 1.68 and Rust 1.74 this stopped + // being sufficient, so `write_fixed_size` truly should only be used for fixed size `s` + // (otherwise this is a libc memcpy call in disguise). for fixed-size strings this + // unrolls into some kind of appropriate series of `mov`. + dest.offset(0 as isize).write(new_bytes[0]); + for i in 1..new_bytes.len() { + dest.offset(i as isize).write(new_bytes[i]); + } + + buf.set_len(buf.len() + new_bytes.len()); + } + + Ok(()) + } + unsafe fn write_lt_32(&mut self, s: &str) -> Result<(), fmt::Error> { + self.reserve(s.len()); + + // SAFETY: todo + let buf = unsafe { self.as_mut_vec() }; + let new_bytes = s.as_bytes(); + + // should get DCE + if new_bytes.len() >= 32 { + unsafe { core::hint::unreachable_unchecked() } + } + + unsafe { + let dest = buf.as_mut_ptr().offset(buf.len() as isize); + let src = new_bytes.as_ptr(); + + let rem = new_bytes.len() as isize; + + // set_len early because there is no way to avoid the following asm!() writing that + // same number of bytes into buf + buf.set_len(buf.len() + new_bytes.len()); + + core::arch::asm!( + "6:", + "cmp {rem:e}, 16", + "jb 7f", + "mov {buf:r}, qword ptr [{src} + {rem} - 16]", + "mov qword ptr [{dest} + {rem} - 16], {buf:r}", + "mov {buf:r}, qword ptr [{src} + {rem} - 8]", + "mov qword ptr [{dest} + {rem} - 8], {buf:r}", + "sub {rem:e}, 16", + "jz 11f", + "7:", + "cmp {rem:e}, 8", + "jb 8f", + "mov {buf:r}, qword ptr [{src} + {rem} - 8]", + "mov qword ptr [{dest} + {rem} - 8], {buf:r}", + "sub {rem:e}, 8", + "jz 11f", + "8:", + "cmp {rem:e}, 4", + "jb 9f", + "mov {buf:e}, dword ptr [{src} + {rem} - 4]", + "mov dword ptr [{dest} + {rem} - 4], {buf:e}", + "sub {rem:e}, 4", + "jz 11f", + "9:", + "cmp {rem:e}, 2", + "jb 10f", + "mov {buf:x}, word ptr [{src} + {rem} - 2]", + "mov word ptr [{dest} + {rem} - 2], {buf:x}", + "sub {rem:e}, 2", + "jz 11f", + "10:", + "cmp {rem:e}, 1", + "jb 11f", + "mov {buf:l}, byte ptr [{src} + {rem} - 1]", + "mov byte ptr [{dest} + {rem} - 1], {buf:l}", + "11:", + src = in(reg) src, + dest = in(reg) dest, + rem = inout(reg) rem => _, + buf = out(reg) _, + options(nostack), + ); + } + + Ok(()) + } + unsafe fn write_lt_16(&mut self, s: &str) -> Result<(), fmt::Error> { + self.reserve(s.len()); + + // SAFETY: todo + let buf = unsafe { self.as_mut_vec() }; + let new_bytes = s.as_bytes(); + + // should get DCE + if new_bytes.len() >= 16 { + unsafe { core::hint::unreachable_unchecked() } + } + + unsafe { + let dest = buf.as_mut_ptr().offset(buf.len() as isize); + let src = new_bytes.as_ptr(); + + let rem = new_bytes.len() as isize; + + // set_len early because there is no way to avoid the following asm!() writing that + // same number of bytes into buf + buf.set_len(buf.len() + new_bytes.len()); + + core::arch::asm!( + "7:", + "cmp {rem:e}, 8", + "jb 8f", + "mov {buf:r}, qword ptr [{src} + {rem} - 8]", + "mov qword ptr [{dest} + {rem} - 8], {buf:r}", + "sub {rem:e}, 8", + "jz 11f", + "8:", + "cmp {rem:e}, 4", + "jb 9f", + "mov {buf:e}, dword ptr [{src} + {rem} - 4]", + "mov dword ptr [{dest} + {rem} - 4], {buf:e}", + "sub {rem:e}, 4", + "jz 11f", + "9:", + "cmp {rem:e}, 2", + "jb 10f", + "mov {buf:x}, word ptr [{src} + {rem} - 2]", + "mov word ptr [{dest} + {rem} - 2], {buf:x}", + "sub {rem:e}, 2", + "jz 11f", + "10:", + "cmp {rem:e}, 1", + "jb 11f", + "mov {buf:l}, byte ptr [{src} + {rem} - 1]", + "mov byte ptr [{dest} + {rem} - 1], {buf:l}", + "11:", + src = in(reg) src, + dest = in(reg) dest, + rem = inout(reg) rem => _, + buf = out(reg) _, + options(nostack), + ); + } + + Ok(()) + } + unsafe fn write_lt_8(&mut self, s: &str) -> Result<(), fmt::Error> { + self.reserve(s.len()); + + // SAFETY: todo + let buf = unsafe { self.as_mut_vec() }; + let new_bytes = s.as_bytes(); + + // should get DCE + if new_bytes.len() >= 8 { + unsafe { core::hint::unreachable_unchecked() } + } + + unsafe { + let dest = buf.as_mut_ptr().offset(buf.len() as isize); + let src = new_bytes.as_ptr(); + + let rem = new_bytes.len() as isize; + + // set_len early because there is no way to avoid the following asm!() writing that + // same number of bytes into buf + buf.set_len(buf.len() + new_bytes.len()); + + core::arch::asm!( + "8:", + "cmp {rem:e}, 4", + "jb 9f", + "mov {buf:e}, dword ptr [{src} + {rem} - 4]", + "mov dword ptr [{dest} + {rem} - 4], {buf:e}", + "sub {rem:e}, 4", + "jz 11f", + "9:", + "cmp {rem:e}, 2", + "jb 10f", + "mov {buf:x}, word ptr [{src} + {rem} - 2]", + "mov word ptr [{dest} + {rem} - 2], {buf:x}", + "sub {rem:e}, 2", + "jz 11f", + "10:", + "cmp {rem:e}, 1", + "jb 11f", + "mov {buf:l}, byte ptr [{src} + {rem} - 1]", + "mov byte ptr [{dest} + {rem} - 1], {buf:l}", + "11:", + src = in(reg) src, + dest = in(reg) dest, + rem = inout(reg) rem => _, + buf = out(reg) _, + options(nostack), + ); + } + + Ok(()) + } + /// write a u8 to the output as a base-16 integer. + /// + /// this is provided for optimization opportunities when the formatted integer can be written + /// directly to the sink (rather than formatted to an intermediate buffer and output as a + /// followup step) + #[inline(always)] + fn write_u8(&mut self, mut v: u8) -> Result<(), core::fmt::Error> { + if v == 0 { + return self.write_fixed_size("0"); + } + // we can fairly easily predict the size of a formatted string here with lzcnt, which also + // means we can write directly into the correct offsets of the output string. + let printed_size = ((8 - v.leading_zeros() + 3) >> 2) as usize; + + self.reserve(printed_size); + + let buf = unsafe { self.as_mut_vec() }; + let new_len = buf.len() + printed_size; + + unsafe { + buf.set_len(new_len); + } + let mut p = unsafe { buf.as_mut_ptr().offset(new_len as isize) }; + + loop { + let digit = v % 16; + let c = u8_to_hex(digit as u8); + unsafe { + p = p.offset(-1); + p.write(c); + } + v = v / 16; + if v == 0 { + break; + } + } + + Ok(()) + } + /// write a u16 to the output as a base-16 integer. + /// + /// this is provided for optimization opportunities when the formatted integer can be written + /// directly to the sink (rather than formatted to an intermediate buffer and output as a + /// followup step) + #[inline(always)] + fn write_u16(&mut self, mut v: u16) -> Result<(), core::fmt::Error> { + if v == 0 { + return self.write_fixed_size("0"); + } + // we can fairly easily predict the size of a formatted string here with lzcnt, which also + // means we can write directly into the correct offsets of the output string. + let printed_size = ((16 - v.leading_zeros() + 3) >> 2) as usize; + + self.reserve(printed_size); + + let buf = unsafe { self.as_mut_vec() }; + let new_len = buf.len() + printed_size; + + unsafe { + buf.set_len(new_len); + } + let mut p = unsafe { buf.as_mut_ptr().offset(new_len as isize) }; + + loop { + let digit = v % 16; + let c = u8_to_hex(digit as u8); + unsafe { + p = p.offset(-1); + p.write(c); + } + v = v / 16; + if v == 0 { + break; + } + } + + Ok(()) + } + /// write a u32 to the output as a base-16 integer. + /// + /// this is provided for optimization opportunities when the formatted integer can be written + /// directly to the sink (rather than formatted to an intermediate buffer and output as a + /// followup step) + #[inline(always)] + fn write_u32(&mut self, mut v: u32) -> Result<(), core::fmt::Error> { + if v == 0 { + return self.write_fixed_size("0"); + } + // we can fairly easily predict the size of a formatted string here with lzcnt, which also + // means we can write directly into the correct offsets of the output string. + let printed_size = ((32 - v.leading_zeros() + 3) >> 2) as usize; + + self.reserve(printed_size); + + let buf = unsafe { self.as_mut_vec() }; + let new_len = buf.len() + printed_size; + + unsafe { + buf.set_len(new_len); + } + let mut p = unsafe { buf.as_mut_ptr().offset(new_len as isize) }; + + loop { + let digit = v % 16; + let c = u8_to_hex(digit as u8); + unsafe { + p = p.offset(-1); + p.write(c); + } + v = v / 16; + if v == 0 { + break; + } + } + + Ok(()) + } + /// write a u64 to the output as a base-16 integer. + /// + /// this is provided for optimization opportunities when the formatted integer can be written + /// directly to the sink (rather than formatted to an intermediate buffer and output as a + /// followup step) + #[inline(always)] + fn write_u64(&mut self, mut v: u64) -> Result<(), core::fmt::Error> { + if v == 0 { + return self.write_fixed_size("0"); + } + // we can fairly easily predict the size of a formatted string here with lzcnt, which also + // means we can write directly into the correct offsets of the output string. + let printed_size = ((64 - v.leading_zeros() + 3) >> 2) as usize; + + self.reserve(printed_size); + + let buf = unsafe { self.as_mut_vec() }; + let new_len = buf.len() + printed_size; + + unsafe { + buf.set_len(new_len); + } + let mut p = unsafe { buf.as_mut_ptr().offset(new_len as isize) }; + + loop { + let digit = v % 16; + let c = u8_to_hex(digit as u8); + unsafe { + p = p.offset(-1); + p.write(c); + } + v = v / 16; + if v == 0 { + break; + } + } + + Ok(()) + } +} + +impl<'buf> DisplaySink for InstructionTextSink<'buf> { + #[inline(always)] + fn write_fixed_size(&mut self, s: &str) -> Result<(), core::fmt::Error> { + if cfg!(debug_assertions) { + if self.buf.capacity() < self.buf.len() + s.len() { + panic!("InstructionTextSink::write_fixed_size would overflow output"); + } + } + + let buf = unsafe { self.buf.as_mut_vec() }; + let new_bytes = s.as_bytes(); + + if new_bytes.len() == 0 { + return Ok(()); + } + + if new_bytes.len() >= 16 { + unsafe { unreachable_kinda_unchecked() } + } + + unsafe { + let dest = buf.as_mut_ptr().offset(buf.len() as isize); + + // this used to be enough to bamboozle llvm away from + // https://github.com/rust-lang/rust/issues/92993#issuecomment-2028915232https://github.com/rust-lang/rust/issues/92993#issuecomment-2028915232 + // if `s` is not fixed size. somewhere between Rust 1.68 and Rust 1.74 this stopped + // being sufficient, so `write_fixed_size` truly should only be used for fixed size `s` + // (otherwise this is a libc memcpy call in disguise). for fixed-size strings this + // unrolls into some kind of appropriate series of `mov`. + dest.offset(0 as isize).write(new_bytes[0]); + for i in 1..new_bytes.len() { + dest.offset(i as isize).write(new_bytes[i]); + } + + buf.set_len(buf.len() + new_bytes.len()); + } + + Ok(()) + } + unsafe fn write_lt_32(&mut self, s: &str) -> Result<(), fmt::Error> { + if cfg!(debug_assertions) { + if self.buf.capacity() < self.buf.len() + s.len() { + panic!("InstructionTextSink::write_lt_32 would overflow output"); + } + } + + // SAFETY: todo + let buf = unsafe { self.buf.as_mut_vec() }; + let new_bytes = s.as_bytes(); + + // should get DCE + if new_bytes.len() >= 32 { + unsafe { core::hint::unreachable_unchecked() } + } + + unsafe { + let dest = buf.as_mut_ptr().offset(buf.len() as isize); + let src = new_bytes.as_ptr(); + + let rem = new_bytes.len() as isize; + + // set_len early because there is no way to avoid the following asm!() writing that + // same number of bytes into buf + buf.set_len(buf.len() + new_bytes.len()); + + core::arch::asm!( + "6:", + "cmp {rem:e}, 16", + "jb 7f", + "mov {buf:r}, qword ptr [{src} + {rem} - 16]", + "mov qword ptr [{dest} + {rem} - 16], {buf:r}", + "mov {buf:r}, qword ptr [{src} + {rem} - 8]", + "mov qword ptr [{dest} + {rem} - 8], {buf:r}", + "sub {rem:e}, 16", + "jz 11f", + "7:", + "cmp {rem:e}, 8", + "jb 8f", + "mov {buf:r}, qword ptr [{src} + {rem} - 8]", + "mov qword ptr [{dest} + {rem} - 8], {buf:r}", + "sub {rem:e}, 8", + "jz 11f", + "8:", + "cmp {rem:e}, 4", + "jb 9f", + "mov {buf:e}, dword ptr [{src} + {rem} - 4]", + "mov dword ptr [{dest} + {rem} - 4], {buf:e}", + "sub {rem:e}, 4", + "jz 11f", + "9:", + "cmp {rem:e}, 2", + "jb 10f", + "mov {buf:x}, word ptr [{src} + {rem} - 2]", + "mov word ptr [{dest} + {rem} - 2], {buf:x}", + "sub {rem:e}, 2", + "jz 11f", + "10:", + "cmp {rem:e}, 1", + "jb 11f", + "mov {buf:l}, byte ptr [{src} + {rem} - 1]", + "mov byte ptr [{dest} + {rem} - 1], {buf:l}", + "11:", + src = in(reg) src, + dest = in(reg) dest, + rem = inout(reg) rem => _, + buf = out(reg) _, + options(nostack), + ); + } + + Ok(()) + } + unsafe fn write_lt_16(&mut self, s: &str) -> Result<(), fmt::Error> { + if cfg!(debug_assertions) { + if self.buf.capacity() < self.buf.len() + s.len() { + panic!("InstructionTextSink::write_lt_16 would overflow output"); + } + } + + // SAFETY: todo + let buf = unsafe { self.buf.as_mut_vec() }; + let new_bytes = s.as_bytes(); + + // should get DCE + if new_bytes.len() >= 16 { + unsafe { core::hint::unreachable_unchecked() } + } + + unsafe { + let dest = buf.as_mut_ptr().offset(buf.len() as isize); + let src = new_bytes.as_ptr(); + + let rem = new_bytes.len() as isize; + + // set_len early because there is no way to avoid the following asm!() writing that + // same number of bytes into buf + buf.set_len(buf.len() + new_bytes.len()); + + core::arch::asm!( + "7:", + "cmp {rem:e}, 8", + "jb 8f", + "mov {buf:r}, qword ptr [{src} + {rem} - 8]", + "mov qword ptr [{dest} + {rem} - 8], {buf:r}", + "sub {rem:e}, 8", + "jz 11f", + "8:", + "cmp {rem:e}, 4", + "jb 9f", + "mov {buf:e}, dword ptr [{src} + {rem} - 4]", + "mov dword ptr [{dest} + {rem} - 4], {buf:e}", + "sub {rem:e}, 4", + "jz 11f", + "9:", + "cmp {rem:e}, 2", + "jb 10f", + "mov {buf:x}, word ptr [{src} + {rem} - 2]", + "mov word ptr [{dest} + {rem} - 2], {buf:x}", + "sub {rem:e}, 2", + "jz 11f", + "10:", + "cmp {rem:e}, 1", + "jb 11f", + "mov {buf:l}, byte ptr [{src} + {rem} - 1]", + "mov byte ptr [{dest} + {rem} - 1], {buf:l}", + "11:", + src = in(reg) src, + dest = in(reg) dest, + rem = inout(reg) rem => _, + buf = out(reg) _, + options(nostack), + ); + } + + Ok(()) + } + unsafe fn write_lt_8(&mut self, s: &str) -> Result<(), fmt::Error> { + if cfg!(debug_assertions) { + if self.buf.capacity() < self.buf.len() + s.len() { + panic!("InstructionTextSink::write_lt_8 would overflow output"); + } + } + + // SAFETY: todo + let buf = unsafe { self.buf.as_mut_vec() }; + let new_bytes = s.as_bytes(); + + // should get DCE + if new_bytes.len() >= 8 { + unsafe { core::hint::unreachable_unchecked() } + } + + unsafe { + let dest = buf.as_mut_ptr().offset(buf.len() as isize); + let src = new_bytes.as_ptr(); + + let rem = new_bytes.len() as isize; + + // set_len early because there is no way to avoid the following asm!() writing that + // same number of bytes into buf + buf.set_len(buf.len() + new_bytes.len()); + + core::arch::asm!( + "8:", + "cmp {rem:e}, 4", + "jb 9f", + "mov {buf:e}, dword ptr [{src} + {rem} - 4]", + "mov dword ptr [{dest} + {rem} - 4], {buf:e}", + "sub {rem:e}, 4", + "jz 11f", + "9:", + "cmp {rem:e}, 2", + "jb 10f", + "mov {buf:x}, word ptr [{src} + {rem} - 2]", + "mov word ptr [{dest} + {rem} - 2], {buf:x}", + "sub {rem:e}, 2", + "jz 11f", + "10:", + "cmp {rem:e}, 1", + "jb 11f", + "mov {buf:l}, byte ptr [{src} + {rem} - 1]", + "mov byte ptr [{dest} + {rem} - 1], {buf:l}", + "11:", + src = in(reg) src, + dest = in(reg) dest, + rem = inout(reg) rem => _, + buf = out(reg) _, + options(nostack), + ); + } + + Ok(()) + } + /// write a u8 to the output as a base-16 integer. + /// + /// this is provided for optimization opportunities when the formatted integer can be written + /// directly to the sink (rather than formatted to an intermediate buffer and output as a + /// followup step) + #[inline(always)] + fn write_u8(&mut self, mut v: u8) -> Result<(), core::fmt::Error> { + if v == 0 { + return self.write_fixed_size("0"); + } + // we can fairly easily predict the size of a formatted string here with lzcnt, which also + // means we can write directly into the correct offsets of the output string. + let printed_size = ((8 - v.leading_zeros() + 3) >> 2) as usize; + + if cfg!(debug_assertions) { + if self.buf.capacity() < self.buf.len() + printed_size { + panic!("InstructionTextSink::write_u8 would overflow output"); + } + } + + let buf = unsafe { self.buf.as_mut_vec() }; + let new_len = buf.len() + printed_size; + + unsafe { + buf.set_len(new_len); + } + let mut p = unsafe { buf.as_mut_ptr().offset(new_len as isize) }; + + loop { + let digit = v % 16; + let c = u8_to_hex(digit as u8); + unsafe { + p = p.offset(-1); + p.write(c); + } + v = v / 16; + if v == 0 { + break; + } + } + + Ok(()) + } + /// write a u16 to the output as a base-16 integer. + /// + /// this is provided for optimization opportunities when the formatted integer can be written + /// directly to the sink (rather than formatted to an intermediate buffer and output as a + /// followup step) + #[inline(always)] + fn write_u16(&mut self, mut v: u16) -> Result<(), core::fmt::Error> { + if v == 0 { + return self.write_fixed_size("0"); + } + + // we can fairly easily predict the size of a formatted string here with lzcnt, which also + // means we can write directly into the correct offsets of the output string. + let printed_size = ((16 - v.leading_zeros() + 3) >> 2) as usize; + + if cfg!(debug_assertions) { + if self.buf.capacity() < self.buf.len() + printed_size { + panic!("InstructionTextSink::write_u16 would overflow output"); + } + } + + let buf = unsafe { self.buf.as_mut_vec() }; + let new_len = buf.len() + printed_size; + + unsafe { + buf.set_len(new_len); + } + let mut p = unsafe { buf.as_mut_ptr().offset(new_len as isize) }; + + loop { + let digit = v % 16; + let c = u8_to_hex(digit as u8); + unsafe { + p = p.offset(-1); + p.write(c); + } + v = v / 16; + if v == 0 { + break; + } + } + + Ok(()) + } + /// write a u32 to the output as a base-16 integer. + /// + /// this is provided for optimization opportunities when the formatted integer can be written + /// directly to the sink (rather than formatted to an intermediate buffer and output as a + /// followup step) + #[inline(always)] + fn write_u32(&mut self, mut v: u32) -> Result<(), core::fmt::Error> { + if v == 0 { + return self.write_fixed_size("0"); + } + + // we can fairly easily predict the size of a formatted string here with lzcnt, which also + // means we can write directly into the correct offsets of the output string. + let printed_size = ((32 - v.leading_zeros() + 3) >> 2) as usize; + + if cfg!(debug_assertions) { + if self.buf.capacity() < self.buf.len() + printed_size { + panic!("InstructionTextSink::write_u32 would overflow output"); + } + } + + let buf = unsafe { self.buf.as_mut_vec() }; + let new_len = buf.len() + printed_size; + + unsafe { + buf.set_len(new_len); + } + let mut p = unsafe { buf.as_mut_ptr().offset(new_len as isize) }; + + loop { + let digit = v % 16; + let c = u8_to_hex(digit as u8); + unsafe { + p = p.offset(-1); + p.write(c); + } + v = v / 16; + if v == 0 { + break; + } + } + + Ok(()) + } + /// write a u64 to the output as a base-16 integer. + /// + /// this is provided for optimization opportunities when the formatted integer can be written + /// directly to the sink (rather than formatted to an intermediate buffer and output as a + /// followup step) + #[inline(always)] + fn write_u64(&mut self, mut v: u64) -> Result<(), core::fmt::Error> { + if v == 0 { + return self.write_fixed_size("0"); + } + + // we can fairly easily predict the size of a formatted string here with lzcnt, which also + // means we can write directly into the correct offsets of the output string. + let printed_size = ((64 - v.leading_zeros() + 3) >> 2) as usize; + + if cfg!(debug_assertions) { + if self.buf.capacity() < self.buf.len() + printed_size { + panic!("InstructionTextSink::write_u64 would overflow output"); + } + } + + let buf = unsafe { self.buf.as_mut_vec() }; + let new_len = buf.len() + printed_size; + + unsafe { + buf.set_len(new_len); + } + let mut p = unsafe { buf.as_mut_ptr().offset(new_len as isize) }; + + loop { + let digit = v % 16; + let c = u8_to_hex(digit as u8); + unsafe { + p = p.offset(-1); + p.write(c); + } + v = v / 16; + if v == 0 { + break; + } + } + + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 7aaba21..a194942 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,10 +24,19 @@ pub use color::{Colorize, NoColors, YaxColors}; #[cfg(feature="colors")] pub use color::ColorSettings; +#[cfg(feature = "alloc")] +extern crate alloc; + +#[cfg(feature = "alloc")] pub mod display; + +pub mod testkit; + mod reader; pub use reader::{Reader, ReaderBuilder, ReadError, U8Reader, U16le, U16be, U32le, U32be, U64le, U64be}; +pub mod safer_unchecked; + /// the minimum set of errors a `yaxpeax-arch` disassembler may produce. /// /// it is permissible for an implementor of `DecodeError` to have items that return `false` for diff --git a/src/safer_unchecked.rs b/src/safer_unchecked.rs new file mode 100644 index 0000000..34216bc --- /dev/null +++ b/src/safer_unchecked.rs @@ -0,0 +1,30 @@ +use core::slice::SliceIndex; + +pub trait GetSaferUnchecked { + unsafe fn get_kinda_unchecked(&self, index: I) -> &>::Output + where + I: SliceIndex<[T]>; +} + +impl GetSaferUnchecked for [T] { + #[inline(always)] + unsafe fn get_kinda_unchecked(&self, index: I) -> &>::Output + where + I: SliceIndex<[T]>, + { + if cfg!(debug_assertions) { + &self[index] + } else { + self.get_unchecked(index) + } + } +} + +#[inline(always)] +pub unsafe fn unreachable_kinda_unchecked() -> ! { + if cfg!(debug_assertions) { + panic!("UB: Unreachable unchecked was executed") + } else { + core::hint::unreachable_unchecked() + } +} diff --git a/src/testkit.rs b/src/testkit.rs new file mode 100644 index 0000000..1018234 --- /dev/null +++ b/src/testkit.rs @@ -0,0 +1,2 @@ +#[cfg(feature="alloc")] +pub mod display; diff --git a/src/testkit/display.rs b/src/testkit/display.rs new file mode 100644 index 0000000..a745b5c --- /dev/null +++ b/src/testkit/display.rs @@ -0,0 +1,166 @@ +//! tools to test the correctness of `yaxpeax-arch` trait implementations. + +use core::fmt; +use core::fmt::Write; + +use crate::display::DisplaySink; + +/// `DisplaySinkValidator` is a `DisplaySink` that panics if invariants required of +/// `DisplaySink`-writing functions are not upheld. +/// +/// there are two categories of invariants that `DisplaySinkValidator` validates. +/// +/// first, this panics if spans are not `span_end_*`-ed in first-in-last-out order with +/// corresponding `span_start_*. second, this panics if `write_lt_*` functions are ever provided +/// inputs longer than the corresponding maximum length. +/// +/// functions that write to a `DisplaySink` are strongly encouraged to come with fuzzing that for +/// all inputs `DisplaySinkValidator` does not panic. +pub struct DisplaySinkValidator { + spans: alloc::vec::Vec<&'static str>, +} + +impl fmt::Write for DisplaySinkValidator { + fn write_str(&mut self, _s: &str) -> Result<(), fmt::Error> { + Ok(()) + } + fn write_char(&mut self, _c: char) -> Result<(), fmt::Error> { + Ok(()) + } +} + +impl DisplaySink for DisplaySinkValidator { + unsafe fn write_lt_32(&mut self, s: &str) -> Result<(), fmt::Error> { + if s.len() >= 32 { + panic!("DisplaySinkValidator::write_lt_32 was given a string longer than the maximum permitted length"); + } + + self.write_str(s) + } + unsafe fn write_lt_16(&mut self, s: &str) -> Result<(), fmt::Error> { + if s.len() >= 16 { + panic!("DisplaySinkValidator::write_lt_16 was given a string longer than the maximum permitted length"); + } + + self.write_str(s) + } + unsafe fn write_lt_8(&mut self, s: &str) -> Result<(), fmt::Error> { + if s.len() >= 8 { + panic!("DisplaySinkValidator::write_lt_8 was given a string longer than the maximum permitted length"); + } + + self.write_str(s) + } + + fn span_start_immediate(&mut self) { + self.spans.push("immediate"); + } + + fn span_end_immediate(&mut self) { + let last = self.spans.pop().expect("item to pop"); + assert_eq!(last, "immediate"); + } + + fn span_start_register(&mut self) { + self.spans.push("register"); + } + + fn span_end_register(&mut self) { + let last = self.spans.pop().expect("item to pop"); + assert_eq!(last, "register"); + } + + fn span_start_opcode(&mut self) { + self.spans.push("opcode"); + } + + fn span_end_opcode(&mut self) { + let last = self.spans.pop().expect("item to pop"); + assert_eq!(last, "opcode"); + } + + fn span_start_program_counter(&mut self) { + self.spans.push("program counter"); + } + + fn span_end_program_counter(&mut self) { + let last = self.spans.pop().expect("item to pop"); + assert_eq!(last, "program counter"); + } + + fn span_start_number(&mut self) { + self.spans.push("number"); + } + + fn span_end_number(&mut self) { + let last = self.spans.pop().expect("item to pop"); + assert_eq!(last, "number"); + } + + fn span_start_address(&mut self) { + self.spans.push("address"); + } + + fn span_end_address(&mut self) { + let last = self.spans.pop().expect("item to pop"); + assert_eq!(last, "address"); + } + + fn span_start_function_expr(&mut self) { + self.spans.push("function expr"); + } + + fn span_end_function_expr(&mut self) { + let last = self.spans.pop().expect("item to pop"); + assert_eq!(last, "function expr"); + } +} + +/// `DisplaySinkWriteComparator` helps test that two `DisplaySink` implementations which should +/// produce the same output actually do. +/// +/// this is most useful for cases like testing specialized `write_lt_*` functions, which ought to +/// behave the same as if `write_str()` were called instead and so can be used as a very simple +/// oracle. +/// +/// this is somewhat less useful when the sinks are expected to produce unequal text, such as when +/// one sink writes ANSI color sequences and the other does not. +pub struct DisplaySinkWriteComparator<'sinks, T: DisplaySink, U: DisplaySink> { + sink1: &'sinks mut T, + sink1_check: fn(&T) -> &str, + sink2: &'sinks mut U, + sink2_check: fn(&U) -> &str, +} + +impl<'sinks, T: DisplaySink, U: DisplaySink> DisplaySinkWriteComparator<'sinks, T, U> { + fn compare_sinks(&self) { + let sink1_text = (self.sink1_check)(self.sink1); + let sink2_text = (self.sink2_check)(self.sink2); + + if sink1_text != sink2_text { + panic!("sinks produced different output: {} != {}", sink1_text, sink2_text); + } + } +} + +impl<'sinks, T: DisplaySink, U: DisplaySink> DisplaySink for DisplaySinkWriteComparator<'sinks, T, U> { + fn write_u8(&mut self, v: u8) -> Result<(), fmt::Error> { + self.sink1.write_u8(v).expect("write to sink1 succeeds"); + self.sink2.write_u8(v).expect("write to sink2 succeeds"); + self.compare_sinks(); + Ok(()) + } +} + +impl<'sinks, T: DisplaySink, U: DisplaySink> fmt::Write for DisplaySinkWriteComparator<'sinks, T, U> { + fn write_str(&mut self, s: &str) -> Result<(), fmt::Error> { + self.sink1.write_str(s).expect("write to sink1 succeeds"); + self.sink2.write_str(s).expect("write to sink2 succeeds"); + Ok(()) + } + fn write_char(&mut self, c: char) -> Result<(), fmt::Error> { + self.sink1.write_char(c).expect("write to sink1 succeeds"); + self.sink2.write_char(c).expect("write to sink2 succeeds"); + Ok(()) + } +} diff --git a/tests/display.rs b/tests/display.rs new file mode 100644 index 0000000..9a8ef2e --- /dev/null +++ b/tests/display.rs @@ -0,0 +1,71 @@ +/* +#[test] +fn sinks_are_equivalent() { + use yaxpeax_arch::display::NoColorsSink; + use yaxpeax_arch::testkit::DisplaySinkWriteComparator; + + let mut bare = String::new(); + let mut through_sink = String::new(); + for i in 0..u64::MAX { + bare.clear(); + through_sink.clear(); + let mut out = NoColorsSink { + out: &mut through_sink + }; + let mut comparator = DisplaySinkWriteComparator { + sink1: &mut out, + sink1_check: |sink| { sink.out.as_str() }, + sink2: &mut bare, + sink2_check: |sink| { sink.as_str() }, + }; + } +} +*/ + +#[cfg(feature="alloc")] +#[test] +fn display_sink_write_hex_helpers() { + use yaxpeax_arch::display::{DisplaySink}; + + // for u8/i8/u16/i16 we can exhaustively test. we'll leave the rest for fuzzers. + let mut buf = String::new(); + for i in 0..=u8::MAX { + buf.clear(); + buf.write_u8(i).expect("write succeeds"); + assert_eq!(buf, format!("{:x}", i)); + + buf.clear(); + buf.write_prefixed_u8(i).expect("write succeeds"); + assert_eq!(buf, format!("0x{:x}", i)); + + let expected = if (i as i8) < 0 { + format!("-0x{:x}", (i as i8).unsigned_abs()) + } else { + format!("0x{:x}", i) + }; + + buf.clear(); + buf.write_prefixed_i8(i as i8).expect("write succeeds"); + assert_eq!(buf, expected); + } + + for i in 0..=u16::MAX { + buf.clear(); + buf.write_u16(i).expect("write succeeds"); + assert_eq!(buf, format!("{:x}", i)); + + buf.clear(); + buf.write_prefixed_u16(i).expect("write succeeds"); + assert_eq!(buf, format!("0x{:x}", i)); + + let expected = if (i as i16) < 0 { + format!("-0x{:x}", (i as i16).unsigned_abs()) + } else { + format!("0x{:x}", i) + }; + + buf.clear(); + buf.write_prefixed_i16(i as i16).expect("write succeeds"); + assert_eq!(buf, expected); + } +} -- cgit v1.1