pgdumpx 0.2.0

Read-only, bounded inspection, extraction, and row scanning for PostgreSQL custom-format dumps
Documentation
use crate::{
    Limits, PgDumpError,
    custom::primitives::{
        ArchiveIntegerSize, ArchiveOffset, ArchiveOffsetSize, read_archive_integer,
        read_archive_offset, read_archive_string,
    },
    io::archive_reader::ArchiveReader,
};
use std::{
    cell::Cell,
    error::Error as _,
    io::{self, Cursor, Read},
    rc::Rc,
};

#[test]
fn exact_read_succeeds_across_partial_reads_and_tracks_offset() {
    let mut reader = ArchiveReader::new(ChunkedReader::new(b"abcd", 2));
    let mut output = [0_u8; 4];

    reader.read_exact(&mut output).unwrap();

    assert_eq!(&output, b"abcd");
    assert_eq!(reader.offset(), 4);
}

#[test]
fn exact_read_reports_short_read_at_first_missing_byte() {
    let mut reader = ArchiveReader::new(Cursor::new(b"ab"));
    let mut output = [0_u8; 4];

    let error = reader.read_exact(&mut output).unwrap_err();

    assert!(matches!(error, PgDumpError::UnexpectedEof { offset: 2 }));
    assert_eq!(reader.offset(), 2);
}

#[test]
fn exact_read_rejects_counter_overflow_before_reading() {
    let read_bytes = Rc::new(Cell::new(0));
    let source = CountingReader::new(b"x", Rc::clone(&read_bytes));
    let mut reader = ArchiveReader::new_at(source, u64::MAX);
    let mut output = [0_u8; 1];

    let error = reader.read_exact(&mut output).unwrap_err();

    assert!(matches!(
        error,
        PgDumpError::ArithmeticOverflow { offset: u64::MAX }
    ));
    assert_eq!(read_bytes.get(), 0);
    assert_eq!(reader.offset(), u64::MAX);
}

#[test]
fn io_errors_preserve_offset_and_source() {
    let mut reader = ArchiveReader::new(FailingReader);
    let mut output = [0_u8; 1];

    let error = reader.read_exact(&mut output).unwrap_err();

    assert!(matches!(&error, PgDumpError::Io { offset: 0, .. }));
    assert_eq!(
        error.source().map(|source| source.to_string()).as_deref(),
        Some("boom")
    );
}

#[test]
fn archive_integer_decodes_representative_values_and_boundaries() {
    for expected in [
        0_i64,
        1,
        -1,
        0x12_34_56,
        -0x12_34_56,
        i64::from(i32::MAX),
        i64::from(i32::MIN),
    ] {
        let mut reader = ArchiveReader::new(Cursor::new(encode_integer(expected, 4)));

        let actual = read_archive_integer(&mut reader, integer_size()).unwrap();

        assert_eq!(i64::from(actual), expected);
        assert_eq!(reader.offset(), 5);
    }
}

#[test]
fn archive_integer_treats_any_nonzero_sign_as_negative() {
    let mut encoded = encode_integer(7, 4);
    encoded[0] = 0xff;
    let mut reader = ArchiveReader::new(Cursor::new(encoded));

    assert_eq!(
        read_archive_integer(&mut reader, integer_size()).unwrap(),
        -7
    );

    let mut negative_zero = encode_integer(0, 4);
    negative_zero[0] = 1;
    let mut reader = ArchiveReader::new(Cursor::new(negative_zero));
    assert_eq!(
        read_archive_integer(&mut reader, integer_size()).unwrap(),
        0
    );
}

#[test]
fn archive_integer_rejects_unsupported_sizes() {
    for size in [0_u8, 5] {
        let error = ArchiveIntegerSize::new(size, 17).unwrap_err();
        assert!(matches!(
            error,
            PgDumpError::UnsupportedArchiveIntegerSize {
                size: actual,
                offset: 17
            } if actual == size
        ));
    }
}

#[test]
fn archive_integer_rejects_values_outside_i32() {
    for value in [i64::from(i32::MAX) + 1, i64::from(i32::MIN) - 1] {
        let mut reader = ArchiveReader::new(Cursor::new(encode_integer(value, 4)));
        let error = read_archive_integer(&mut reader, integer_size()).unwrap_err();
        assert!(matches!(
            error,
            PgDumpError::ArchiveIntegerOutOfRange { offset: 0 }
        ));
    }
}

#[test]
fn archive_integer_truncation_is_typed() {
    let mut reader = ArchiveReader::new(Cursor::new([0_u8, 1, 2]));

    let error = read_archive_integer(&mut reader, integer_size()).unwrap_err();

    assert!(matches!(error, PgDumpError::UnexpectedEof { offset: 3 }));
}

#[test]
fn archive_offset_preserves_not_set_set_and_no_data_states() {
    let size = ArchiveOffsetSize::new(8, 0).unwrap();
    let cases = [
        (1_u8, 99_u128, ArchiveOffset::PositionNotSet),
        (2, 0x12_34_56_78, ArchiveOffset::Position(0x12_34_56_78)),
        (3, 99, ArchiveOffset::NoData),
    ];

    for (state, value, expected) in cases {
        let mut reader = ArchiveReader::new(Cursor::new(encode_offset(state, 8, value)));
        assert_eq!(read_archive_offset(&mut reader, size).unwrap(), expected);
        assert_eq!(reader.offset(), 9);
    }
}

#[test]
fn archive_offset_rejects_invalid_size_and_state() {
    assert!(matches!(
        ArchiveOffsetSize::new(0, 23).unwrap_err(),
        PgDumpError::InvalidArchiveOffsetSize {
            size: 0,
            offset: 23
        }
    ));

    let size = ArchiveOffsetSize::new(8, 0).unwrap();
    let mut reader = ArchiveReader::new(Cursor::new(encode_offset(0, 8, 0)));
    let error = read_archive_offset(&mut reader, size).unwrap_err();
    assert!(matches!(
        error,
        PgDumpError::InvalidArchiveOffsetState {
            state: 0,
            offset: 0
        }
    ));
}

#[test]
fn archive_offset_accepts_zero_extension_and_rejects_overflow() {
    let size = ArchiveOffsetSize::new(9, 0).unwrap();
    let mut valid = ArchiveReader::new(Cursor::new(encode_offset(2, 9, u128::from(u64::MAX))));
    assert_eq!(
        read_archive_offset(&mut valid, size).unwrap(),
        ArchiveOffset::Position(u64::MAX)
    );

    let mut overflowing = ArchiveReader::new(Cursor::new(encode_offset(2, 9, 1_u128 << 64)));
    let error = read_archive_offset(&mut overflowing, size).unwrap_err();
    assert!(matches!(
        error,
        PgDumpError::ArchiveOffsetOutOfRange { offset: 9 }
    ));
}

#[test]
fn archive_offset_truncation_is_typed() {
    let size = ArchiveOffsetSize::new(8, 0).unwrap();
    let mut reader = ArchiveReader::new(Cursor::new([2_u8, 1, 2]));

    let error = read_archive_offset(&mut reader, size).unwrap_err();

    assert!(matches!(error, PgDumpError::UnexpectedEof { offset: 3 }));
}

#[test]
fn archive_string_decodes_null_empty_non_utf8_and_exact_limit() {
    let cases = [
        (-7_i64, Vec::new(), 3, None),
        (0, Vec::new(), 0, Some(Vec::new())),
        (2, b"ok".to_vec(), 3, Some(b"ok".to_vec())),
        (3, vec![0xff, 0x00, 0xfe], 3, Some(vec![0xff, 0x00, 0xfe])),
    ];

    for (length, payload, limit, expected) in cases {
        let mut encoded = encode_integer(length, 4);
        encoded.extend_from_slice(&payload);
        let mut reader = ArchiveReader::new(Cursor::new(encoded));
        assert_eq!(
            read_archive_string(&mut reader, integer_size(), limit).unwrap(),
            expected
        );
    }
}

#[test]
fn archive_string_rejects_oversize_before_payload_read() {
    let read_bytes = Rc::new(Cell::new(0));
    let mut encoded = encode_integer(4, 4);
    encoded.extend_from_slice(b"data");
    let source = CountingReader::new(&encoded, Rc::clone(&read_bytes));
    let mut reader = ArchiveReader::new(source);

    let error = read_archive_string(&mut reader, integer_size(), 3).unwrap_err();

    assert!(matches!(
        error,
        PgDumpError::ArchiveStringLimitExceeded {
            length: 4,
            limit: 3,
            offset: 0
        }
    ));
    assert_eq!(reader.offset(), 5);
    assert_eq!(read_bytes.get(), 5);
}

#[test]
fn archive_string_payload_truncation_is_typed() {
    let mut encoded = encode_integer(3, 4);
    encoded.extend_from_slice(b"ab");
    let mut reader = ArchiveReader::new(Cursor::new(encoded));

    let error = read_archive_string(&mut reader, integer_size(), 3).unwrap_err();

    assert!(matches!(error, PgDumpError::UnexpectedEof { offset: 7 }));
}

#[test]
fn default_archive_string_path_has_an_explicit_finite_limit() {
    let limit = Limits::default().max_string_bytes();
    assert!(limit > 0);
    assert!(limit < usize::MAX);
}

fn integer_size() -> ArchiveIntegerSize {
    ArchiveIntegerSize::new(4, 0).unwrap()
}

fn encode_integer(value: i64, size: u8) -> Vec<u8> {
    let mut encoded = Vec::with_capacity(usize::from(size) + 1);
    encoded.push(u8::from(value.is_negative()));
    let mut magnitude = value.unsigned_abs();
    for _ in 0..size {
        encoded.push((magnitude & 0xff) as u8);
        magnitude >>= 8;
    }
    encoded
}

fn encode_offset(state: u8, size: u8, mut value: u128) -> Vec<u8> {
    let mut encoded = Vec::with_capacity(usize::from(size) + 1);
    encoded.push(state);
    for _ in 0..size {
        encoded.push((value & 0xff) as u8);
        value >>= 8;
    }
    encoded
}

struct ChunkedReader<'a> {
    bytes: &'a [u8],
    position: usize,
    chunk_size: usize,
}

impl<'a> ChunkedReader<'a> {
    fn new(bytes: &'a [u8], chunk_size: usize) -> Self {
        Self {
            bytes,
            position: 0,
            chunk_size,
        }
    }
}

impl Read for ChunkedReader<'_> {
    fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
        let remaining = &self.bytes[self.position..];
        let count = remaining.len().min(output.len()).min(self.chunk_size);
        output[..count].copy_from_slice(&remaining[..count]);
        self.position += count;
        Ok(count)
    }
}

struct CountingReader<'a> {
    bytes: Cursor<&'a [u8]>,
    read_bytes: Rc<Cell<usize>>,
}

impl<'a> CountingReader<'a> {
    fn new(bytes: &'a [u8], read_bytes: Rc<Cell<usize>>) -> Self {
        Self {
            bytes: Cursor::new(bytes),
            read_bytes,
        }
    }
}

impl Read for CountingReader<'_> {
    fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
        let count = self.bytes.read(output)?;
        self.read_bytes.set(self.read_bytes.get() + count);
        Ok(count)
    }
}

struct FailingReader;

impl Read for FailingReader {
    fn read(&mut self, _output: &mut [u8]) -> io::Result<usize> {
        Err(io::Error::other("boom"))
    }
}