use lexical;
use std::{fmt::Write, fs, io::stdout, path::PathBuf, sync::mpsc::Receiver, sync::mpsc::channel};
use crate::{
SkywayError,
chunks::{Chunk, ElementChunk, OrderedChunkIterator},
elements::{ElementType, Metadata, SimpleElementType},
};
use super::Writer;
struct ToFmtWrite<T>(pub T);
impl<T> Write for ToFmtWrite<T>
where
T: std::io::Write,
{
fn write_str(&mut self, s: &str) -> std::fmt::Result {
self.0.write_all(s.as_bytes()).map_err(|_| std::fmt::Error)
}
}
fn should_escape_char(input: char) -> bool {
match input {
'\u{0021}'..='\u{0024}' => false, '\u{0026}'..='\u{002b}' => false, '\u{002d}'..='\u{003c}' => false, '\u{003e}'..='\u{003f}' => false, '\u{0041}'..='\u{007e}' => false, '\u{00a1}'..='\u{00ac}' => false, '\u{00ae}'..='\u{05ff}' => false, _ => true,
}
}
fn push_escaped_char(base: &mut String, input: char) {
base.push('%');
let code_point = input as u32;
let hex = format!("{:x}", code_point);
base.push_str(&hex);
base.push('%');
}
fn push_escaped_string(base: &mut String, input: &str) {
for c in input.chars() {
if should_escape_char(c) {
push_escaped_char(base, c);
} else {
base.push(c);
}
}
}
fn serialize_chunk(chunk: ElementChunk) -> Chunk<String> {
let mut output = String::with_capacity(chunk.content.len() * 80);
for element in chunk.content {
match element.element_type {
ElementType::Node { .. } => {
output.push('n');
}
ElementType::Way { .. } => {
output.push('w');
}
ElementType::Relation { .. } => {
output.push('r');
}
}
output.push_str(&element.id.to_string());
if let Some(v) = element.version {
output.push_str(" v");
output.push_str(&lexical::to_string(v));
}
if let Some(v) = element.visible {
if v {
output.push_str(" dV");
} else {
output.push_str(" dD");
}
}
if let Some(c) = element.changeset {
output.push_str(" c");
output.push_str(&lexical::to_string(c));
}
if let Some(t) = element.timestamp {
output.push_str(" t");
output.push_str(&t);
}
if let Some(u) = element.uid {
output.push_str(" i");
output.push_str(&lexical::to_string(u));
}
if let Some(u) = element.user {
output.push_str(" u");
push_escaped_string(&mut output, &u);
}
output.push_str(" T");
let mut first_tag_written = false;
for (k, v) in element.tags {
if first_tag_written {
output.push(',');
}
first_tag_written = true;
push_escaped_string(&mut output, &k);
output.push('=');
push_escaped_string(&mut output, &v);
}
match element.element_type {
ElementType::Node { lat, lon } => {
output.push_str(" x");
output.push_str(&lexical::to_string(lon));
output.push_str(" y");
output.push_str(&lexical::to_string(lat));
}
ElementType::Way { nodes } => {
output.push_str(" N");
let mut first_node_written = false;
for n in nodes {
if first_node_written {
output.push(',');
}
first_node_written = true;
output.push('n');
output.push_str(&lexical::to_string(n));
}
}
ElementType::Relation { members } => {
output.push_str(" M");
let mut first_member_written = false;
for m in members {
if first_member_written {
output.push(',');
}
first_member_written = true;
output.push(match m.t {
Some(SimpleElementType::Node) => 'n',
Some(SimpleElementType::Way) => 'w',
Some(SimpleElementType::Relation) => 'r',
None => panic!("Member type is None"),
});
output.push_str(&lexical::to_string(m.id));
output.push('@');
if let Some(role) = m.role {
push_escaped_string(&mut output, &role);
}
}
}
}
output.push('\n');
}
Chunk {
index: chunk.index,
content: output,
}
}
pub struct OplWriter {}
fn write_output(chunk_iterator: Receiver<Chunk<String>>, dest_buffer: impl std::io::Write) {
let mut writer = ToFmtWrite(dest_buffer);
let ordered_chunks = OrderedChunkIterator::new(chunk_iterator.into_iter());
for chunk_content in ordered_chunks {
writer
.write_str(&chunk_content)
.expect("Failed to write chunk");
}
}
impl Writer for OplWriter {
fn write(
&self,
element_receiver: Receiver<ElementChunk>,
metadata_receiver: Receiver<Metadata>,
dest: Option<PathBuf>,
) -> Result<(), SkywayError> {
let (sender, receiver) = channel();
let write_thread = std::thread::spawn({
move || {
let _metadata = metadata_receiver.into_iter().next();
match dest {
None => write_output(receiver, stdout()),
Some(a) => match fs::File::create(PathBuf::from(a)) {
Ok(b) => write_output(receiver, b),
Err(e) => {
panic!("Unable to open output file: {e:?}");
}
},
}
}
});
for chunk in element_receiver {
sender
.send(serialize_chunk(chunk))
.expect("Failed to send serialized chunk");
}
drop(sender);
write_thread.join().map_err(|e| {
SkywayError::UnexpectedError(format!("Could not join writer thread: {:?}", e))
})?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_push_escaped_char() {
let mut string1 = String::new();
push_escaped_char(&mut string1, ' ');
assert_eq!(string1, "%20%");
let mut string2 = String::new();
push_escaped_char(&mut string2, ',');
assert_eq!(string2, "%2c%");
let mut string3 = String::new();
push_escaped_char(&mut string3, '😱');
assert_eq!(string3, "%1f631%");
let mut string4 = String::new();
push_escaped_char(&mut string4, '𒄈');
assert_eq!(string4, "%12108%");
}
#[test]
fn test_should_escape_char() {
let test_chars = vec![' ', '\n', ',', '=', '@', '%', '😱'];
for c in test_chars {
assert_eq!(should_escape_char(c), true);
}
}
#[test]
fn test_push_escaped_string() {
let mut string1 = String::new();
push_escaped_string(&mut string1, "A,B");
assert_eq!(string1, "A%2c%B");
let mut string2 = String::new();
push_escaped_string(&mut string2, "ohmy😱goodness");
assert_eq!(string2, "ohmy%1f631%goodness");
}
}