pgdumpx 0.2.0

Read-only, bounded inspection, extraction, and row scanning for PostgreSQL custom-format dumps
Documentation
use pgdumpx::{Archive, DataLocation, PgDumpError};
use std::io::Cursor;

const SECTION_PRE_DATA: i32 = 2;
const NO_DATA: u8 = 3;

#[test]
fn optional_metadata_preserves_null_empty_and_non_utf8_bytes() {
    let bytes = build_archive([1, 16, 0], 2, |output| {
        write_metadata_entry(output, 1, &[0xff], None, true);
        write_metadata_entry(output, 2, b"empty-owner", Some(b""), true);
    });
    let archive = Archive::open(Cursor::new(bytes)).expect("archive must open");

    let non_utf8 = &archive.entries()[0];
    assert_eq!(non_utf8.name().as_bytes(), &[0xff]);
    assert!(non_utf8.name().to_str().is_err());
    assert!(non_utf8.owner().is_none());
    assert!(non_utf8.owner_bytes().is_none());

    let empty_owner = &archive.entries()[1];
    assert_eq!(
        empty_owner.owner().map(|owner| owner.as_bytes()),
        Some(b"".as_slice())
    );
    assert_eq!(empty_owner.owner_bytes(), Some(b"".as_slice()));
}

#[test]
fn archive_1_15_does_not_consume_a_relkind_slot() {
    let bytes = build_archive([1, 15, 0], 1, |output| {
        write_metadata_entry(output, 1, b"entry", Some(b"owner-after-tableam"), false);
    });
    let archive = Archive::open(Cursor::new(bytes)).expect("1.15 archive must open");
    let entry = &archive.entries()[0];

    assert!(entry.relation_kind().is_none());
    assert_eq!(
        entry.owner().map(|owner| owner.as_bytes()),
        Some(b"owner-after-tableam".as_slice())
    );
}

#[test]
fn archive_1_16_large_object_metadata_is_inspectable_without_payload_special_casing() {
    let bytes = build_archive([1, 16, 0], 1, |output| {
        write_metadata_entry_with_description(
            output,
            1,
            b"BLOB METADATA",
            b"BLOB METADATA",
            Some(b"postgres"),
            true,
        );
    });
    let archive = Archive::open(Cursor::new(bytes)).expect("1.16 archive must open");
    let entry = &archive.entries()[0];

    assert_eq!(entry.name().as_bytes(), b"BLOB METADATA");
    assert_eq!(entry.description().as_bytes(), b"BLOB METADATA");
    assert_eq!(entry.relation_kind(), Some(0));
    assert!(entry.table_access_method().is_none());
    assert_eq!(entry.owner_bytes(), Some(b"postgres".as_slice()));
    assert_eq!(entry.data_location(), DataLocation::NoData);
}

#[test]
fn truncated_1_16_relkind_returns_a_typed_eof_error() {
    let mut bytes = complete_header([1, 16, 0]);
    write_int(&mut bytes, 1);
    write_toc_prefix_through_tableam(&mut bytes, 1, b"entry", b"COMMENT");

    let error = Archive::open(Cursor::new(bytes)).expect_err("relkind is required in 1.16");
    assert!(matches!(error, PgDumpError::UnexpectedEof { .. }));
}

fn build_archive(
    version: [u8; 3],
    entry_count: i32,
    write_entries: impl FnOnce(&mut Vec<u8>),
) -> Vec<u8> {
    let mut output = complete_header(version);
    write_int(&mut output, entry_count);
    write_entries(&mut output);
    output
}

fn complete_header(version: [u8; 3]) -> Vec<u8> {
    let mut output = b"PGDMP".to_vec();
    output.extend_from_slice(&version);
    output.push(4);
    output.push(8);
    output.push(1);
    output.push(0);
    for value in [0, 0, 0, 1, 0, 126, 0] {
        write_int(&mut output, value);
    }
    write_string(&mut output, Some(b"database"));
    write_string(&mut output, Some(b"server"));
    write_string(&mut output, Some(b"pg_dump"));
    output
}

fn write_metadata_entry(
    output: &mut Vec<u8>,
    id: i32,
    tag: &[u8],
    owner: Option<&[u8]>,
    include_relkind: bool,
) {
    write_metadata_entry_with_description(output, id, tag, b"COMMENT", owner, include_relkind);
}

fn write_metadata_entry_with_description(
    output: &mut Vec<u8>,
    id: i32,
    tag: &[u8],
    description: &[u8],
    owner: Option<&[u8]>,
    include_relkind: bool,
) {
    write_toc_prefix_through_tableam(output, id, tag, description);
    if include_relkind {
        write_int(output, 0);
    }
    write_string(output, owner);
    write_string(output, Some(b"false"));
    write_string(output, None);
    output.push(NO_DATA);
    output.extend_from_slice(&0_u64.to_le_bytes());
}

fn write_toc_prefix_through_tableam(output: &mut Vec<u8>, id: i32, tag: &[u8], description: &[u8]) {
    write_int(output, id);
    write_int(output, 0);
    write_string(output, Some(b"0"));
    write_string(output, Some(b"0"));
    write_string(output, Some(tag));
    write_string(output, Some(description));
    write_int(output, SECTION_PRE_DATA);
    for _ in 0..6 {
        write_string(output, None);
    }
}

fn write_int(output: &mut Vec<u8>, value: i32) {
    output.push(u8::from(value.is_negative()));
    output.extend_from_slice(&value.unsigned_abs().to_le_bytes());
}

fn write_string(output: &mut Vec<u8>, value: Option<&[u8]>) {
    match value {
        Some(bytes) => {
            write_int(
                output,
                i32::try_from(bytes.len()).expect("test string length fits i32"),
            );
            output.extend_from_slice(bytes);
        }
        None => write_int(output, -1),
    }
}