pbfhogg 0.5.0

Fast OpenStreetMap PBF reader and writer for Rust. Read, write, and merge .osm.pbf files with pipelined parallel decoding.
Documentation
use std::collections::HashSet;

use crate::{DenseNodeInfo, Info};

use super::ExportOptions;

pub(super) struct MetaView<'a> {
    version: Option<i32>,
    timestamp_millis: Option<i64>,
    changeset: Option<i64>,
    uid: Option<i32>,
    user: Option<crate::Result<&'a str>>,
    visible: Option<bool>,
}

impl<'a> MetaView<'a> {
    pub(super) fn from_info(info: &Info<'a>) -> Self {
        Self {
            version: info.version(),
            timestamp_millis: info.milli_timestamp(),
            changeset: info.changeset(),
            uid: info.uid(),
            user: info.user(),
            visible: info.visible_opt(),
        }
    }

    pub(super) fn from_dense(info: &'a DenseNodeInfo<'a>) -> Self {
        Self {
            version: Some(info.version()),
            timestamp_millis: Some(info.milli_timestamp()),
            changeset: Some(info.changeset()),
            uid: Some(info.uid()),
            user: Some(info.user()),
            visible: Some(info.visible()),
        }
    }
}

fn push_json_string(buf: &mut String, value: &str) -> crate::Result<()> {
    let encoded =
        serde_json::to_string(value).map_err(|error| std::io::Error::other(error.to_string()))?;
    buf.push_str(&encoded);
    Ok(())
}

fn push_name(buf: &mut String, first: &mut bool, name: &str) -> crate::Result<()> {
    if !*first {
        buf.push(',');
    }
    *first = false;
    push_json_string(buf, name)?;
    buf.push(':');
    Ok(())
}

pub(super) fn write_properties<'a, T>(
    buf: &mut String,
    id: i64,
    object_type: &str,
    tags: T,
    meta: Option<&MetaView<'a>>,
    opts: &ExportOptions,
) -> crate::Result<()>
where
    T: Iterator<Item = (&'a str, &'a str)>,
{
    use std::fmt::Write;

    buf.clear();
    buf.push('{');
    let mut first = true;
    push_name(buf, &mut first, "@id")?;
    write!(buf, "{id}").ok();
    push_name(buf, &mut first, "@type")?;
    push_json_string(buf, object_type)?;

    let mut reserved = HashSet::from(["@id", "@type"]);
    if opts.metadata {
        reserved.extend([
            "@version",
            "@timestamp",
            "@changeset",
            "@uid",
            "@user",
            "@visible",
        ]);
        if let Some(meta) = meta {
            if let Some(value) = meta.version {
                push_name(buf, &mut first, "@version")?;
                write!(buf, "{value}").ok();
            }
            if let Some(millis) = meta.timestamp_millis {
                push_name(buf, &mut first, "@timestamp")?;
                let seconds = millis.div_euclid(1000).cast_unsigned();
                push_json_string(buf, &crate::commands::format_epoch_secs(seconds))?;
            }
            if let Some(value) = meta.changeset {
                push_name(buf, &mut first, "@changeset")?;
                write!(buf, "{value}").ok();
            }
            if let Some(value) = meta.uid {
                push_name(buf, &mut first, "@uid")?;
                write!(buf, "{value}").ok();
            }
            if let Some(user) = &meta.user {
                push_name(buf, &mut first, "@user")?;
                push_json_string(
                    buf,
                    user.as_ref()
                        .map_err(|error| std::io::Error::other(error.to_string()))?,
                )?;
            }
            if let Some(value) = meta.visible {
                push_name(buf, &mut first, "@visible")?;
                buf.push_str(if value { "true" } else { "false" });
            }
        }
    }

    let mut emitted = HashSet::new();
    for (key, value) in tags {
        if reserved.contains(key)
            || !emitted.insert(key)
            || opts
                .properties
                .as_ref()
                .is_some_and(|keys| !keys.contains(key))
        {
            continue;
        }
        push_name(buf, &mut first, key)?;
        push_json_string(buf, value)?;
    }
    buf.push('}');
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::commands::export::{ExportFormat, ExportTypes};

    #[test]
    fn escapes_whitelists_and_suppresses_collisions() {
        let options = ExportOptions::new(
            ExportFormat::GeoJsonSeq,
            ExportTypes::All,
            &[],
            Some(vec!["name".to_owned(), "@id".to_owned()]),
            None,
            false,
        )
        .expect("options");
        let tags = [
            ("@id", "wrong"),
            ("name", "a \"quote\" and \\ slash"),
            ("name", "duplicate"),
            ("drop", "me"),
        ];
        let mut buf = String::new();
        write_properties(&mut buf, 7, "node", tags.into_iter(), None, &options)
            .expect("properties");
        assert_eq!(
            buf,
            "{\"@id\":7,\"@type\":\"node\",\"name\":\"a \\\"quote\\\" and \\\\ slash\"}"
        );
    }
}