fix: C allocator contract, shadertoy overloads/MSL stride, minidumps
Closes the remaining library rows of the final completeness audit (docs/rewrite/notes/final-Z3.md): - libghostty-vt: OSC parser storage goes through the caller-supplied C allocator via a ByteStorage abstraction; ghostty_osc_new reports GHOSTTY_OUT_OF_MEMORY and failed grows invalidate the OSC like Zig. Tested with a counting/failing/limiting C allocator. - shadertoy: sampler parameter rewriting is keyed by full signature so overloads work; the Metal uniform block keeps std140 strides for iChannelTime[4] (MSL offsets checked against the upload layout). - tripwire.zig ported (named trip points, hit counts, verification) and used to port the previously skipped allocation-failure tests that have a fallible Rust path; the rest are documented. - crash: fatal signals capture siginfo + registers and a forked helper writes a minidump (minidump-writer) as an envelope attachment, like sentry-native/Breakpad; tests parse it with the minidump crate. - kitty LoadingImage::debug_dump (debug builds) and the Darwin Instruments signpost around benchmark runs. Test Plan: - cargo test -p ghostty-vt-capi --test c_allocator - cargo test -p ghostty-crash --test minidump
This commit is contained in:
1 parent
96314c1a8b
commit
f6002d7519
22 files changed
+2079
-57
No files matched your search
@@ -351,6 +351,114 @@ enum Writer {
|
||||
Allocating,
|
||||
}
|
||||
|
||||
/// The payload storage ran out of memory (the allocator refused to grow).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct StorageFull;
|
||||
|
||||
/// Allocator abstraction for the parser's payload buffer (Zig: the
|
||||
/// `Allocator` the parser was initialized with). The default storage is a
|
||||
/// `Vec<u8>` on the global allocator; the C API supplies storage that is
|
||||
/// allocated through the caller's `GhosttyAllocator`, so allocation limits and
|
||||
/// failures of that allocator govern the parser's memory like they do in Zig.
|
||||
///
|
||||
/// A failed `try_extend`/`try_reserve` invalidates the OSC being parsed
|
||||
/// (Zig: the writer's `error.WriteFailed` sets the state to invalid; a failed
|
||||
/// initial allocating-writer allocation falls back to the fixed buffer).
|
||||
pub trait ByteStorage: std::fmt::Debug + Send {
|
||||
/// The bytes accumulated so far.
|
||||
fn bytes(&self) -> &[u8];
|
||||
/// Make room for `additional` more bytes.
|
||||
fn try_reserve(&mut self, additional: usize) -> Result<(), StorageFull>;
|
||||
/// Append bytes; on error nothing is appended.
|
||||
fn try_extend(&mut self, bytes: &[u8]) -> Result<(), StorageFull>;
|
||||
/// Drop the contents and release any memory acquired by growth (Zig
|
||||
/// `reset` deinits the allocating writer).
|
||||
fn reset(&mut self);
|
||||
/// For `Parser: Clone`.
|
||||
fn clone_box(&self) -> Box<dyn ByteStorage>;
|
||||
}
|
||||
|
||||
impl ByteStorage for Vec<u8> {
|
||||
fn bytes(&self) -> &[u8] {
|
||||
self
|
||||
}
|
||||
fn try_reserve(&mut self, additional: usize) -> Result<(), StorageFull> {
|
||||
Vec::try_reserve(self, additional).map_err(|_| StorageFull)
|
||||
}
|
||||
fn try_extend(&mut self, bytes: &[u8]) -> Result<(), StorageFull> {
|
||||
Vec::try_reserve(self, bytes.len()).map_err(|_| StorageFull)?;
|
||||
self.extend_from_slice(bytes);
|
||||
Ok(())
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.clear();
|
||||
}
|
||||
fn clone_box(&self) -> Box<dyn ByteStorage> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Box<dyn ByteStorage> {
|
||||
fn clone(&self) -> Self {
|
||||
self.clone_box()
|
||||
}
|
||||
}
|
||||
|
||||
/// Payload buffer: a plain `Vec` (fast path) or custom allocator-backed
|
||||
/// storage.
|
||||
#[derive(Debug, Clone)]
|
||||
enum Buffer {
|
||||
Vec(Vec<u8>),
|
||||
Custom(Box<dyn ByteStorage>),
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
#[inline]
|
||||
fn bytes(&self) -> &[u8] {
|
||||
match self {
|
||||
Buffer::Vec(v) => v,
|
||||
Buffer::Custom(c) => c.bytes(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn len(&self) -> usize {
|
||||
self.bytes().len()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_extend(&mut self, b: &[u8]) -> Result<(), StorageFull> {
|
||||
match self {
|
||||
Buffer::Vec(v) => {
|
||||
v.extend_from_slice(b);
|
||||
Ok(())
|
||||
}
|
||||
Buffer::Custom(c) => c.try_extend(b),
|
||||
}
|
||||
}
|
||||
|
||||
fn try_reserve(&mut self, n: usize) -> Result<(), StorageFull> {
|
||||
match self {
|
||||
Buffer::Vec(v) => {
|
||||
v.reserve(n);
|
||||
Ok(())
|
||||
}
|
||||
Buffer::Custom(c) => c.try_reserve(n),
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
match self {
|
||||
Buffer::Vec(v) => v.clear(),
|
||||
Buffer::Custom(c) => c.reset(),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_vec(&self) -> Vec<u8> {
|
||||
self.bytes().to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Parser {
|
||||
/// Whether an allocator is available, i.e. whether data longer than
|
||||
@@ -363,7 +471,7 @@ pub struct Parser {
|
||||
pub state: State,
|
||||
|
||||
/// Buffer for temporary storage of OSC data
|
||||
buffer: Vec<u8>,
|
||||
buffer: Buffer,
|
||||
writer: Writer,
|
||||
}
|
||||
|
||||
@@ -381,7 +489,18 @@ impl Parser {
|
||||
Parser {
|
||||
alloc,
|
||||
state: State::Start,
|
||||
buffer: Vec::new(),
|
||||
buffer: Buffer::Vec(Vec::new()),
|
||||
writer: Writer::None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Like [`Parser::init`] with an allocator-backed payload buffer
|
||||
/// (`alloc` is true: an allocator is available).
|
||||
pub fn with_storage(storage: Box<dyn ByteStorage>) -> Parser {
|
||||
Parser {
|
||||
alloc: true,
|
||||
state: State::Start,
|
||||
buffer: Buffer::Custom(storage),
|
||||
writer: Writer::None,
|
||||
}
|
||||
}
|
||||
@@ -389,7 +508,7 @@ impl Parser {
|
||||
/// Reset the parser state.
|
||||
pub fn reset(&mut self) {
|
||||
self.state = State::Start;
|
||||
self.buffer.clear();
|
||||
self.buffer.reset();
|
||||
self.writer = Writer::None;
|
||||
}
|
||||
|
||||
@@ -420,6 +539,12 @@ impl Parser {
|
||||
self.write_to_fixed();
|
||||
return;
|
||||
}
|
||||
// Zig: `Writer.Allocating.initCapacity(alloc, 2048) catch` falls back
|
||||
// to the fixed buffer.
|
||||
if self.buffer.try_reserve(Self::MAX_BUF).is_err() {
|
||||
self.write_to_fixed();
|
||||
return;
|
||||
}
|
||||
self.writer = Writer::Allocating;
|
||||
}
|
||||
|
||||
@@ -438,7 +563,7 @@ impl Parser {
|
||||
self.state = State::Invalid;
|
||||
return None;
|
||||
}
|
||||
Some(self.buffer.clone())
|
||||
Some(self.buffer.to_vec())
|
||||
}
|
||||
|
||||
/// Zig `writer.writeByte(0)` capacity check: when the fixed buffer is
|
||||
@@ -460,7 +585,7 @@ impl Parser {
|
||||
self.state = State::Invalid;
|
||||
return None;
|
||||
}
|
||||
Some(self.buffer.clone())
|
||||
Some(self.buffer.to_vec())
|
||||
}
|
||||
|
||||
/// Whether any payload writer has been set up (Zig `parser.writer != null`).
|
||||
@@ -468,6 +593,15 @@ impl Parser {
|
||||
self.writer != Writer::None
|
||||
}
|
||||
|
||||
/// Append to the buffer; an allocation failure invalidates the parser
|
||||
/// (Zig: `error.WriteFailed` -> `.invalid`).
|
||||
#[inline]
|
||||
fn push_or_invalid(&mut self, bytes: &[u8]) {
|
||||
if self.buffer.try_extend(bytes).is_err() {
|
||||
self.state = State::Invalid;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn write_byte(&mut self, c: u8) {
|
||||
match self.writer {
|
||||
@@ -477,10 +611,10 @@ impl Parser {
|
||||
// Overflow: discard any further input.
|
||||
self.state = State::Invalid;
|
||||
} else {
|
||||
self.buffer.push(c);
|
||||
self.push_or_invalid(&[c]);
|
||||
}
|
||||
}
|
||||
Writer::Allocating => self.buffer.push(c),
|
||||
Writer::Allocating => self.push_or_invalid(&[c]),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,16 +653,16 @@ impl Parser {
|
||||
bytes = rest;
|
||||
}
|
||||
Writer::Allocating => {
|
||||
self.buffer.extend_from_slice(bytes);
|
||||
self.push_or_invalid(bytes);
|
||||
return;
|
||||
}
|
||||
Writer::Fixed => {
|
||||
let room = Self::MAX_BUF.saturating_sub(self.buffer.len());
|
||||
if bytes.len() <= room {
|
||||
self.buffer.extend_from_slice(bytes);
|
||||
self.push_or_invalid(bytes);
|
||||
} else {
|
||||
// Overflow: keep what fits, discard the rest.
|
||||
self.buffer.extend_from_slice(&bytes[..room]);
|
||||
self.push_or_invalid(&bytes[..room]);
|
||||
self.state = State::Invalid;
|
||||
}
|
||||
return;
|
||||
@@ -795,6 +929,56 @@ impl Parser {
|
||||
mod next_slice_tests {
|
||||
use super::*;
|
||||
|
||||
/// Storage that holds at most `cap` bytes (an allocator with a limit).
|
||||
#[derive(Debug, Clone)]
|
||||
struct Limited {
|
||||
data: Vec<u8>,
|
||||
cap: usize,
|
||||
}
|
||||
impl ByteStorage for Limited {
|
||||
fn bytes(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
fn try_reserve(&mut self, additional: usize) -> Result<(), StorageFull> {
|
||||
if self.data.len() + additional > self.cap {
|
||||
return Err(StorageFull);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn try_extend(&mut self, bytes: &[u8]) -> Result<(), StorageFull> {
|
||||
self.try_reserve(bytes.len())?;
|
||||
self.data.extend_from_slice(bytes);
|
||||
Ok(())
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.data.clear();
|
||||
}
|
||||
fn clone_box(&self) -> Box<dyn ByteStorage> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn storage_failure_invalidates_osc() {
|
||||
let mut p = Parser::with_storage(Box::new(Limited {
|
||||
data: Vec::new(),
|
||||
cap: 8,
|
||||
}));
|
||||
for b in b"0;abcdefghijkl" {
|
||||
p.next(*b);
|
||||
}
|
||||
assert_eq!(p.state, State::Invalid);
|
||||
assert!(p.end(Some(0x07)).is_none());
|
||||
|
||||
// Within the limit it parses as usual.
|
||||
p.reset();
|
||||
for b in b"0;abc" {
|
||||
p.next(*b);
|
||||
}
|
||||
let cmd = p.end(Some(0x07)).expect("command");
|
||||
assert!(matches!(cmd, Command::ChangeWindowTitle(t) if t == b"abc"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_slice_matches_next() {
|
||||
for alloc in [false, true] {
|
||||
@@ -811,7 +995,7 @@ mod next_slice_tests {
|
||||
b.next_slice(&input[..mid]);
|
||||
b.next_slice(&input[mid..]);
|
||||
assert_eq!(a.state, b.state);
|
||||
assert_eq!(a.buffer, b.buffer);
|
||||
assert_eq!(a.buffer.bytes(), b.buffer.bytes());
|
||||
assert_eq!(a.writer, b.writer);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user