#[derive(Debug, thiserror::Error)]
pub enum NdrError {
#[error("NDR buffer underrun: need {need} bytes at offset {pos}")]
Underrun { need: usize, pos: usize },
}
pub type Result<T> = std::result::Result<T, NdrError>;
#[derive(Default)]
pub struct NdrEncoder {
buf: Vec<u8>,
next_referent: u32,
}
impl NdrEncoder {
pub fn new() -> Self {
NdrEncoder {
buf: Vec::new(),
next_referent: 0x0002_0000,
}
}
pub fn align(&mut self, a: usize) {
if a <= 1 {
return;
}
while self.buf.len() % a != 0 {
self.buf.push(0);
}
}
pub fn u8(&mut self, v: u8) {
self.buf.push(v);
}
pub fn u16(&mut self, v: u16) {
self.align(2);
self.buf.extend_from_slice(&v.to_le_bytes());
}
pub fn u32(&mut self, v: u32) {
self.align(4);
self.buf.extend_from_slice(&v.to_le_bytes());
}
pub fn u64(&mut self, v: u64) {
self.align(8);
self.buf.extend_from_slice(&v.to_le_bytes());
}
pub fn bytes(&mut self, b: &[u8]) {
self.buf.extend_from_slice(b);
}
pub fn uuid(&mut self, b: &[u8; 16]) {
self.align(4);
self.buf.extend_from_slice(b);
}
pub fn referent(&mut self) -> u32 {
let id = self.next_referent;
self.next_referent += 4;
self.u32(id);
id
}
pub fn null_ptr(&mut self) {
self.u32(0);
}
pub fn conformant_varying_wstr(&mut self, s: &str) {
let mut units: Vec<u16> = s.encode_utf16().collect();
units.push(0);
let n = units.len() as u32;
self.u32(n); self.u32(0); self.u32(n); for u in units {
self.u16(u);
}
}
pub fn into_bytes(self) -> Vec<u8> {
self.buf
}
pub fn len(&self) -> usize {
self.buf.len()
}
pub fn is_empty(&self) -> bool {
self.buf.is_empty()
}
}
pub struct NdrDecoder<'a> {
buf: &'a [u8],
pos: usize,
}
impl<'a> NdrDecoder<'a> {
pub fn new(buf: &'a [u8]) -> Self {
NdrDecoder { buf, pos: 0 }
}
pub fn align(&mut self, a: usize) {
if a <= 1 {
return;
}
let remainder = self.pos % a;
if remainder != 0 {
self.pos = self.pos.saturating_add(a - remainder);
}
}
fn take(&mut self, n: usize) -> Result<&'a [u8]> {
let end = self.pos.checked_add(n).ok_or(NdrError::Underrun {
need: n,
pos: self.pos,
})?;
if end > self.buf.len() {
return Err(NdrError::Underrun {
need: n,
pos: self.pos,
});
}
let s = &self.buf[self.pos..end];
self.pos = end;
Ok(s)
}
pub fn u8(&mut self) -> Result<u8> {
Ok(self.take(1)?[0])
}
pub fn u16(&mut self) -> Result<u16> {
self.align(2);
Ok(u16::from_le_bytes(self.take(2)?.try_into().unwrap()))
}
pub fn u32(&mut self) -> Result<u32> {
self.align(4);
Ok(u32::from_le_bytes(self.take(4)?.try_into().unwrap()))
}
pub fn u64(&mut self) -> Result<u64> {
self.align(8);
Ok(u64::from_le_bytes(self.take(8)?.try_into().unwrap()))
}
pub fn uuid(&mut self) -> Result<[u8; 16]> {
self.align(4);
Ok(self.take(16)?.try_into().unwrap())
}
pub fn conformant_varying_wstr(&mut self) -> Result<String> {
let max = self.u32()?;
let offset = self.u32()?;
let actual = self.u32()? as usize;
if offset > max || actual > (max - offset) as usize {
return Err(NdrError::Underrun {
need: actual,
pos: self.pos,
});
}
let need = actual.checked_mul(2).ok_or(NdrError::Underrun {
need: usize::MAX,
pos: self.pos,
})?;
if need > self.remaining() {
return Err(NdrError::Underrun {
need,
pos: self.pos,
});
}
let mut units = Vec::with_capacity(actual);
for _ in 0..actual {
units.push(self.u16()?);
}
while units.last() == Some(&0) {
units.pop();
}
Ok(String::from_utf16_lossy(&units))
}
pub fn read_bytes(&mut self, n: usize) -> Result<&'a [u8]> {
self.take(n)
}
pub fn remaining(&self) -> usize {
self.buf.len().saturating_sub(self.pos)
}
pub fn position(&self) -> usize {
self.pos
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn primitive_alignment() {
let mut e = NdrEncoder::new();
e.u8(0x01);
e.u32(0xAABB_CCDD); assert_eq!(e.into_bytes(), vec![0x01, 0, 0, 0, 0xDD, 0xCC, 0xBB, 0xAA]);
}
#[test]
fn wstr_roundtrip() {
let mut e = NdrEncoder::new();
e.conformant_varying_wstr("ADHAMMER");
let bytes = e.into_bytes();
assert_eq!(bytes.len(), 30);
let mut d = NdrDecoder::new(&bytes);
assert_eq!(d.conformant_varying_wstr().unwrap(), "ADHAMMER");
assert_eq!(d.remaining(), 0);
}
#[test]
fn referent_ids_are_nonzero_and_advance() {
let mut e = NdrEncoder::new();
let a = e.referent();
let b = e.referent();
assert_ne!(a, 0);
assert_eq!(b, a + 4);
}
#[test]
fn underrun_reports_offset() {
let bytes = [0x01, 0x02]; let mut d = NdrDecoder::new(&bytes);
assert!(matches!(
d.u32(),
Err(NdrError::Underrun { need: 4, pos: 0 })
));
}
#[test]
fn wstr_rejects_hostile_actual_count() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&0u32.to_le_bytes()); bytes.extend_from_slice(&0u32.to_le_bytes()); bytes.extend_from_slice(&0x7FFF_FFFFu32.to_le_bytes()); let mut d = NdrDecoder::new(&bytes);
let start = std::time::Instant::now();
let r = d.conformant_varying_wstr();
let elapsed_ms = start.elapsed().as_millis();
assert!(
matches!(r, Err(NdrError::Underrun { .. })),
"expected Underrun, got {r:?}"
);
assert!(
elapsed_ms < 50,
"preflight must reject in <50ms, took {elapsed_ms}ms — probably allocated"
);
}
#[test]
fn wstr_rejects_actual_count_overflow() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&0u32.to_le_bytes());
bytes.extend_from_slice(&0u32.to_le_bytes());
bytes.extend_from_slice(&0xFFFF_FFFFu32.to_le_bytes()); let mut d = NdrDecoder::new(&bytes);
assert!(matches!(
d.conformant_varying_wstr(),
Err(NdrError::Underrun { .. })
));
}
#[test]
fn zero_alignment_is_a_noop() {
let mut e = NdrEncoder::new();
e.u8(1);
e.align(0);
assert_eq!(e.into_bytes(), vec![1]);
let bytes = [1u8];
let mut d = NdrDecoder::new(&bytes);
d.align(0);
assert_eq!(d.u8().unwrap(), 1);
}
#[test]
fn alignment_past_end_reports_underrun() {
let bytes = [1u8];
let mut d = NdrDecoder::new(&bytes);
assert_eq!(d.u8().unwrap(), 1);
d.align(4);
assert_eq!(d.remaining(), 0);
assert!(matches!(
d.u32(),
Err(NdrError::Underrun { need: 4, pos: 4 })
));
}
#[test]
fn wstr_rejects_inconsistent_varying_bounds() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&2u32.to_le_bytes()); bytes.extend_from_slice(&2u32.to_le_bytes()); bytes.extend_from_slice(&1u32.to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes());
let mut d = NdrDecoder::new(&bytes);
assert!(d.conformant_varying_wstr().is_err());
}
}