use std::cell::RefCell;
use std::io;
use flate2::Compress;
use flate2::Compression as FlateCompression;
use flate2::FlushCompress;
use flate2::Status;
use protohoggr::{encode_bytes_field, encode_int32_field};
use crate::read::blob_wire::MAX_BLOB_HEADER_SIZE;
use crate::write::metrics::WRITER_METRICS;
use std::sync::atomic::Ordering::Relaxed;
use super::compression::Compression;
use super::pipeline::{FramedBlobParts, elapsed_ns_u64};
pub(super) struct FrameScratch {
pub(super) blob_buf: Vec<u8>,
pub(super) header_buf: Vec<u8>,
compress_buf: Vec<u8>,
zlib_compressor: Option<(u32, Compress)>,
zstd_compressor: Option<(i32, zstd::bulk::Compressor<'static>)>,
}
impl FrameScratch {
pub(super) const fn new() -> Self {
Self {
blob_buf: Vec::new(),
header_buf: Vec::new(),
compress_buf: Vec::new(),
zlib_compressor: None,
zstd_compressor: None,
}
}
}
thread_local! {
pub(super) static PIPELINE_SCRATCH: RefCell<FrameScratch> = const { RefCell::new(FrameScratch::new()) };
}
pub(crate) fn frame_blob(
blob_type: &str,
uncompressed: &[u8],
compression: &Compression,
indexdata: Option<&[u8]>,
) -> io::Result<Vec<u8>> {
let mut scratch = FrameScratch::new();
Ok(frame_blob_into(
blob_type,
uncompressed,
compression,
indexdata,
None,
None,
&mut scratch,
)?
.into_vec())
}
pub(crate) fn frame_blob_pipelined(
uncompressed: &[u8],
compression: &Compression,
indexdata: Option<&[u8]>,
tagdata: Option<&[u8]>,
way_members: Option<&[u8]>,
) -> io::Result<FramedBlobParts> {
PIPELINE_SCRATCH.with_borrow_mut(|scratch| {
frame_blob_into(
"OSMData",
uncompressed,
compression,
indexdata,
tagdata,
way_members,
scratch,
)
})
}
#[hotpath::measure]
pub(super) fn frame_blob_into(
blob_type: &str,
uncompressed: &[u8],
compression: &Compression,
indexdata: Option<&[u8]>,
tagdata: Option<&[u8]>,
way_members: Option<&[u8]>,
scratch: &mut FrameScratch,
) -> io::Result<FramedBlobParts> {
let t_compress = std::time::Instant::now();
encode_blob_body(uncompressed, compression, scratch)?;
WRITER_METRICS
.compress_ns
.fetch_add(elapsed_ns_u64(t_compress), Relaxed);
let t_frame = std::time::Instant::now();
let datasize = i32::try_from(scratch.blob_buf.len()).map_err(|_| {
io::Error::other(format!(
"blob datasize overflow: {} bytes",
scratch.blob_buf.len()
))
})?;
encode_blob_header_into(
blob_type,
datasize,
indexdata,
tagdata,
way_members,
&mut scratch.header_buf,
)?;
let header_len = u32::try_from(scratch.header_buf.len()).map_err(|_| {
io::Error::other(format!(
"header too large: {} bytes",
scratch.header_buf.len()
))
})?;
let total_len = 4 + scratch.header_buf.len() + scratch.blob_buf.len();
WRITER_METRICS
.frame_ns
.fetch_add(elapsed_ns_u64(t_frame), Relaxed);
WRITER_METRICS
.bytes_framed
.fetch_add(total_len as u64, Relaxed);
Ok(FramedBlobParts {
prefix: header_len.to_be_bytes(),
header: scratch.header_buf.clone(),
blob: scratch.blob_buf.clone(),
})
}
pub(super) fn encode_blob_body(
uncompressed: &[u8],
compression: &Compression,
scratch: &mut FrameScratch,
) -> io::Result<()> {
scratch.blob_buf.clear();
match compression {
Compression::None => {
encode_bytes_field(&mut scratch.blob_buf, 1, uncompressed);
}
Compression::Zlib(level) => {
compress_zlib(uncompressed, *level, scratch)?;
}
Compression::Zstd(level) => {
match &scratch.zstd_compressor {
Some((cached_level, _)) if *cached_level == *level => {}
_ => {
scratch.zstd_compressor = Some((
*level,
zstd::bulk::Compressor::new(*level).map_err(io::Error::other)?,
));
}
}
let (_, compressor) = scratch.zstd_compressor.as_mut().expect("just initialized");
let bound = zstd::zstd_safe::compress_bound(uncompressed.len());
scratch.compress_buf.clear();
scratch.compress_buf.reserve(bound);
compressor
.compress_to_buffer(uncompressed, &mut scratch.compress_buf)
.map_err(io::Error::other)?;
let raw_size = i32::try_from(uncompressed.len()).map_err(|_| {
io::Error::other(format!(
"blob raw_size overflow: {} bytes",
uncompressed.len()
))
})?;
encode_int32_field(&mut scratch.blob_buf, 2, raw_size);
encode_bytes_field(&mut scratch.blob_buf, 7, &scratch.compress_buf);
}
}
Ok(())
}
fn compress_zlib(uncompressed: &[u8], level: u32, scratch: &mut FrameScratch) -> io::Result<()> {
let needs_new = match &scratch.zlib_compressor {
Some((cached_level, _)) => *cached_level != level,
None => true,
};
if needs_new {
scratch.zlib_compressor = Some((level, Compress::new(FlateCompression::new(level), true)));
}
let (_, compressor) = scratch.zlib_compressor.as_mut().expect("just initialized");
scratch.compress_buf.clear();
scratch
.compress_buf
.reserve(uncompressed.len() + (uncompressed.len() >> 10) + 64);
let status = compressor
.compress_vec(
uncompressed,
&mut scratch.compress_buf,
FlushCompress::Finish,
)
.map_err(|e| io::Error::other(format!("zlib compress error: {e}")))?;
if !matches!(status, Status::StreamEnd) {
return Err(io::Error::other(
"zlib compress did not complete in one call",
));
}
compressor.reset();
let raw_size = i32::try_from(uncompressed.len()).map_err(|_| {
io::Error::other(format!(
"blob raw_size overflow: {} bytes",
uncompressed.len()
))
})?;
encode_int32_field(&mut scratch.blob_buf, 2, raw_size);
encode_bytes_field(&mut scratch.blob_buf, 3, &scratch.compress_buf);
Ok(())
}
pub(crate) fn encode_blob_header_into(
blob_type: &str,
datasize: i32,
indexdata: Option<&[u8]>,
tagdata: Option<&[u8]>,
way_members: Option<&[u8]>,
buf: &mut Vec<u8>,
) -> io::Result<()> {
buf.clear();
encode_bytes_field(buf, 1, blob_type.as_bytes());
if let Some(data) = indexdata {
encode_bytes_field(buf, 2, data);
}
encode_int32_field(buf, 3, datasize);
if let Some(data) = tagdata {
encode_bytes_field(buf, 4, data);
}
if let Some(data) = way_members {
encode_bytes_field(buf, 5, data);
}
if buf.len() as u64 >= MAX_BLOB_HEADER_SIZE {
return Err(io::Error::other(format!(
"BlobHeader for {blob_type} is {} bytes, must be smaller than {MAX_BLOB_HEADER_SIZE}",
buf.len()
)));
}
Ok(())
}
pub(crate) fn strip_blob_header_fields(
header_bytes: &[u8],
strip_fields: &[u32],
out: &mut Vec<u8>,
) -> io::Result<()> {
strip_message_fields(header_bytes, strip_fields, out, "BlobHeader")
}
pub(crate) fn strip_header_block_fields(
header_block_bytes: &[u8],
strip_fields: &[u32],
out: &mut Vec<u8>,
) -> io::Result<()> {
strip_message_fields(header_block_bytes, strip_fields, out, "HeaderBlock")
}
fn strip_message_fields(
msg_bytes: &[u8],
strip_fields: &[u32],
out: &mut Vec<u8>,
ctx: &str,
) -> io::Result<()> {
use protohoggr::Cursor;
out.clear();
let mut cursor = Cursor::new(msg_bytes);
loop {
let field_start = cursor.position();
let Some((field, wire_type)) = cursor
.read_tag()
.map_err(|e| io::Error::other(format!("{ctx} parse: {e}")))?
else {
break;
};
cursor
.skip_field(wire_type)
.map_err(|e| io::Error::other(format!("{ctx} parse: {e}")))?;
let field_end = cursor.position();
if !strip_fields.contains(&field) {
out.extend_from_slice(&msg_bytes[field_start..field_end]);
}
}
Ok(())
}
pub(crate) fn reframe_raw_with_index(
blob_bytes: &[u8],
indexdata: &[u8],
tagdata: Option<&[u8]>,
) -> io::Result<Vec<u8>> {
let mut header_buf = Vec::new();
reframe_raw_with_index_scratch(blob_bytes, indexdata, tagdata, &mut header_buf)
}
pub(crate) fn reframe_raw_with_index_scratch(
blob_bytes: &[u8],
indexdata: &[u8],
tagdata: Option<&[u8]>,
header_buf: &mut Vec<u8>,
) -> io::Result<Vec<u8>> {
let datasize = i32::try_from(blob_bytes.len()).map_err(|_| {
io::Error::other(format!(
"blob datasize overflow: {} bytes",
blob_bytes.len()
))
})?;
header_buf.clear();
encode_blob_header_into(
"OSMData",
datasize,
Some(indexdata),
tagdata,
None,
header_buf,
)?;
let header_len = u32::try_from(header_buf.len())
.map_err(|_| io::Error::other(format!("header too large: {} bytes", header_buf.len())))?;
let total_len = 4 + header_buf.len() + blob_bytes.len();
let mut out = Vec::with_capacity(total_len);
out.extend_from_slice(&header_len.to_be_bytes());
out.extend_from_slice(header_buf);
out.extend_from_slice(blob_bytes);
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::read::blob_wire::WireBlobHeader;
#[test]
fn way_members_header_roundtrip_respects_toggle() {
let mut bytes = Vec::new();
let payload = [1, 9, 0b1010_0101, 0b0000_0001];
encode_blob_header_into("OSMData", 12, None, None, Some(&payload), &mut bytes)
.expect("way-members header must encode");
match WireBlobHeader::parse(&bytes, false, false, true) {
Ok(parsed) => assert_eq!(parsed.waymembers.as_deref(), Some(payload.as_slice())),
Err(err) => panic!("failed to parse way-members header: {err}"),
}
match WireBlobHeader::parse(&bytes, false, false, false) {
Ok(skipped) => assert!(skipped.waymembers.is_none()),
Err(err) => panic!("failed to parse toggle-off header: {err}"),
}
}
#[test]
fn strip_blob_header_fields_preserves_untargeted_fields_verbatim() {
use protohoggr::{encode_bytes_field, encode_int32_field, encode_varint_field};
let v1_index = [0xABu8; 26];
let tag_index = [1u8, 2, 3, 4];
let way_members = [1u8, 9, 0b1010_0101, 0b0000_0001];
let unknown = [0x77u8, 0x88];
let mut header = Vec::new();
encode_bytes_field(&mut header, 1, b"OSMData"); encode_bytes_field(&mut header, 2, &v1_index); encode_int32_field(&mut header, 3, 4242); encode_bytes_field(&mut header, 4, &tag_index); encode_bytes_field(&mut header, 5, &way_members); encode_varint_field(&mut header, 9, 0x0102); encode_bytes_field(&mut header, 11, &unknown);
let mut out = Vec::new();
strip_blob_header_fields(&header, &[], &mut out).expect("strip none");
assert_eq!(out, header, "empty strip set must be an identity copy");
let mut expect_no_tag = Vec::new();
encode_bytes_field(&mut expect_no_tag, 1, b"OSMData");
encode_bytes_field(&mut expect_no_tag, 2, &v1_index);
encode_int32_field(&mut expect_no_tag, 3, 4242);
encode_bytes_field(&mut expect_no_tag, 5, &way_members);
encode_varint_field(&mut expect_no_tag, 9, 0x0102);
encode_bytes_field(&mut expect_no_tag, 11, &unknown);
let mut out = Vec::new();
strip_blob_header_fields(&header, &[4], &mut out).expect("strip tagdata");
assert_eq!(
out, expect_no_tag,
"stripping field 4 must drop only tagdata and preserve v1 index, \
WayMembers, and unknown fields byte-for-byte"
);
let parsed = WireBlobHeader::parse(&out, true, true, true).expect("parse stripped header");
assert!(parsed.tagdata.is_none(), "tagdata must be gone");
assert_eq!(
parsed.waymembers.as_deref(),
Some(way_members.as_slice()),
"WayMembers-v1 must survive a tagdata strip"
);
assert_eq!(
&parsed.indexdata.expect("index present")[..26],
v1_index.as_slice(),
"v1 index bytes must be preserved unchanged"
);
let mut expect_no_index = Vec::new();
encode_bytes_field(&mut expect_no_index, 1, b"OSMData");
encode_int32_field(&mut expect_no_index, 3, 4242);
encode_bytes_field(&mut expect_no_index, 4, &tag_index);
encode_bytes_field(&mut expect_no_index, 5, &way_members);
encode_varint_field(&mut expect_no_index, 9, 0x0102);
encode_bytes_field(&mut expect_no_index, 11, &unknown);
let mut out = Vec::new();
strip_blob_header_fields(&header, &[2], &mut out).expect("strip indexdata");
assert_eq!(
out, expect_no_index,
"stripping field 2 must drop only indexdata and preserve tagdata, \
WayMembers, and unknown fields byte-for-byte"
);
let mut expect_neither = Vec::new();
encode_bytes_field(&mut expect_neither, 1, b"OSMData");
encode_int32_field(&mut expect_neither, 3, 4242);
encode_bytes_field(&mut expect_neither, 5, &way_members);
encode_varint_field(&mut expect_neither, 9, 0x0102);
encode_bytes_field(&mut expect_neither, 11, &unknown);
let mut out = Vec::new();
strip_blob_header_fields(&header, &[2, 4], &mut out).expect("strip both");
assert_eq!(
out, expect_neither,
"stripping fields 2 and 4 must leave WayMembers and unknown fields intact"
);
}
#[test]
fn strip_header_block_fields_preserves_untargeted_fields_verbatim() {
use protohoggr::{encode_bytes_field, encode_int64_field, encode_varint_field};
let bbox = [1u8, 2, 3, 4];
let source = b"survey-import-2019";
let custom_feature = b"Custom.Extension-v9";
let unknown = [0x77u8, 0x88];
let mut header = Vec::new();
encode_bytes_field(&mut header, 1, &bbox); encode_bytes_field(&mut header, 4, b"OsmSchema-V0.6"); encode_bytes_field(&mut header, 5, b"Sort.Type_then_ID"); encode_bytes_field(&mut header, 5, custom_feature); encode_bytes_field(&mut header, 16, b"my-writer/2.0"); encode_bytes_field(&mut header, 17, source); encode_int64_field(&mut header, 32, 1_700_000_000); encode_int64_field(&mut header, 33, 4242); encode_bytes_field(&mut header, 34, b"https://example.org/repl"); encode_varint_field(&mut header, 60, 0x0102); encode_bytes_field(&mut header, 61, &unknown);
let mut out = Vec::new();
strip_header_block_fields(&header, &[], &mut out).expect("strip none");
assert_eq!(out, header, "empty strip set must be an identity copy");
let mut expect = Vec::new();
encode_bytes_field(&mut expect, 4, b"OsmSchema-V0.6");
encode_bytes_field(&mut expect, 5, b"Sort.Type_then_ID");
encode_bytes_field(&mut expect, 5, custom_feature);
encode_bytes_field(&mut expect, 16, b"my-writer/2.0");
encode_bytes_field(&mut expect, 17, source);
encode_int64_field(&mut expect, 32, 1_700_000_000);
encode_int64_field(&mut expect, 33, 4242);
encode_bytes_field(&mut expect, 34, b"https://example.org/repl");
encode_varint_field(&mut expect, 60, 0x0102);
encode_bytes_field(&mut expect, 61, &unknown);
let mut out = Vec::new();
strip_header_block_fields(&header, &[1], &mut out).expect("strip bbox");
assert_eq!(
out, expect,
"stripping field 1 must drop only the bbox and preserve source, \
writingprogram, custom optional features, replication metadata, \
and unknown fields byte-for-byte"
);
let parsed = crate::read::block::HeaderBlock::parse_from_bytes(&out).expect("parse");
assert!(parsed.bbox().is_none(), "bbox must be gone");
assert_eq!(parsed.source(), Some("survey-import-2019"));
assert_eq!(parsed.writing_program(), Some("my-writer/2.0"));
assert!(parsed.is_sorted());
assert!(
parsed
.optional_features()
.iter()
.any(|f| f == "Custom.Extension-v9"),
"custom optional feature must survive"
);
assert_eq!(parsed.osmosis_replication_timestamp(), Some(1_700_000_000));
assert_eq!(parsed.osmosis_replication_sequence_number(), Some(4242));
assert_eq!(
parsed.osmosis_replication_base_url(),
Some("https://example.org/repl")
);
}
#[test]
fn blob_header_cap_rejects_at_strict_boundary() {
let mut buf = Vec::new();
let probe = vec![0u8; 60_000];
encode_blob_header_into("OSMData", 1, None, None, Some(&probe), &mut buf)
.expect("probe header must encode");
let overhead = buf.len() - probe.len();
let cap = usize::try_from(MAX_BLOB_HEADER_SIZE).unwrap_or(usize::MAX);
let pass_payload = vec![0u8; cap - 1 - overhead];
encode_blob_header_into("OSMData", 1, None, None, Some(&pass_payload), &mut buf)
.expect("65,535-byte header must encode");
assert_eq!(buf.len() as u64, MAX_BLOB_HEADER_SIZE - 1);
let fail_payload = vec![0u8; cap - overhead];
let err = encode_blob_header_into("OSMData", 1, None, None, Some(&fail_payload), &mut buf)
.expect_err("65,536-byte header must error");
assert!(err.to_string().contains("BlobHeader"));
}
}