cea608_types/
lib.rs

1// Copyright (C) 2024 Matthew Waters <matthew@centricular.com>
2//
3// Licensed under the MIT license <LICENSE-MIT> or
4// http://opensource.org/licenses/MIT>, at your option. This file may not be
5// copied, modified, or distributed except according to those terms.
6
7#![deny(missing_debug_implementations)]
8#![deny(missing_docs)]
9
10//! # cea608-types
11//!
12//! Provides the necessary infrastructure to read and write CEA-608 byte pairs
13//!
14//! The reference for this implementation is the [ANSI/CTA-608-E S-2019](https://shop.cta.tech/products/line-21-data-services) specification.
15
16use std::collections::VecDeque;
17
18use tables::{Channel, Code, Field, MidRow, PreambleAddressCode};
19
20#[macro_use]
21extern crate log;
22
23pub mod tables;
24
25/// Various possible errors when parsing data
26#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
27pub enum ParserError {
28    /// Invalid parity
29    #[error("Invalid parity")]
30    InvalidParity,
31    /// Length of data does not match length advertised
32    #[error("Length of the data ({actual}) does not match the expected length ({expected})")]
33    LengthMismatch {
34        /// The expected size
35        expected: usize,
36        /// The actual size
37        actual: usize,
38    },
39}
40
41/// An error enum returned when writing data fails
42#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
43pub enum WriterError {
44    /// Writing would overflow by how many bytes
45    #[error("Writing would overflow by {0} bytes")]
46    WouldOverflow(usize),
47    /// It is not possible to write to this resource
48    #[error("Read only resource")]
49    ReadOnly,
50}
51
52impl From<tables::CodeError> for ParserError {
53    fn from(err: tables::CodeError) -> Self {
54        match err {
55            tables::CodeError::LengthMismatch { expected, actual } => {
56                ParserError::LengthMismatch { expected, actual }
57            }
58            tables::CodeError::InvalidParity => ParserError::InvalidParity,
59        }
60    }
61}
62
63/// A CEA-08 presentation mode
64#[derive(Debug, Copy, Clone, PartialEq, Eq)]
65pub enum Mode {
66    /// The Pop-On CEA-608 mode.  Text is stored in a hidden buffer that is swapped with the
67    /// displayed text.
68    PopOn,
69    /// The Paint-On CEA-608 mode.  Text is written directly to the display as it arrives.
70    PaintOn,
71    /// The Roll-Up 2 CEA-608 mode.  Text is appended to rows and on a CR command, all of the rows
72    /// move up one row.  This variant contains 2 rows of display.
73    RollUp2,
74    /// The Roll-Up 2 CEA-608 mode.  Text is appended to rows and on a CR command, all of the rows
75    /// move up one row.  This variant contains 3 rows of display.
76    RollUp3,
77    /// The Roll-Up 2 CEA-608 mode.  Text is appended to rows and on a CR command, all of the rows
78    /// move up one row.  This variant contains 4 rows of display.
79    RollUp4,
80}
81
82impl Mode {
83    /// Whether this mode is a roll-up mode
84    pub fn is_rollup(&self) -> bool {
85        matches!(self, Self::RollUp2 | Self::RollUp3 | Self::RollUp4)
86    }
87
88    /// How many rows of roll up captions this mode supports.  Other modes will return [`None`].
89    pub fn rollup_rows(&self) -> Option<u8> {
90        match self {
91            Self::RollUp2 => Some(2),
92            Self::RollUp3 => Some(3),
93            Self::RollUp4 => Some(4),
94            _ => None,
95        }
96    }
97}
98
99/// Text information
100#[derive(Debug, Copy, Clone, PartialEq, Eq)]
101pub struct Text {
102    /// Whether the character needs the remove the previous character.
103    pub needs_backspace: bool,
104    /// Optional character 1
105    pub char1: Option<char>,
106    /// Optional character 2
107    pub char2: Option<char>,
108    /// The last channel received
109    pub channel: Channel,
110}
111
112/// CEA-608 information
113#[derive(Debug, Copy, Clone, PartialEq, Eq)]
114pub enum Cea608 {
115    /// Text
116    Text(Text),
117    /// The channel is changing (or resending) mode
118    NewMode(Channel, Mode),
119    /// Erase the currently displayed window contents
120    EraseDisplay(Channel),
121    /// Erase the undisplayed window contents
122    EraseNonDisplay(Channel),
123    /// A carriage return was received
124    CarriageReturn(Channel),
125    /// A backspace was received
126    Backspace(Channel),
127    /// An end of caption was received.  In Pop-On mode, swap the undisplayed and displayed window
128    /// contents
129    EndOfCaption(Channel),
130    /// Offset the cursor
131    TabOffset(Channel, u8),
132    /// Delete characters from the current cursor position to the end of the row
133    DeleteToEndOfRow(Channel),
134    /// A preamble was received
135    Preamble(Channel, PreambleAddressCode),
136    /// A mid-row was received
137    MidRowChange(Channel, MidRow),
138}
139
140impl Cea608 {
141    /// The channel for this parsed CEA-608 data
142    pub fn channel(&self) -> Channel {
143        match self {
144            Self::Text(text) => text.channel,
145            Self::NewMode(chan, _) => *chan,
146            Self::EraseDisplay(chan) => *chan,
147            Self::EraseNonDisplay(chan) => *chan,
148            Self::CarriageReturn(chan) => *chan,
149            Self::Backspace(chan) => *chan,
150            Self::EndOfCaption(chan) => *chan,
151            Self::TabOffset(chan, _) => *chan,
152            Self::Preamble(chan, _) => *chan,
153            Self::MidRowChange(chan, _) => *chan,
154            Self::DeleteToEndOfRow(chan) => *chan,
155        }
156    }
157
158    /// Convert into one or two [`Code`] values.
159    pub fn into_code(&self, field: Field) -> [Code; 2] {
160        match self {
161            Self::Text(text) => {
162                let mut ret = [Code::NUL, Code::NUL];
163                if let Some(char1) = text.char1 {
164                    ret[0] = Code::from_char(char1, text.channel).unwrap();
165                }
166                if let Some(char2) = text.char2 {
167                    ret[1] = Code::from_char(char2, text.channel).unwrap();
168                }
169                ret
170            }
171            Self::NewMode(chan, mode) => [
172                Code::Control(tables::ControlCode {
173                    field: Some(field),
174                    channel: *chan,
175                    control: match mode {
176                        Mode::RollUp2 => tables::Control::RollUp2,
177                        Mode::RollUp3 => tables::Control::RollUp3,
178                        Mode::RollUp4 => tables::Control::RollUp4,
179                        Mode::PaintOn => tables::Control::ResumeDirectionCaptioning,
180                        Mode::PopOn => tables::Control::ResumeCaptionLoading,
181                    },
182                }),
183                Code::NUL,
184            ],
185            Self::EraseDisplay(chan) => [
186                Code::Control(tables::ControlCode {
187                    field: Some(field),
188                    channel: *chan,
189                    control: tables::Control::EraseDisplayedMemory,
190                }),
191                Code::NUL,
192            ],
193            Self::EraseNonDisplay(chan) => [
194                Code::Control(tables::ControlCode {
195                    field: Some(field),
196                    channel: *chan,
197                    control: tables::Control::EraseNonDisplayedMemory,
198                }),
199                Code::NUL,
200            ],
201            Self::CarriageReturn(chan) => [
202                Code::Control(tables::ControlCode {
203                    field: Some(field),
204                    channel: *chan,
205                    control: tables::Control::CarriageReturn,
206                }),
207                Code::NUL,
208            ],
209            Self::Backspace(chan) => [
210                Code::Control(tables::ControlCode {
211                    field: Some(field),
212                    channel: *chan,
213                    control: tables::Control::Backspace,
214                }),
215                Code::NUL,
216            ],
217            Self::EndOfCaption(chan) => [
218                Code::Control(tables::ControlCode {
219                    field: Some(field),
220                    channel: *chan,
221                    control: tables::Control::EndOfCaption,
222                }),
223                Code::NUL,
224            ],
225            Self::TabOffset(chan, count) => [
226                Code::Control(tables::ControlCode {
227                    field: Some(field),
228                    channel: *chan,
229                    control: match count {
230                        1 => tables::Control::TabOffset1,
231                        2 => tables::Control::TabOffset2,
232                        3 => tables::Control::TabOffset3,
233                        _ => unreachable!(),
234                    },
235                }),
236                Code::NUL,
237            ],
238            Self::Preamble(chan, preamble) => [
239                Code::Control(tables::ControlCode {
240                    field: Some(field),
241                    channel: *chan,
242                    control: tables::Control::PreambleAddress(*preamble),
243                }),
244                Code::NUL,
245            ],
246            Self::MidRowChange(chan, midrow) => [
247                Code::Control(tables::ControlCode {
248                    field: Some(field),
249                    channel: *chan,
250                    control: tables::Control::MidRow(*midrow),
251                }),
252                Code::NUL,
253            ],
254            Self::DeleteToEndOfRow(chan) => [
255                Code::Control(tables::ControlCode {
256                    field: Some(field),
257                    channel: *chan,
258                    control: tables::Control::DeleteToEndOfRow,
259                }),
260                Code::NUL,
261            ],
262        }
263    }
264}
265
266/// Helper struct that has two purposes:
267/// 1. Tracks the previous data for control code de-duplication
268/// 2. Adds the last received channel to non control codes.
269///
270/// This object only keeps data for a single [`Field`]
271#[derive(Debug, Default)]
272pub struct Cea608State {
273    last_data: Option<[u8; 2]>,
274    last_channel: Option<Channel>,
275    last_received_field: Option<Field>,
276}
277
278impl Cea608State {
279    /// Decode the provided bytes into an optional parsed [`Cea608`] command.
280    pub fn decode(&mut self, data: [u8; 2]) -> Result<Option<Cea608>, ParserError> {
281        trace!("decoding {data:x?}, last data {:x?}", self.last_data);
282        let code = Code::from_data(data)?;
283
284        if Some(data) == self.last_data {
285            if let Code::Control(_control) = code[0] {
286                debug!("Skipping duplicate");
287                return Ok(None);
288            }
289        }
290        self.last_data = Some(data);
291        trace!("decoded into codes {code:x?}");
292
293        // TODO: handle xds and text mode
294
295        match code {
296            [Code::Control(control_code), _] => {
297                let channel = control_code.channel();
298                self.last_channel = Some(channel);
299                if let Some(field) = control_code.field() {
300                    self.last_received_field = Some(field);
301                }
302                Ok(Some(match control_code.code() {
303                    tables::Control::MidRow(midrow) => Cea608::MidRowChange(channel, midrow),
304                    tables::Control::PreambleAddress(preamble) => {
305                        Cea608::Preamble(channel, preamble)
306                    }
307                    tables::Control::EraseDisplayedMemory => Cea608::EraseDisplay(channel),
308                    tables::Control::EraseNonDisplayedMemory => Cea608::EraseNonDisplay(channel),
309                    tables::Control::CarriageReturn => Cea608::CarriageReturn(channel),
310                    tables::Control::Backspace => Cea608::Backspace(channel),
311                    tables::Control::EndOfCaption => Cea608::EndOfCaption(channel),
312                    tables::Control::RollUp2 => Cea608::NewMode(channel, Mode::RollUp2),
313                    tables::Control::RollUp3 => Cea608::NewMode(channel, Mode::RollUp3),
314                    tables::Control::RollUp4 => Cea608::NewMode(channel, Mode::RollUp4),
315                    tables::Control::ResumeDirectionCaptioning => {
316                        Cea608::NewMode(channel, Mode::PaintOn)
317                    }
318                    tables::Control::ResumeCaptionLoading => Cea608::NewMode(channel, Mode::PopOn),
319                    tables::Control::TabOffset1 => Cea608::TabOffset(channel, 1),
320                    tables::Control::TabOffset2 => Cea608::TabOffset(channel, 2),
321                    tables::Control::TabOffset3 => Cea608::TabOffset(channel, 3),
322                    tables::Control::DeleteToEndOfRow => Cea608::DeleteToEndOfRow(channel),
323                    // TODO: TextRestart, ResumeTextDisplay
324                    _ => {
325                        if let Some(char) = code[0].char() {
326                            Cea608::Text(Text {
327                                needs_backspace: code[0].needs_backspace(),
328                                char1: Some(char),
329                                char2: None,
330                                channel,
331                            })
332                        } else {
333                            return Ok(None);
334                        }
335                    }
336                }))
337            }
338            _ => {
339                let Some(channel) = self.last_channel else {
340                    return Ok(None);
341                };
342                let char1 = code[0].char();
343                let char2 = code[1].char();
344                if char1.is_some() || char2.is_some() {
345                    Ok(Some(Cea608::Text(Text {
346                        needs_backspace: false,
347                        char1,
348                        char2,
349                        channel,
350                    })))
351                } else {
352                    Ok(None)
353                }
354            }
355        }
356    }
357
358    /// The [`Field`] that some specific [`tables::Control`] codes referenced.  Can be used to detect field
359    /// reversal of the incoming data.
360    pub fn last_received_field(&self) -> Option<Field> {
361        self.last_received_field
362    }
363
364    /// Reset the state to that of an initially constructed object.
365    pub fn reset(&mut self) {
366        *self = Self::default();
367    }
368}
369
370/// A writer that handles combining single byte [`Code`]s and double byte [`Code`]s.
371#[derive(Debug, Default)]
372pub struct Cea608Writer {
373    pending: VecDeque<Code>,
374    pending_code: Option<Code>,
375}
376
377impl Cea608Writer {
378    /// Push a [`Code`] into this writer
379    pub fn push(&mut self, code: Code) {
380        if code == Code::NUL {
381            return;
382        }
383        self.pending.push_front(code)
384    }
385
386    /// Pop a [`Code`] from this writer
387    pub fn pop(&mut self) -> [u8; 2] {
388        let mut ret = [0x80; 2];
389        let mut prev = None::<Code>;
390
391        if let Some(code) = self.pending_code.take() {
392            trace!("returning pending code {code:?}");
393            code.write_into(&mut ret);
394            return ret;
395        }
396
397        while let Some(code) = self.pending.pop_back() {
398            if let Some(prev) = prev {
399                trace!("have prev {prev:?}");
400                if code.byte_len() == 1 {
401                    let mut data = [0; 2];
402                    prev.write_into(&mut ret);
403                    code.write_into(&mut data);
404                    ret[1] = data[0];
405                    trace!("have 1 byte code {code:?}, returning {ret:x?}");
406                    return ret;
407                } else if code.needs_backspace() {
408                    self.pending_code = Some(code);
409                    let mut data = [0; 2];
410                    prev.write_into(&mut ret);
411                    Code::Space.write_into(&mut data);
412                    ret[1] = data[0];
413                    trace!("have backspace needing code {code:?} stored as pending, pushing space with previous code {prev:?}");
414                    return ret;
415                } else {
416                    self.pending_code = Some(code);
417                    prev.write_into(&mut ret);
418                    trace!("have two byte code {code:?} stored as pending, pushing space");
419                    return ret;
420                }
421            } else if code.needs_backspace() {
422                // all back space needing codes are 2 byte commands
423                self.pending_code = Some(code);
424                Code::Space.write_into(&mut ret);
425                trace!("have backspace needing code {code:?} stored as pending, pushing space");
426                return ret;
427            } else if code.byte_len() == 1 {
428                prev = Some(code);
429            } else {
430                trace!("have standalone 2 byte code {code:?}");
431                code.write_into(&mut ret);
432                return ret;
433            }
434        }
435        if let Some(prev) = prev {
436            trace!("have no more pending codes, writing prev {prev:?}");
437            prev.write_into(&mut ret);
438        }
439        ret
440    }
441
442    /// The number of codes currently stored
443    pub fn n_codes(&self) -> usize {
444        self.pending.len() + if self.pending_code.is_some() { 1 } else { 0 }
445    }
446
447    /// Reset as if it was a newly created instance
448    pub fn reset(&mut self) {
449        *self = Self::default();
450    }
451}
452
453/// A CEA-608 caption identifier unique within a CEA-608 stream
454#[derive(Debug, Copy, Clone, PartialEq, Eq)]
455pub enum Id {
456    /// The CC1 caption stream placed in field 1 with caption channel 1.
457    CC1,
458    /// The CC2 caption stream placed in field 1 with caption channel 2.
459    CC2,
460    /// The CC1 caption stream placed in field 2 with caption channel 1.
461    CC3,
462    /// The CC4 caption stream placed in field 2 with caption channel 2.
463    CC4,
464    // TODO: add Text1/2
465}
466
467impl Id {
468    /// The [`Field`] that this [`Id`] is contained within
469    pub fn field(&self) -> Field {
470        match self {
471            Self::CC1 | Self::CC2 => Field::ONE,
472            Self::CC3 | Self::CC4 => Field::TWO,
473        }
474    }
475
476    /// The caption [`Channel`] that this [`Id`] references
477    pub fn channel(&self) -> Channel {
478        match self {
479            Self::CC1 | Self::CC3 => Channel::ONE,
480            Self::CC2 | Self::CC4 => Channel::TWO,
481        }
482    }
483
484    /// Construct an [`Id`] from a [`Field`] and [`Channel`]
485    pub fn from_caption_field_channel(field: Field, channel: Channel) -> Self {
486        match (field, channel) {
487            (Field::ONE, Channel::ONE) => Self::CC1,
488            (Field::ONE, Channel::TWO) => Self::CC2,
489            (Field::TWO, Channel::ONE) => Self::CC3,
490            (Field::TWO, Channel::TWO) => Self::CC4,
491        }
492    }
493
494    /// Construct an [`Id`] from its integer value in the range [1, 4]
495    pub fn from_value(value: i8) -> Self {
496        match value {
497            1 => Self::CC1,
498            2 => Self::CC2,
499            3 => Self::CC3,
500            4 => Self::CC4,
501            _ => unreachable!(),
502        }
503    }
504}
505
506#[cfg(test)]
507mod test {
508    use self::tables::ControlCode;
509
510    use super::*;
511    use crate::tests::*;
512
513    #[test]
514    fn state_duplicate_control() {
515        test_init_log();
516        let mut data = vec![];
517        Code::Control(ControlCode::new(
518            Field::ONE,
519            Channel::ONE,
520            tables::Control::EraseDisplayedMemory,
521        ))
522        .write(&mut data)
523        .unwrap();
524        let mut state = Cea608State::default();
525        assert_eq!(
526            Ok(Some(Cea608::EraseDisplay(Channel::ONE))),
527            state.decode([data[0], data[1]])
528        );
529        assert_eq!(state.last_received_field(), Some(Field::ONE));
530        assert_eq!(Ok(None), state.decode([data[0], data[1]]));
531        assert_eq!(state.last_received_field(), Some(Field::ONE));
532    }
533
534    #[test]
535    fn state_text_after_control() {
536        test_init_log();
537        let mut state = Cea608State::default();
538
539        let mut data = vec![];
540        Code::Control(ControlCode::new(
541            Field::ONE,
542            Channel::ONE,
543            tables::Control::RollUp2,
544        ))
545        .write(&mut data)
546        .unwrap();
547        assert_eq!(
548            Ok(Some(Cea608::NewMode(Channel::ONE, Mode::RollUp2))),
549            state.decode([data[0], data[1]])
550        );
551        assert_eq!(state.last_received_field(), Some(Field::ONE));
552
553        let mut data = vec![];
554        Code::LatinCapitalA.write(&mut data).unwrap();
555        assert_eq!(
556            Ok(Some(Cea608::Text(Text {
557                needs_backspace: false,
558                char1: Some('A'),
559                char2: None,
560                channel: Channel::ONE,
561            }))),
562            state.decode([data[0], 0x80])
563        );
564        assert_eq!(state.last_received_field(), Some(Field::ONE));
565
566        let mut data = vec![];
567        Code::Control(ControlCode::new(
568            Field::TWO,
569            Channel::TWO,
570            tables::Control::RollUp2,
571        ))
572        .write(&mut data)
573        .unwrap();
574        assert_eq!(
575            Ok(Some(Cea608::NewMode(Channel::TWO, Mode::RollUp2))),
576            state.decode([data[0], data[1]])
577        );
578        assert_eq!(state.last_received_field(), Some(Field::TWO));
579
580        let mut data = vec![];
581        Code::LatinCapitalA.write(&mut data).unwrap();
582        assert_eq!(
583            Ok(Some(Cea608::Text(Text {
584                needs_backspace: false,
585                char1: Some('A'),
586                char2: None,
587                channel: Channel::TWO,
588            }))),
589            state.decode([data[0], 0x80])
590        );
591    }
592
593    #[test]
594    fn writer_padding() {
595        test_init_log();
596        let mut writer = Cea608Writer::default();
597        assert_eq!(writer.pop(), [0x80, 0x80]);
598    }
599
600    #[test]
601    fn writer_single_byte_code() {
602        test_init_log();
603        let mut writer = Cea608Writer::default();
604        writer.push(Code::LatinLowerA);
605        assert_eq!(writer.pop(), [0x61, 0x80]);
606        assert_eq!(writer.pop(), [0x80, 0x80]);
607    }
608
609    #[test]
610    fn writer_two_single_byte_codes() {
611        test_init_log();
612        let mut writer = Cea608Writer::default();
613        writer.push(Code::LatinLowerA);
614        writer.push(Code::LatinLowerB);
615        assert_eq!(writer.pop(), [0x61, 0x62]);
616        assert_eq!(writer.pop(), [0x80, 0x80]);
617    }
618
619    #[test]
620    fn writer_single_byte_and_control() {
621        test_init_log();
622        let mut writer = Cea608Writer::default();
623        writer.push(Code::LatinLowerA);
624        writer.push(Code::Control(ControlCode::new(
625            Field::ONE,
626            Channel::ONE,
627            tables::Control::DegreeSign,
628        )));
629        assert_eq!(writer.pop(), [0x61, 0x80]);
630        assert_eq!(writer.pop(), [0x91, 0x31]);
631        assert_eq!(writer.pop(), [0x80, 0x80]);
632    }
633
634    #[test]
635    fn writer_single_byte_and_control_needing_backspace() {
636        test_init_log();
637        let mut writer = Cea608Writer::default();
638        writer.push(Code::LatinLowerA);
639        writer.push(Code::Control(ControlCode::new(
640            Field::ONE,
641            Channel::ONE,
642            tables::Control::Tilde,
643        )));
644        assert_eq!(writer.pop(), [0x61, 0x20]);
645        assert_eq!(writer.pop(), [0x13, 0x2f]);
646        assert_eq!(writer.pop(), [0x80, 0x80]);
647    }
648
649    #[test]
650    fn writer_control_needing_backspace() {
651        test_init_log();
652        let mut writer = Cea608Writer::default();
653        writer.push(Code::Control(ControlCode::new(
654            Field::ONE,
655            Channel::ONE,
656            tables::Control::Tilde,
657        )));
658        assert_eq!(writer.pop(), [0x20, 0x80]);
659        assert_eq!(writer.pop(), [0x13, 0x2f]);
660        assert_eq!(writer.pop(), [0x80, 0x80]);
661    }
662
663    #[test]
664    fn writer_control() {
665        test_init_log();
666        let mut writer = Cea608Writer::default();
667        writer.push(Code::Control(ControlCode::new(
668            Field::ONE,
669            Channel::ONE,
670            tables::Control::DegreeSign,
671        )));
672        assert_eq!(writer.pop(), [0x91, 0x31]);
673        assert_eq!(writer.pop(), [0x80, 0x80]);
674    }
675
676    #[test]
677    fn state_into_writer() {
678        test_init_log();
679        let stream = [[0x20, 0x80], [0x13, 0x2f], [0x80, 0x80]];
680        let mut state = Cea608State::default();
681        let mut writer = Cea608Writer::default();
682        for pair in stream {
683            let Some(cea608) = state.decode(pair).unwrap() else {
684                continue;
685            };
686            for code in cea608.into_code(Field::ONE) {
687                writer.push(code);
688            }
689        }
690        assert_eq!(writer.pop(), [0x20, 0x80]);
691        assert_eq!(writer.pop(), [0x13, 0x2f]);
692        assert_eq!(writer.pop(), [0x80, 0x80]);
693    }
694
695    #[test]
696    fn cea608_to_from_code() {
697        test_init_log();
698
699        let controls = [
700            tables::Control::ResumeCaptionLoading,
701            tables::Control::RollUp2,
702            tables::Control::RollUp3,
703            tables::Control::RollUp4,
704            tables::Control::EndOfCaption,
705            tables::Control::ResumeDirectionCaptioning,
706            tables::Control::tab_offset(1).unwrap(),
707            tables::Control::tab_offset(2).unwrap(),
708            tables::Control::tab_offset(3).unwrap(),
709            tables::Control::PreambleAddress(PreambleAddressCode::new(
710                3,
711                true,
712                tables::PreambleType::Indent4,
713            )),
714            tables::Control::EraseDisplayedMemory,
715            tables::Control::EraseNonDisplayedMemory,
716            tables::Control::CarriageReturn,
717            tables::Control::Backspace,
718            tables::Control::DeleteToEndOfRow,
719        ];
720        let controls_with_preceding_overwritten_char = [tables::Control::MidRow(
721            MidRow::new_color(tables::Color::Green, true),
722        )];
723
724        let codes = [Code::PercentSign, Code::LatinLowerA];
725
726        let mut writer = Cea608Writer::default();
727
728        let mut state = Cea608State::default();
729
730        for field in [Field::ONE, Field::TWO] {
731            for channel in [Channel::ONE, Channel::TWO] {
732                for control in controls {
733                    let code = Code::Control(ControlCode {
734                        field: Some(field),
735                        channel,
736                        control,
737                    });
738                    writer.push(code);
739                    let cea608 = state.decode(writer.pop()).unwrap().unwrap();
740                    assert_eq!(cea608.into_code(field)[0], code);
741                }
742                for control in controls_with_preceding_overwritten_char {
743                    let code = Code::Control(ControlCode {
744                        field: Some(field),
745                        channel,
746                        control,
747                    });
748                    writer.push(code);
749                    writer.pop(); // eat the preceding char
750                    let cea608 = state.decode(writer.pop()).unwrap().unwrap();
751                    assert_eq!(cea608.into_code(field)[0], code);
752                }
753            }
754        }
755
756        for code in codes {
757            debug!("pushing {code:?}");
758            writer.push(code);
759            let data = writer.pop();
760            let cea608 = state.decode(data).unwrap().unwrap();
761            assert_eq!(cea608.into_code(Field::ONE)[0], code);
762        }
763    }
764
765    #[test]
766    fn writer_ignore_padding() {
767        test_init_log();
768
769        let mut writer = Cea608Writer::default();
770        writer.push(Code::NUL);
771        writer.push(Code::LatinLowerA);
772        assert_eq!(writer.pop(), [0x61, 0x80]);
773    }
774}
775
776#[cfg(test)]
777pub(crate) mod tests {
778    use std::sync::OnceLock;
779
780    static TRACING: OnceLock<()> = OnceLock::new();
781
782    pub fn test_init_log() {
783        TRACING.get_or_init(|| {
784            env_logger::init();
785        });
786    }
787}