barehttp 0.1.0

Blocking HTTP/1.1 client for no_std + alloc
Documentation
//! Gzip member parse + inflate (RFC 1952).

use super::DecompressError;
use super::crc32::crc32;
use super::inflate::{self, RunningChecksum};
use alloc::vec::Vec;

const ID1: u8 = 0x1f;
const ID2: u8 = 0x8b;
const CM_DEFLATE: u8 = 8;

const FHCRC: u8 = 1 << 1;
const FEXTRA: u8 = 1 << 2;
const FNAME: u8 = 1 << 3;
const FCOMMENT: u8 = 1 << 4;
const RESERVED: u8 = 0xe0;

/// Locate the DEFLATE payload and its absolute start offset in `data`.
fn member_deflate(data: &[u8]) -> Result<(&[u8], usize), DecompressError> {
  let mut i = 0usize;

  let id1 = *data.get(i).ok_or(DecompressError::InvalidInput)?;
  i = i.saturating_add(1);
  let id2 = *data.get(i).ok_or(DecompressError::InvalidInput)?;
  i = i.saturating_add(1);
  if id1 != ID1 || id2 != ID2 {
    return Err(DecompressError::InvalidInput);
  }

  let cm = *data.get(i).ok_or(DecompressError::InvalidInput)?;
  i = i.saturating_add(1);
  if cm != CM_DEFLATE {
    return Err(DecompressError::InvalidInput);
  }

  let flg = *data.get(i).ok_or(DecompressError::InvalidInput)?;
  i = i.saturating_add(1);
  // RFC 1952 ยง2.3.1.2: reserved FLG bits MUST be zero.
  if flg & RESERVED != 0 {
    return Err(DecompressError::InvalidInput);
  }

  // MTIME (4) + XFL (1) + OS (1)
  i = i.checked_add(6).ok_or(DecompressError::InvalidInput)?;
  if i > data.len() {
    return Err(DecompressError::InvalidInput);
  }

  if flg & FEXTRA != 0 {
    let b0 = *data.get(i).ok_or(DecompressError::InvalidInput)?;
    let b1 = *data
      .get(i.saturating_add(1))
      .ok_or(DecompressError::InvalidInput)?;
    let xlen = usize::from(u16::from_le_bytes([b0, b1]));
    i = i
      .checked_add(2)
      .and_then(|v| v.checked_add(xlen))
      .ok_or(DecompressError::InvalidInput)?;
    if i > data.len() {
      return Err(DecompressError::InvalidInput);
    }
  }

  if flg & FNAME != 0 {
    i = skip_cstr(data, i)?;
  }
  if flg & FCOMMENT != 0 {
    i = skip_cstr(data, i)?;
  }

  if flg & FHCRC != 0 {
    let header = data.get(..i).ok_or(DecompressError::InvalidInput)?;
    let expect = u16::try_from(crc32(header) & 0xffff).map_err(|_| DecompressError::InvalidInput)?;
    let b0 = *data.get(i).ok_or(DecompressError::InvalidInput)?;
    let b1 = *data
      .get(i.saturating_add(1))
      .ok_or(DecompressError::InvalidInput)?;
    let got = u16::from_le_bytes([b0, b1]);
    if got != expect {
      return Err(DecompressError::InvalidInput);
    }
    i = i.checked_add(2).ok_or(DecompressError::InvalidInput)?;
  }

  let deflate = data.get(i..).ok_or(DecompressError::InvalidInput)?;
  Ok((deflate, i))
}

fn check_trailer(
  data: &[u8],
  trailer_off: usize,
  out: &[u8],
  crc: RunningChecksum,
) -> Result<(), DecompressError> {
  let trailer = data
    .get(trailer_off..trailer_off.saturating_add(8))
    .ok_or(DecompressError::InvalidInput)?;
  let crc_got = u32::from_le_bytes([
    *trailer.first().ok_or(DecompressError::InvalidInput)?,
    *trailer.get(1).ok_or(DecompressError::InvalidInput)?,
    *trailer.get(2).ok_or(DecompressError::InvalidInput)?,
    *trailer.get(3).ok_or(DecompressError::InvalidInput)?,
  ]);
  let isize = u32::from_le_bytes([
    *trailer.get(4).ok_or(DecompressError::InvalidInput)?,
    *trailer.get(5).ok_or(DecompressError::InvalidInput)?,
    *trailer.get(6).ok_or(DecompressError::InvalidInput)?,
    *trailer.get(7).ok_or(DecompressError::InvalidInput)?,
  ]);

  if crc_got != crc.crc_value() {
    return Err(DecompressError::InvalidInput);
  }
  #[allow(clippy::cast_possible_truncation)] // ISIZE is defined as len mod 2^32
  let isize_expect = out.len() as u32;
  if isize != isize_expect {
    return Err(DecompressError::InvalidInput);
  }
  Ok(())
}

/// Decompress one gzip member (allocating; public API / Callgrind path).
pub(super) fn decompress_member_owned(
  data: &[u8],
  max_out: usize,
) -> Result<Vec<u8>, DecompressError> {
  let (deflate, i) = member_deflate(data)?;
  // Post-pass CRC: the full output is already contiguous here. Incremental
  // updates during inflate help the streaming/`into` path; on tiny members
  // (Callgrind `hello`) they add call overhead for no memory win.
  let mut none = RunningChecksum::None;
  let (out, consumed) = inflate::inflate_owned(deflate, max_out, &mut none)?;
  let trailer_off = i
    .checked_add(consumed)
    .ok_or(DecompressError::InvalidInput)?;
  let crc = RunningChecksum::Crc(crc32(&out));
  check_trailer(data, trailer_off, &out, crc)?;
  Ok(out)
}

/// Decompress one gzip member into `out` (cleared on entry when non-empty / pooled).
pub(super) fn decompress_member(
  data: &[u8],
  max_out: usize,
  out: &mut Vec<u8>,
) -> Result<(), DecompressError> {
  // Drop stale bytes before header checks; skip when `out` is a fresh empty Vec
  // so the inflate `with_capacity` fast path stays intact.
  if !out.is_empty() {
    out.clear();
  }
  let (deflate, i) = member_deflate(data)?;
  let mut crc = RunningChecksum::crc();
  let consumed = inflate::inflate(deflate, max_out, &mut crc, out)?;
  let trailer_off = i
    .checked_add(consumed)
    .ok_or(DecompressError::InvalidInput)?;
  check_trailer(data, trailer_off, out, crc)
}

fn skip_cstr(
  data: &[u8],
  mut i: usize,
) -> Result<usize, DecompressError> {
  loop {
    let b = *data.get(i).ok_or(DecompressError::InvalidInput)?;
    i = i.checked_add(1).ok_or(DecompressError::InvalidInput)?;
    if b == 0 {
      return Ok(i);
    }
  }
}