#![allow(clippy::unwrap_used)]
mod common;
use std::io::Write;
use std::path::Path;
use common::adversarial::{
locate_blobs, mutate_blob_header_indexdata, mutate_blob_payload,
set_relation_memids_terminator_continuation,
};
use common::cli::CliInvoker;
use common::{write_test_pbf, TestMember, TestNode, TestRelation, TestWay};
use pbfhogg::block_builder::{self, BlockBuilder};
use pbfhogg::writer::{Compression, PbfWriter};
use pbfhogg::MemberId;
use tempfile::TempDir;
const LAT: i32 = 550_000_000;
const LON: i32 = 120_000_000;
fn write_lying_sorted_pbf(path: &Path) {
let file = std::fs::File::create(path).expect("create");
let buf = std::io::BufWriter::with_capacity(256 * 1024, file);
let mut writer = PbfWriter::new(buf, Compression::default());
let header = block_builder::HeaderBuilder::new()
.sorted()
.build()
.expect("header");
writer.write_header(&header).expect("write header");
for (start, end) in [(1_i64, 100_i64), (500, 600), (200, 300)] {
let mut bb = BlockBuilder::new();
for id in start..=end {
bb.add_node(id, LAT, LON, [], None);
}
let bytes = bb.take().expect("take").expect("non-empty");
writer.write_primitive_block(bytes).expect("write block");
}
writer.flush().expect("flush");
}
#[test]
fn renumber_survives_lying_sorted_header_out_of_order_blobs() {
let dir = TempDir::new().expect("tempdir");
let input = dir.path().join("input.osm.pbf");
let output = dir.path().join("output.osm.pbf");
write_lying_sorted_pbf(&input);
let out = CliInvoker::new()
.arg("renumber")
.arg(&input)
.arg("-o")
.arg(&output)
.run();
let stderr = out.stderr_str();
assert!(
!stderr.contains("pre_allocate only covers"),
"renumber panicked in IdSet::set_atomic despite the \
max_id scan fix; stderr:\n{stderr}",
);
}
#[test]
fn apply_changes_rejects_unsorted_header() {
let dir = TempDir::new().expect("tempdir");
let base = dir.path().join("base.osm.pbf");
let diff = dir.path().join("diff.osc.gz");
let output = dir.path().join("output.osm.pbf");
let nodes = vec![TestNode {
id: 1,
lat: LAT,
lon: LON,
tags: vec![],
meta: None,
}];
let ways: Vec<TestWay> = vec![];
let relations: Vec<TestRelation> = vec![];
write_test_pbf(&base, &nodes, &ways, &relations);
let file = std::fs::File::create(&diff).expect("create");
let mut enc = flate2::write::GzEncoder::new(file, flate2::Compression::fast());
enc.write_all(b"<?xml version='1.0' encoding='UTF-8'?>\n<osmChange version='0.6'/>\n")
.expect("write xml");
enc.finish().expect("finish gz");
let out = CliInvoker::new()
.arg("apply-changes")
.arg(&base)
.arg(&diff)
.arg("-o")
.arg(&output)
.run();
assert!(
!out.status.success(),
"apply-changes must reject an unsorted base header; stdout:\n{}\nstderr:\n{}",
out.stdout_str(),
out.stderr_str(),
);
let stderr = out.stderr_str();
assert!(
stderr.contains("sorted base PBF"),
"expected a sortedness error message; stderr:\n{stderr}",
);
}
fn write_three_kind_fixture(path: &Path) {
let nodes = (1..=8_i64)
.map(|id| TestNode {
id,
lat: LAT,
lon: LON,
tags: vec![],
meta: None,
})
.collect::<Vec<_>>();
let ways = (1..=4_i64)
.map(|id| TestWay {
id,
refs: vec![1, 2, 3, 4],
tags: vec![],
meta: None,
})
.collect::<Vec<_>>();
let relations = vec![TestRelation {
id: 1,
members: vec![
TestMember {
id: MemberId::Way(1),
role: "outer",
},
TestMember {
id: MemberId::Way(2),
role: "outer",
},
TestMember {
id: MemberId::Way(3),
role: "inner",
},
TestMember {
id: MemberId::Way(4),
role: "inner",
},
],
tags: vec![("type", "multipolygon")],
meta: None,
}];
let file = std::fs::File::create(path).expect("create");
let buf = std::io::BufWriter::with_capacity(256 * 1024, file);
let mut writer = PbfWriter::new(buf, Compression::default());
let header = block_builder::HeaderBuilder::new()
.sorted()
.build()
.expect("header");
writer.write_header(&header).expect("write header");
let mut bb = BlockBuilder::new();
for n in &nodes {
bb.add_node(n.id, n.lat, n.lon, [], None);
}
let bytes = bb.take().expect("take").expect("non-empty");
writer.write_primitive_block(bytes).expect("write nodes");
let mut bb = BlockBuilder::new();
for w in &ways {
bb.add_way(w.id, [], &w.refs, None);
}
let bytes = bb.take().expect("take").expect("non-empty");
writer.write_primitive_block(bytes).expect("write ways");
let mut bb = BlockBuilder::new();
for r in &relations {
let members: Vec<block_builder::MemberData<'_>> = r
.members
.iter()
.map(|m| block_builder::MemberData {
id: m.id,
role: m.role,
})
.collect();
bb.add_relation(r.id, r.tags.iter().map(|(k, v)| (*k, *v)), &members, None);
}
let bytes = bb.take().expect("take").expect("non-empty");
writer.write_primitive_block(bytes).expect("write relations");
writer.flush().expect("flush");
}
#[test]
fn altw_external_rejects_reversed_indexdata_range() {
let dir = TempDir::new().expect("tempdir");
let input = dir.path().join("input.osm.pbf");
let output = dir.path().join("output.osm.pbf");
write_three_kind_fixture(&input);
let pbf = std::fs::read(&input).expect("read fixture");
let mutated = mutate_blob_header_indexdata(&pbf, 1, |ix| {
assert!(ix.len() >= 18, "indexdata too short to swap min/max");
let mut min_buf = [0u8; 8];
let mut max_buf = [0u8; 8];
min_buf.copy_from_slice(&ix[2..10]);
max_buf.copy_from_slice(&ix[10..18]);
ix[2..10].copy_from_slice(&max_buf);
ix[10..18].copy_from_slice(&min_buf);
});
std::fs::write(&input, &mutated).expect("rewrite fixture");
let out = CliInvoker::new()
.arg("add-locations-to-ways")
.arg(&input)
.arg("-o")
.arg(&output)
.arg("--index-type")
.arg("external")
.run();
assert!(
!out.status.success(),
"altw external must reject reversed indexdata; stdout:\n{}\nstderr:\n{}",
out.stdout_str(),
out.stderr_str(),
);
let stderr = out.stderr_str();
assert!(
stderr.contains("reversed indexdata range"),
"expected the stage1 reversed-range hard error; stderr:\n{stderr}",
);
assert!(
!stderr.contains("panicked at"),
"altw external must not panic on reversed indexdata; stderr:\n{stderr}",
);
}
#[test]
fn renumber_rejects_truncated_relation_blob_payload() {
let dir = TempDir::new().expect("tempdir");
let input = dir.path().join("input.osm.pbf");
let output = dir.path().join("output.osm.pbf");
write_three_kind_fixture(&input);
let pbf = std::fs::read(&input).expect("read fixture");
let blobs = locate_blobs(&pbf);
let blob_idx = blobs.len() - 1;
let mutated = set_relation_memids_terminator_continuation(&pbf, blob_idx, 0);
std::fs::write(&input, &mutated).expect("rewrite fixture");
let out = CliInvoker::new()
.arg("renumber")
.arg(&input)
.arg("-o")
.arg(&output)
.run();
assert!(
!out.status.success(),
"renumber must reject a truncated relation payload; stdout:\n{}\nstderr:\n{}",
out.stdout_str(),
out.stderr_str(),
);
let stderr = out.stderr_str();
assert!(
!stderr.contains("panicked at"),
"renumber must not panic on truncated relation payload; stderr:\n{stderr}",
);
assert!(
stderr.contains("reframe_relations: relation 1 memids:"),
"expected count_varints_strict error from reframe_relations; stderr:\n{stderr}",
);
}
#[test]
fn cat_rejects_truncated_node_blob_payload() {
let dir = TempDir::new().expect("tempdir");
let input = dir.path().join("input.osm.pbf");
let output = dir.path().join("output.osm.pbf");
write_three_kind_fixture(&input);
let pbf = std::fs::read(&input).expect("read fixture");
let mutated = mutate_blob_payload(&pbf, 1, |payload| {
payload.pop();
});
std::fs::write(&input, &mutated).expect("rewrite fixture");
let out = CliInvoker::new()
.arg("cat")
.arg(&input)
.arg("-o")
.arg(&output)
.run();
let stderr = out.stderr_str();
assert!(
!stderr.contains("panicked at"),
"cat must not panic on truncated node payload; stderr:\n{stderr}",
);
}