From ebfe0c5cc190381a6512957f3ea6c159c6ecbdf6 Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Thu, 9 Jul 2026 09:18:08 -0500 Subject: [PATCH 1/2] fix: preserve xml entities in xmltv --- .../ersatztv-playout-generator/src/xmltv.rs | 17 +++--- crates/ersatztv/src/xmltv.rs | 55 ++++++++++++++++++- 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/crates/ersatztv-playout-generator/src/xmltv.rs b/crates/ersatztv-playout-generator/src/xmltv.rs index 4ad2b9d..340b6fe 100644 --- a/crates/ersatztv-playout-generator/src/xmltv.rs +++ b/crates/ersatztv-playout-generator/src/xmltv.rs @@ -130,18 +130,21 @@ fn read_existing_programmes( let stop = OffsetDateTime::parse(&stop_s, &XMLTV_FMT)?; let mut title = String::new(); - let mut in_title = false; loop { match reader .read_event_into(&mut inner_buf) .map_err(std::io::Error::other)? { - Event::Start(t) if t.name().as_ref() == b"title" => in_title = true, - Event::End(t) if t.name().as_ref() == b"title" => in_title = false, - Event::Text(t) if in_title => title.push_str( - &t.xml_content(XmlVersion::Implicit1_0) - .map_err(std::io::Error::other)?, - ), + Event::Start(t) if t.name().as_ref() == b"title" => { + let mut text_buf = Vec::new(); + let text_event = reader + .read_text_into(t.name(), &mut text_buf) + .map_err(std::io::Error::other)?; + let text = text_event.decode().map_err(std::io::Error::other)?; + title = quick_xml::escape::unescape(&text) + .map_err(std::io::Error::other)? + .into_owned(); + } Event::End(t) if t.name().as_ref() == b"programme" => break, Event::Eof => break, _ => {} diff --git a/crates/ersatztv/src/xmltv.rs b/crates/ersatztv/src/xmltv.rs index 9f9abeb..e52eb47 100644 --- a/crates/ersatztv/src/xmltv.rs +++ b/crates/ersatztv/src/xmltv.rs @@ -88,7 +88,7 @@ fn copy_programmes( buf: &mut Vec, ) -> std::io::Result<()> { let mut reader = Reader::from_reader(reader); - reader.config_mut().trim_text(true); + reader.config_mut().trim_text(false); let mut depth = 0u32; loop { match reader.read_event_into(buf).map_err(std::io::Error::other)? { @@ -103,12 +103,61 @@ fn copy_programmes( Event::Empty(e) if e.name().as_ref() == b"programme" || depth > 0 => { writer.write_event(Event::Empty(e))? } - Event::Text(e) if depth > 0 => writer.write_event(Event::Text(e))?, - Event::CData(e) if depth > 0 => writer.write_event(Event::CData(e))?, Event::Eof => return Ok(()), + other if depth > 0 => { + writer.write_event(other)?; + } _ => {} } buf.clear(); } } + +#[cfg(test)] +mod tests { + use super::*; + + // regression test for #172: entity/character references inside programmes were + // dropped because quick-xml emits them as separate `Event::GeneralRef` + // events that the copy loop did not forward. + #[test] + fn copy_programmes_preserves_entities_and_children() { + let input = r#" + + + Mister Rogers' Neighborhood + DAY CARE & NIGHT CARE + Line one. Line two. + Kids + S13E16 + +"#; + + let mut writer = Writer::new(Cursor::new(Vec::new())); + let mut buf = Vec::new(); + copy_programmes(input.as_bytes(), &mut writer, &mut buf).unwrap(); + let output = String::from_utf8(writer.into_inner().into_inner()).unwrap(); + + assert!( + output.contains("Mister Rogers' Neighborhood"), + "apostrophe reference dropped: {output}" + ); + assert!( + output.contains("DAY CARE & NIGHT CARE"), + "ampersand reference dropped: {output}" + ); + assert!( + output.contains("Line one. Line two."), + "newline references dropped: {output}" + ); + assert!( + output.contains(r#""#), + "child element dropped: {output}" + ); + assert!( + output.contains(r#"S13E16"#), + "child element dropped: {output}" + ); + } +} From cbab8f96996c9192c3b082a692120ff2e0f38086 Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Thu, 9 Jul 2026 09:24:43 -0500 Subject: [PATCH 2/2] fix newly-flagged redundant references --- crates/ersatztv/src/channel_session.rs | 2 +- crates/ffpipeline/src/probe.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/ersatztv/src/channel_session.rs b/crates/ersatztv/src/channel_session.rs index e27acac..7f1b1ef 100644 --- a/crates/ersatztv/src/channel_session.rs +++ b/crates/ersatztv/src/channel_session.rs @@ -48,7 +48,7 @@ impl ChannelSession { let _ = child.wait().await; watcher.abort(); - log::debug!("channel {} exited", &channel_number); + log::debug!("channel {} exited", channel_number); active.lock().await.remove(&channel_number); if ready_file.exists() { diff --git a/crates/ffpipeline/src/probe.rs b/crates/ffpipeline/src/probe.rs index 1a79796..cac7dd2 100644 --- a/crates/ffpipeline/src/probe.rs +++ b/crates/ffpipeline/src/probe.rs @@ -175,8 +175,7 @@ impl std::fmt::Display for ProbeResult { write!( f, "{}", - &self - .streams + self.streams .iter() .map(|s| s.to_string()) .collect::>()