#![deny(clippy::disallowed_methods, clippy::disallowed_types)]
use std::io::Write;
mod lzo1x;
mod lzo2a;
mod lzx;
pub use lzo1x::Lzo1x;
pub use lzo2a::Lzo2a;
pub use lzx::{LZX_FRAME, LzxDecoder};
#[allow(unsafe_code)]
pub mod oodle;
pub mod oodle_framing;
#[cfg(any(feature = "reference-decoder", not(windows)))]
pub mod reference;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("{codec} at input offset {offset}: {detail}")]
Invalid {
codec: &'static str,
offset: usize,
detail: String,
},
#[error("output size {actual}, expected {expected}")]
Size {
expected: usize,
actual: usize,
},
#[error("allocation/size limit exceeded")]
Limit,
#[error("I/O: {0}")]
Io(#[from] std::io::Error),
#[error("Oodle library: {0}")]
Library(#[from] libloading::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
pub(crate) fn invalid(codec: &'static str, offset: usize, detail: impl ToString) -> Error {
Error::Invalid {
codec,
offset,
detail: detail.to_string(),
}
}
pub(crate) fn check_claim(
codec: &'static str,
input_len: usize,
expected: usize,
max_ratio: usize,
) -> Result<()> {
let most = input_len.saturating_mul(max_ratio);
if expected > most {
return Err(invalid(
codec,
0,
format!(
"the container claims {expected} output bytes, but {input_len} input bytes make at most {most} ({max_ratio}:1)"
),
));
}
Ok(())
}
fn total_in(d: &zlib_rs::Inflate) -> usize {
usize::try_from(d.total_in()).unwrap_or(usize::MAX)
}
fn inflate_error(e: zlib_rs::InflateError) -> &'static str {
match e {
zlib_rs::InflateError::NeedDict { .. } => "requires a dictionary",
zlib_rs::InflateError::StreamError
| zlib_rs::InflateError::DataError
| zlib_rs::InflateError::MemError => "deflate decompression error",
}
}
const WINDOW_BITS: u8 = 15;
static ZERO_WINDOW: [u8; 1 << WINDOW_BITS] = [0; 1 << WINDOW_BITS];
fn inflater(zlib: bool) -> Result<zlib_rs::Inflate> {
let mut d = zlib_rs::Inflate::new(zlib, WINDOW_BITS);
if !zlib {
d.set_dictionary(&ZERO_WINDOW)
.map_err(|e| invalid("deflate", 0, inflate_error(e)))?;
}
Ok(d)
}
fn consumed(input: &[u8], left: usize) -> usize {
input.len().saturating_sub(left)
}
pub(crate) fn buffer(size: usize) -> Result<Vec<u8>> {
let mut out = Vec::new();
out.try_reserve_exact(size).map_err(|_| Error::Limit)?;
out.resize(size, 0);
Ok(out)
}
pub(crate) fn check_size(actual: usize, expected: usize) -> Result<()> {
if actual == expected {
Ok(())
} else {
Err(Error::Size { expected, actual })
}
}
pub trait Decompressor {
fn decompress(&self, input: &[u8], expected: usize) -> Result<Vec<u8>>;
}
pub trait Compressor {
fn compress(&self, input: &[u8]) -> Result<Vec<u8>>;
}
pub const LZ4_MAX_RATIO: usize = 255;
#[derive(Debug, Clone, Copy)]
pub struct Lz4;
impl Decompressor for Lz4 {
fn decompress(&self, input: &[u8], expected: usize) -> Result<Vec<u8>> {
if input.is_empty() {
return Err(invalid("lz4", 0, "missing token: expected at least one byte"));
}
check_claim("lz4", input.len(), expected, LZ4_MAX_RATIO)?;
let mut out = buffer(expected)?;
let n = lz4_flex::block::decompress_into(input, &mut out).map_err(|e| invalid("lz4", 0, e))?;
check_size(n, expected)?;
Ok(out)
}
}
impl Compressor for Lz4 {
fn compress(&self, input: &[u8]) -> Result<Vec<u8>> {
Ok(lz4_flex::block::compress(input))
}
}
#[derive(Debug, Clone, Copy)]
pub struct Lzma;
impl Lzma {
fn run(input: &[u8], size: Option<usize>, limit: usize) -> Result<Option<(Vec<u8>, usize)>> {
struct Bounded {
out: Vec<u8>,
limit: usize,
overflowed: bool,
}
impl Write for Bounded {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
if buf.len() > self.limit.saturating_sub(self.out.len()) {
self.overflowed = true;
return Err(std::io::Error::other("past the expected size"));
}
self.out.try_reserve(buf.len()).map_err(std::io::Error::other)?;
self.out.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let options = lzma_rs::decompress::Options {
unpacked_size: lzma_rs::decompress::UnpackedSize::UseProvided(
size.map(|s| u64::try_from(s).unwrap_or(u64::MAX)),
),
memlimit: None,
allow_incomplete: false,
};
let mut src = std::io::Cursor::new(input);
let mut out = Bounded {
out: Vec::new(),
limit,
overflowed: false,
};
match lzma_rs::lzma_decompress_with_options(&mut src, &mut out, &options) {
Ok(()) => Ok(Some((
out.out,
usize::try_from(src.position()).unwrap_or(usize::MAX),
))),
Err(_) if out.overflowed => Ok(None),
Err(e) => Err(invalid("lzma", usize::try_from(src.position()).unwrap_or(0), e)),
}
}
}
impl Decompressor for Lzma {
fn decompress(&self, input: &[u8], expected: usize) -> Result<Vec<u8>> {
if input.len() < 5 {
return Err(invalid("lzma", 0, "the five property bytes are missing"));
}
let Some((out, used)) = Lzma::run(input, Some(expected), expected)? else {
return Err(Error::Size {
expected,
actual: expected.saturating_add(1),
});
};
check_size(out.len(), expected)?;
if used == input.len() {
return Ok(out);
}
match Lzma::run(input, None, expected) {
Ok(Some((marked, all))) if all == input.len() && marked.len() == expected => Ok(out),
_ => Err(invalid(
"lzma",
used,
format!(
"{} bytes after the payload that are no end-of-payload marker",
input.len().saturating_sub(used)
),
)),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Brotli;
impl Decompressor for Brotli {
fn decompress(&self, input: &[u8], expected: usize) -> Result<Vec<u8>> {
use brotli_decompressor::{BrotliDecompressStream, BrotliResult, BrotliState, StandardAlloc};
let mut state = BrotliState::new(
StandardAlloc::default(),
StandardAlloc::default(),
StandardAlloc::default(),
);
let mut chunk = vec![0; expected.saturating_add(1).min(1 << 16)];
let mut out = Vec::new();
let (mut available_in, mut input_offset, mut total_out) = (input.len(), 0, 0);
loop {
let (mut available_out, mut output_offset) = (chunk.len(), 0);
let result = BrotliDecompressStream(
&mut available_in,
&mut input_offset,
input,
&mut available_out,
&mut output_offset,
&mut chunk,
&mut total_out,
&mut state,
);
let made = chunk.get(..output_offset).unwrap_or_default();
let total = out.len().checked_add(made.len()).ok_or(Error::Limit)?;
if total > expected {
return Err(Error::Size {
expected,
actual: total,
});
}
out.try_reserve(made.len()).map_err(|_| Error::Limit)?;
out.extend_from_slice(made);
match result {
BrotliResult::ResultSuccess => break,
BrotliResult::NeedsMoreOutput => {}
BrotliResult::NeedsMoreInput => {
return Err(invalid("brotli", input_offset, "the stream ends early"));
}
BrotliResult::ResultFailure => {
return Err(invalid("brotli", input_offset, "malformed stream"));
}
}
}
if input_offset != input.len() {
return Err(invalid(
"brotli",
input_offset,
format!(
"{} bytes after the stream",
input.len().saturating_sub(input_offset)
),
));
}
check_size(out.len(), expected)?;
Ok(out)
}
}
#[derive(Debug, Clone, Copy)]
pub struct Zstd;
impl Decompressor for Zstd {
fn decompress(&self, input: &[u8], expected: usize) -> Result<Vec<u8>> {
use ruzstd::{BlockDecodingStrategy, FrameDecoder};
use std::io::Read;
let (frame, _) = ruzstd::frame::read_frame_header(input).map_err(|e| invalid("zstd", 0, e))?;
if let Some(id) = frame.header.dictionary_id() {
return Err(invalid("zstd", 0, format!("frame needs dictionary {id}")));
}
let descriptor = &frame.header.descriptor;
let states_size = descriptor.single_segment_flag()
|| descriptor
.frame_content_size_bytes()
.map_err(|e| invalid("zstd", 0, e))?
> 0;
if states_size {
let stated = usize::try_from(frame.header.frame_content_size()).map_err(|_| Error::Limit)?;
check_size(stated, expected)?;
}
let mut d = FrameDecoder::new();
let mut src = input;
d.init(&mut src).map_err(|e| invalid("zstd", 0, e))?;
let mut out = Vec::new();
loop {
let before = src.len();
d.decode_blocks(&mut src, BlockDecodingStrategy::UptoBlocks(1))
.map_err(|e| invalid("zstd", consumed(input, before), e))?;
let ready = d.can_collect();
let total = out.len().checked_add(ready).ok_or(Error::Limit)?;
if total > expected {
return Err(Error::Size {
expected,
actual: total,
});
}
let at = out.len();
out.try_reserve(ready).map_err(|_| Error::Limit)?;
out.resize(total, 0);
let fresh = out.get_mut(at..).ok_or_else(|| {
invalid(
"zstd",
consumed(input, src.len()),
"output shorter than collected",
)
})?;
d.read_exact(fresh)?;
if d.is_finished() && d.can_collect() == 0 {
break;
}
if src.len() == before && ready == 0 {
return Err(invalid(
"zstd",
consumed(input, src.len()),
"frame ends before its last block: expected another block",
));
}
}
if !src.is_empty() {
return Err(invalid(
"zstd",
consumed(input, src.len()),
"trailing input after the frame: expected the frame to end here",
));
}
if let (Some(stored), Some(computed)) = (d.get_checksum_from_data(), d.get_calculated_checksum())
&& stored != computed
{
return Err(invalid(
"zstd",
input.len(),
format!("content checksum {stored:08x}, computed {computed:08x}"),
));
}
check_size(out.len(), expected)?;
Ok(out)
}
}
#[derive(Debug, Clone, Copy)]
pub struct Deflate;
#[derive(Debug, Clone, Copy)]
pub struct Zlib;
#[derive(Debug, Clone, Copy)]
pub struct ZlibUnterminated;
fn inflate(input: &[u8], expected: usize, zlib: bool, incomplete: bool) -> Result<Vec<u8>> {
check_claim("deflate/zlib", input.len(), expected, DEFLATE_MAX_RATIO)?;
let mut d = inflater(zlib)?;
let mut out = buffer(expected.checked_add(1).ok_or(Error::Limit)?)?;
let status = d
.decompress(input, &mut out, zlib_rs::InflateFlush::NoFlush)
.map_err(|e| invalid("deflate/zlib", total_in(&d), inflate_error(e)))?;
check_size(usize::try_from(d.total_out()).unwrap_or(usize::MAX), expected)?;
if total_in(&d) != input.len() {
return Err(invalid(
"deflate/zlib",
total_in(&d),
"trailing input: expected the stream to end here",
));
}
if status != zlib_rs::Status::StreamEnd && !incomplete {
return Err(invalid("deflate/zlib", input.len(), "missing stream end"));
}
if zlib && input.len() < 2 {
return Err(invalid("zlib", input.len(), "short header"));
}
out.truncate(expected);
Ok(out)
}
impl Decompressor for Deflate {
fn decompress(&self, input: &[u8], expected: usize) -> Result<Vec<u8>> {
inflate(input, expected, false, false)
}
}
pub const DEFLATE_MAX_RATIO: usize = 1032;
pub const LZO1X_MAX_RATIO: usize = 255;
pub const LZO2A_MAX_RATIO: usize = 255;
impl Deflate {
pub fn decompress_up_to(&self, input: &[u8], max: usize) -> Result<Vec<u8>> {
let mut d = inflater(false)?;
let mut out: Vec<u8> = Vec::new();
loop {
if out.len() == out.capacity() {
let want = out
.capacity()
.max(4096)
.saturating_mul(2)
.min(max.saturating_add(1));
let more = want.saturating_sub(out.len());
if more == 0 {
return Err(Error::Size {
expected: max,
actual: out.len(),
});
}
out.try_reserve_exact(more).map_err(|_| Error::Limit)?;
}
let taken = total_in(&d);
let rest = input.get(taken..).unwrap_or_default();
let before = (d.total_in(), d.total_out());
let at = out.len();
out.resize(out.capacity(), 0);
let result = match out.get_mut(at..) {
Some(room) => d.decompress(rest, room, zlib_rs::InflateFlush::NoFlush),
None => Ok(zlib_rs::Status::BufError),
};
let produced = usize::try_from(d.total_out().saturating_sub(before.1)).unwrap_or(usize::MAX);
out.truncate(at.saturating_add(produced));
let status = result.map_err(|e| invalid("deflate", taken, inflate_error(e)))?;
if out.len() > max {
return Err(Error::Size {
expected: max,
actual: out.len(),
});
}
if status == zlib_rs::Status::StreamEnd {
break;
}
if (d.total_in(), d.total_out()) == before && out.len() < out.capacity() {
return Err(invalid("deflate", input.len(), "missing stream end"));
}
}
if total_in(&d) != input.len() {
return Err(invalid(
"deflate",
total_in(&d),
"trailing input: expected the stream to end here",
));
}
Ok(out)
}
}
impl Decompressor for Zlib {
fn decompress(&self, input: &[u8], expected: usize) -> Result<Vec<u8>> {
inflate(input, expected, true, false)
}
}
impl Decompressor for ZlibUnterminated {
fn decompress(&self, input: &[u8], expected: usize) -> Result<Vec<u8>> {
inflate(input, expected, true, true)
}
}
pub struct Inflater {
d: zlib_rs::Inflate,
codec: &'static str,
ended: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Inflated {
pub consumed: usize,
pub produced: usize,
pub ended: bool,
}
impl std::fmt::Debug for Inflater {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Inflater")
.field("codec", &self.codec)
.field("total_in", &self.d.total_in())
.field("total_out", &self.d.total_out())
.field("ended", &self.ended)
.finish()
}
}
impl Inflater {
pub fn zlib() -> Result<Self> {
Ok(Self {
d: inflater(true)?,
codec: "zlib",
ended: false,
})
}
pub fn deflate() -> Result<Self> {
Ok(Self {
d: inflater(false)?,
codec: "deflate",
ended: false,
})
}
pub fn inflate(&mut self, input: &[u8], output: &mut [u8]) -> Result<Inflated> {
if self.ended {
return Ok(Inflated {
consumed: 0,
produced: 0,
ended: true,
});
}
let (before_in, before_out) = (self.d.total_in(), self.d.total_out());
let status = self
.d
.decompress(input, output, zlib_rs::InflateFlush::SyncFlush)
.map_err(|e| invalid(self.codec, total_in(&self.d), inflate_error(e)))?;
self.ended = status == zlib_rs::Status::StreamEnd;
let moved =
|after: u64, before: u64| usize::try_from(after.saturating_sub(before)).unwrap_or(usize::MAX);
Ok(Inflated {
consumed: moved(self.d.total_in(), before_in),
produced: moved(self.d.total_out(), before_out),
ended: self.ended,
})
}
pub fn total_in(&self) -> usize {
total_in(&self.d)
}
pub fn total_out(&self) -> usize {
usize::try_from(self.d.total_out()).unwrap_or(usize::MAX)
}
}
impl Compressor for Deflate {
fn compress(&self, input: &[u8]) -> Result<Vec<u8>> {
let mut e = flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::default());
e.write_all(input)?;
Ok(e.finish()?)
}
}
impl Compressor for Zlib {
fn compress(&self, input: &[u8]) -> Result<Vec<u8>> {
let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
e.write_all(input)?;
Ok(e.finish()?)
}
}