Skip to main content

SlotWriter

Struct SlotWriter 

Source
pub struct SlotWriter<'a> { /* private fields */ }
Expand description

Append-shaped writing into the fixed slot handed to a sized new_with.

The sized constructors hand out a slot whose length is already decided, which is what makes growth — and therefore the reallocation hazard — inexpressible. SlotWriter restores the append shape on top of that, so concatenating a wire format does not mean writing offsets by hand. push_slice panics on overrun and leaves the slot untouched; the std-gated Write path cannot do that and stay inside Write’s contract, so it truncates and reports the refusal on a later call. Debug shows the cursor — position, remaining, length — and never the bytes. A write cursor over a fixed-length slot.

Writes advance a position and panic on overrun rather than truncating or wrapping — running past the end of a secret’s buffer is a bug, not a condition to recover from, and the panic names the shortfall.

The slot is not owned here. Zeroization remains the wrapper’s, so a SlotWriter borrowed from inside new_with inherits the same panic-safety: unwinding drops the wrapper’s Zeroizing, which wipes whatever was written.

§Examples

RFC 9180-style labeled concatenation, where the length is the sum of the parts. Here every part has a length the compiler knows, so the total is a constant and the wrapper is a Fixed:

use secure_gate::{Fixed, RevealSecret, SlotWriter};

const LABEL: &[u8] = b"HPKE-v1";
let suite_id = [0xAAu8; 3];
let ikm = [0xBBu8; 6];

let secret = Fixed::<[u8; 16]>::new_with(|slot| {
    let mut w = SlotWriter::new(slot);
    w.push_slice(LABEL);
    w.push_slice(&suite_id);
    w.push_slice(&ikm);
    assert!(w.is_full());
});

assert_eq!(&secret.expose_secret()[..7], LABEL);

The same concatenation when the parts are sized at runtime. This is the commoner shape for a real wire format — a suite identifier and an IKM that arrived over the network have no length the compiler can see — and the only thing that changes is which constructor takes the sum: Dynamic::new_with accepts the total as a value, so SlotWriter serves it exactly as it serves the constant case. There is no reason to reach for an oversized Fixed plus a separate logical length:

use secure_gate::{Dynamic, RevealSecret, SlotWriter};

const LABEL: &[u8] = b"HPKE-v1";
let suite_id: Vec<u8> = vec![0xAA; 3];   // lengths known only at run time
let ikm: Vec<u8> = vec![0xBB; 6];

let total = LABEL.len() + suite_id.len() + ikm.len();

let secret = Dynamic::<Vec<u8>>::new_with(total, |slot| {
    let mut w = SlotWriter::new(slot);
    w.push_slice(LABEL);
    w.push_slice(&suite_id);
    w.push_slice(&ikm);
    assert!(w.is_full());   // the sum and the writes agreed
});

assert_eq!(secret.expose_secret().len(), total);
assert_eq!(&secret.expose_secret()[..7], LABEL);

§The zero tail is inherited, not imposed

A SlotWriter never clears anything. It copies the bytes it is handed and leaves every other byte exactly as it found it, so what an unwritten tail holds is a fact about the slot you passed to new, not a guarantee this type supplies.

Over a slot from a sized constructor that fact is zeros, by documented contract rather than by luck: Fixed::new_with starts from SentinelValue::sentinel_value, which is [0u8; N] for a byte array, and Dynamic::new_with allocates len zero bytes and documents the zero fill as input callers may rely on. For those two slots — and for a stack buffer you zeroed yourself — a short write is not undefined; it is a zero tail, which is frequently what a format wants:

use secure_gate::{Fixed, RevealSecret, SlotWriter};

let key = Fixed::<[u8; 16]>::new_with(|slot| {
    let mut w = SlotWriter::new(slot);
    w.push_slice(&[1, 2, 3, 4, 5]);   // 40-bit key, zero-padded to 16
});
assert_eq!(&key.expose_secret()[5..], &[0u8; 11]);

Over any other slot the tail is whatever was there before, and two of those are traps. One is a scratch buffer reused for a second key. The other is rewriting a wrapper in place — SlotWriter::new(&mut secret.expose_secret_mut()[..]) — where the bytes already in the slot are the previous secret. Write fewer bytes than the previous pass did and the untouched tail is stale key material that the next derivation consumes as input: a wrong key that still runs, which is the failure mode this type exists to remove rather than to introduce by a different route. Build a new wrapper with a sized new_with instead of rewriting one in place; failing that, zero the slot before wrapping it, or write it to is_full so that no byte of the old contents survives.

§Debug shows the cursor, never the slot

{:?} prints position, remaining and len, and no byte of the slot — see the Debug impl below for the exact rendering and for why it is hand-written.

Implementations§

Source§

impl<'a> SlotWriter<'a>

Source

pub fn new(slot: &'a mut [u8]) -> Self

Wraps a slot, positioned at its start.

§Precondition: a zeroed slot, unless every byte will be written

Any &mut [u8] is accepted and nothing here zeroes it, so the slot’s current contents survive as the tail of whatever you write. Either write the slot to is_full, in which case nothing of the old contents is left, or pass a slot that is already zeroed — the ones Fixed::new_with and Dynamic::new_with hand their closures both are, by documented contract. Neither, and an unwritten tail holds whatever the buffer held before, which for a reused scratch buffer or a wrapper being rewritten in place is the previous secret. The type-level section The zero tail is inherited, not imposed spells out what that costs.

Source

pub fn push_slice(&mut self, bytes: &[u8])

Appends bytes and advances past them.

§Panics

If bytes does not fit in the remaining space. The message names how much was wanted and how much was left, because the useful question when this fires is which length calculation was wrong.

Source

pub fn push_byte(&mut self, byte: u8)

Appends one byte and advances past it.

§Panics

If the slot is already full.

Source

pub fn position(&self) -> usize

Bytes written so far.

Source

pub fn remaining(&self) -> usize

Bytes still unwritten.

These keep whatever the slot already held — zeros when the slot came from Fixed::new_with or Dynamic::new_with, which is the case the zero-tail contract covers, and the buffer’s previous contents otherwise. See The zero tail is inherited, not imposed.

Source

pub fn is_full(&self) -> bool

Whether every byte of the slot has been written.

Useful as an assertion at the end of a wire-format build, where a slot that is not full usually means a length calculation disagreed with the writes.

Trait Implementations§

Source§

impl Debug for SlotWriter<'_>

Prints cursor state — position, remaining, len — and never a byte of the slot.

Hand-written rather than derived, and the difference is not cosmetic. The one data field here is slot: &'a mut [u8], so a derived Debug printed the borrowed bytes verbatim: a writer six bytes into an eight-byte slot rendered as SlotWriter { slot: [222, 173, 190, 239, 202, 254, 0, 0], pos: 6 } — live secret material, in decimal, in whatever log line the {:?} went to. A SlotWriter is alive exactly while the slot holds that material, and the case this type is built for, a wire format whose offsets are hard to get right, is precisely the case where someone reaches for dbg!. Every other secret-bearing type in the crate hand-writes Debug for the same reason; Fixed, Dynamic and EncodedSecret print a flat [REDACTED].

This one prints the cursor rather than [REDACTED] because the cursor is the thing worth seeing when an offset is wrong, and it is not secret material: len is the slot length the caller chose and passed to the constructor, and position counts the bytes that caller wrote. None of the three numbers depends on the value of any byte, written or unwritten — which is what makes the output safe to put in a log and safe to rely on.

use secure_gate::{Fixed, SlotWriter};

let secret = Fixed::<[u8; 8]>::new_with(|slot| {
    let mut w = SlotWriter::new(slot);
    w.push_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE]);

    let shown = format!("{:?}", w);
    assert_eq!(shown, "SlotWriter { position: 6, remaining: 2, len: 8 }");
    assert!(!shown.contains("222"), "0xDE is 222, which the derived impl printed");
});
assert_eq!(format!("{:?}", secret), "[REDACTED]");
Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Write for SlotWriter<'_>

Available on crate feature std only.

Writes into the slot, failing rather than panicking when it is full.

Present so a slot can be handed to code that writes through std::io::Write — a serializer, an encoder, a hasher’s output sink. Overrun is reported as ErrorKind::WriteZero rather than as a panic, because a Write implementation returning an error is how that interface expects to refuse.

Note the difference from push_slice, which panics: this path exists for foreign code that cannot be asked to respect a length, so an error is the only way to tell it to stop.

§Which call reports the overrun, and what the slot holds when it does

Panic-versus-error is not the whole of the difference. The other half is when the refusal arrives and what is in the slot by then. push_slice checks the whole length before it copies anything, so an overrun there leaves the slot byte-for-byte as it was. write cannot do that and stay inside its contract: Write::write is defined to accept a prefix and report how much it took, so an oversized buf is a short write — min(buf.len(), remaining()) bytes are copied, Ok(n) comes back, and WriteZero appears only on a later call, once the slot has no room left at all.

CallOversized payloadSlot afterwards
push_slicepanics, naming the shortfalluntouched — the check runs before any copy
write_allErr of kind WriteZeroholds the bytes that fit
writeOk(n) with n < buf.len(), no errorholds the bytes that fit

So write_all is the call that surfaces overrun as an error, and it is the one to prefer. A bare write whose return value is ignored surfaces nothing at all: the weaker signal in the likelier case, since dropping a write count on the floor is the mistake write_all exists to prevent.

Either way, and unlike push_slice, a fill driven through this impl can leave a partial value in the slot: the bytes that fit, with whatever the slot already held behind them. That is indistinguishable, by inspection of the slot, from a deliberate short write. Assert the length you meant to reach instead — is_full when the writes were supposed to fill the slot, position or remaining when they were not — which is the same post-condition the push_slice example asserts, applied to a fill you did not write yourself:

use std::io::{ErrorKind, Write};
use secure_gate::{Fixed, RevealSecret, SlotWriter};

let truncated = Fixed::<[u8; 4]>::new_with(|slot| {
    let mut w = SlotWriter::new(slot);

    let err = w.write_all(&[1, 2, 3, 4, 5, 6]).unwrap_err();
    assert_eq!(err.kind(), ErrorKind::WriteZero);

    // The slot is NOT untouched: four bytes were taken before the refusal.
    assert!(w.is_full());
    assert_eq!(w.remaining(), 0);
});
assert_eq!(truncated.expose_secret(), &[1, 2, 3, 4]);
Source§

fn write(&mut self, buf: &[u8]) -> Result<usize>

Copies min(buf.len(), remaining()) bytes and returns that count.

An oversized buf is a short write, not an error: the return value is the only place the shortfall is reported, so a caller that ignores it gets a silently truncated slot. WriteZero is returned only when space is wanted and none remains; an empty buf wants none and always succeeds with Ok(0).

Source§

fn flush(&mut self) -> Result<()>

Flushes this output stream, ensuring that all intermediately buffered contents reach their destination. Read more
1.36.0 · Source§

fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> Result<usize, Error>

Like write, except that it writes from a slice of buffers. Read more
Source§

fn is_write_vectored(&self) -> bool

🔬This is a nightly-only experimental API. (can_vector)
Determines if this Writer has an efficient write_vectored implementation. Read more
1.0.0 · Source§

fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>

Attempts to write an entire buffer into this writer. Read more
Source§

fn write_all_vectored(&mut self, bufs: &mut [IoSlice<'_>]) -> Result<(), Error>

🔬This is a nightly-only experimental API. (write_all_vectored)
Attempts to write multiple buffers into this writer. Read more
1.0.0 · Source§

fn write_fmt(&mut self, args: Arguments<'_>) -> Result<(), Error>

Writes a formatted string into this writer, returning any error encountered. Read more
1.0.0 · Source§

fn by_ref(&mut self) -> &mut Self
where Self: Sized,

Creates a “by reference” adapter for this instance of Write. Read more

Auto Trait Implementations§

§

impl<'a> !UnwindSafe for SlotWriter<'a>

§

impl<'a> Freeze for SlotWriter<'a>

§

impl<'a> RefUnwindSafe for SlotWriter<'a>

§

impl<'a> Send for SlotWriter<'a>

§

impl<'a> Sync for SlotWriter<'a>

§

impl<'a> Unpin for SlotWriter<'a>

§

impl<'a> UnsafeUnpin for SlotWriter<'a>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.