Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions crates/diffr-core/src/protocol/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,17 @@
//! last line. Columns are 0-based byte offsets into the UTF-8 text on the
//! wire. All ranges are half-open.
//!
//! Records end at `\n` only. U+0085, U+2028 and U+2029 are written as
//! `\u0085`, `\u2028` and `\u2029`, because some line readers also end a
//! line at them.
//!
//! Sides are always `lhs` (before) and `rhs` (after). A `Pairing` says which
//! sides exist and serializes by presence: `{lhs, rhs}`, `{lhs}`, or `{rhs}`.

use std::io::{self, Write};

use serde::{Deserialize, Serialize};
use serde_json::ser::Formatter;

use crate::pairing::Pairing;

Expand All @@ -23,6 +30,40 @@ pub mod project;
/// The current wire version. Changes within a version are additive.
pub const VERSION: u32 = 3;

/// Write `event` as one record: compact JSON and a `\n`.
pub fn write_record<W: Write>(writer: &mut W, event: &Event) -> serde_json::Result<()> {
event.serialize(&mut serde_json::Serializer::with_formatter(
&mut *writer,
RecordFormatter,
))?;
writer.write_all(b"\n").map_err(serde_json::Error::io)
}

/// Compact JSON that also escapes U+0085, U+2028 and U+2029.
struct RecordFormatter;

impl Formatter for RecordFormatter {
fn write_string_fragment<W: ?Sized + Write>(
&mut self,
writer: &mut W,
fragment: &str,
) -> io::Result<()> {
fragment
.split_inclusive(['\u{85}', '\u{2028}', '\u{2029}'])
.try_for_each(|piece| {
let mut chars = piece.chars();
let escape: &[u8] = match chars.next_back() {
Some('\u{85}') => b"\\u0085",
Some('\u{2028}') => b"\\u2028",
Some('\u{2029}') => b"\\u2029",
_ => return writer.write_all(piece.as_bytes()),
};
writer.write_all(chars.as_str().as_bytes())?;
writer.write_all(escape)
})
}
}

// ── stream ────────────────────────────────────────────────────────────────

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
Expand Down Expand Up @@ -590,6 +631,26 @@ mod tests {
}
}

#[test]
fn records_escape_line_separators() {
let event = Event::Complete {
succeeded: 0,
failed: 1,
aborted: Some(Problem {
code: "x".to_owned(),
message: "a\u{2028}b\u{2029}c\u{85}d\u{2028}".to_owned(),
}),
};
let mut record = Vec::new();
write_record(&mut record, &event).unwrap();
let line = String::from_utf8(record).unwrap();
assert!(
line.ends_with("\"message\":\"a\\u2028b\\u2029c\\u0085d\\u2028\"}}\n"),
"{line}"
);
assert_eq!(serde_json::from_str::<Event>(&line).unwrap(), event);
}

#[test]
fn defaults_are_omitted() {
let line = serde_json::to_string(&example_file()).unwrap();
Expand Down
5 changes: 2 additions & 3 deletions src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::options::DiffOptions;
use crate::plugin::{Classifier, MutationFailed, Pipeline};
use crate::present::present;
use crate::protocol::project::{self, Inputs};
use crate::protocol::{Diff, Event, FileChange, Outcome, Problem, VERSION};
use crate::protocol::{self, Diff, Event, FileChange, Outcome, Problem, VERSION};
use crate::summary::{DiffResult, FallbackCause, FileContent, FileFormat};
use crate::tags;
use std::io::{BufWriter, Write};
Expand Down Expand Up @@ -102,8 +102,7 @@ pub(crate) fn stream(
ended.failed |= *failed > 0;
ended.aborted = aborted.is_some();
}
serde_json::to_writer(&mut output, &event)?;
output.write_all(b"\n")?;
protocol::write_record(&mut output, &event)?;
output.flush()?;
}
Ok(ended)
Expand Down
Loading