skrt/
lib.rs

1//! A library for parsing and manipulating SubRip Text (SRT) subtitle files.
2//!
3//! SRT is a simple, widely-supported subtitle format used by media players,
4//! video editors, and streaming platforms. Each subtitle entry consists of:
5//! - A sequential index number
6//! - A timestamp range (start --> end)
7//! - One or more lines of text
8//!
9//! # Example
10//!
11//! ```
12//! use skrt::{Srt, Timestamp};
13//!
14//! // Parse an existing SRT file
15//! let data = r#"1
16//! 00:00:01,000 --> 00:00:04,000
17//! Hello, world!
18//!
19//! 2
20//! 00:00:05,000 --> 00:00:08,000
21//! This is a subtitle.
22//!
23//! "#;
24//!
25//! let srt = Srt::try_parse(data).unwrap();
26//! assert_eq!(2, srt.subtitles().len());
27//!
28//! // Or build one programmatically
29//! let mut srt = Srt::new();
30//! srt.add_subtitle(
31//!     Timestamp::from_millis(1000).unwrap(),
32//!     Timestamp::from_millis(4000).unwrap(),
33//!     "Hello, world!".into(),
34//! );
35//!
36//! let output = srt.serialize();
37//! ```
38//!
39//! # Format Details
40//!
41//! This crate handles common SRT variations:
42//! - Both LF and CRLF line endings
43//! - Optional UTF-8 BOM at the start of the file
44//! - Trailing whitespace after timestamps
45//! - Files that don't end with a blank line
46//!
47//! Timestamps follow the format `HH:MM:SS,mmm` where:
48//! - `HH` = hours (00-99)
49//! - `MM` = minutes (00-59)
50//! - `SS` = seconds (00-59)
51//! - `mmm` = milliseconds (000-999)
52
53#![warn(rust_2018_idioms)]
54
55use std::{
56    borrow::Cow,
57    cmp::Ordering,
58    error::Error,
59    fmt::{self, Display, Write},
60};
61
62/// Error type for SRT parsing and manipulation operations.
63///
64/// Errors that include a `position` field report the byte offset into the
65/// original input string where the error occurred. This is useful for
66/// displaying user-friendly error messages with location information.
67///
68/// # Example
69///
70/// ```
71/// use skrt::{Srt, SrtError};
72///
73/// let bad_input = "1\n00:XX:00,000 --> 00:00:01,000\nHello\n\n";
74/// let err = Srt::try_parse(bad_input).unwrap_err();
75///
76/// match err {
77///     SrtError::InvalidTimestamp { position } => {
78///         println!("Bad timestamp at byte {position}");
79///     }
80///     _ => {}
81/// }
82/// ```
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub enum SrtError {
85    /// Input ended unexpectedly.
86    ///
87    /// Returned when the parser reaches the end of input while expecting
88    /// more data, such as a complete timestamp.
89    UnexpectedEof,
90
91    /// Invalid or missing sequence number.
92    ///
93    /// Each subtitle block must begin with a positive integer sequence number.
94    /// This error is returned when the sequence number is missing, empty,
95    /// or contains non-digit characters.
96    InvalidSequenceNumber {
97        /// Byte offset where the invalid sequence number was found.
98        position: usize,
99    },
100
101    /// Timestamp format is invalid.
102    ///
103    /// Timestamps must follow the format `HH:MM:SS,mmm`. This error is
104    /// returned for malformed timestamps, invalid digit ranges (e.g.,
105    /// minutes > 59), or incorrect separators.
106    InvalidTimestamp {
107        /// Byte offset where the invalid timestamp begins.
108        position: usize,
109    },
110
111    /// Timestamp value exceeds the maximum representable range.
112    ///
113    /// The SRT format conventionally supports hours from 00-99, giving a
114    /// maximum timestamp of `99:59:59,999` (359,999,999 milliseconds).
115    TimestampOutOfRange,
116
117    /// Timestamp value is too small or large to be represented as a `i64`.
118    TimestampOverflow,
119
120    /// Shift operation would result in a negative timestamp.
121    ///
122    /// Returned by [`Timestamp::shift_millis`] when the shift amount would
123    /// make the timestamp negative.
124    NegativeTimestamp,
125
126    /// Expected ` --> ` separator between timestamps.
127    ///
128    /// The start and end timestamps must be separated by exactly ` --> `
129    /// (space, dash, dash, greater-than, space).
130    ExpectedTimeSeparator {
131        /// Byte offset where the separator was expected.
132        position: usize,
133    },
134
135    /// Expected whitespace or newline character.
136    ///
137    /// Returned when a newline was expected (e.g., after a timestamp line)
138    /// but other characters were found.
139    ExpectedNewline {
140        /// Byte offset where the newline was expected.
141        position: usize,
142    },
143}
144
145impl Display for SrtError {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        match self {
148            SrtError::UnexpectedEof => write!(f, "unexpected end of input"),
149            SrtError::InvalidSequenceNumber { position } => {
150                write!(f, "invalid sequence number at byte {position}")
151            }
152            SrtError::InvalidTimestamp { position } => {
153                write!(f, "invalid timestamp at byte {position}")
154            }
155            SrtError::TimestampOutOfRange => {
156                write!(f, "timestamp exceeds maximum range (99:59:59,999)")
157            }
158            SrtError::TimestampOverflow => write!(f, "timestamp overflows `i64`"),
159            SrtError::NegativeTimestamp => {
160                write!(f, "operation would result in negative timestamp")
161            }
162            SrtError::ExpectedTimeSeparator { position } => {
163                write!(f, "expected ' --> ' at byte {position}")
164            }
165            SrtError::ExpectedNewline { position } => {
166                write!(f, "expected newline at byte {position}")
167            }
168        }
169    }
170}
171
172impl Error for SrtError {}
173
174/// A convenient Result type alias for SRT operations.
175pub type Result<T> = std::result::Result<T, SrtError>;
176
177/// A collection of subtitles representing an SRT file.
178///
179/// `Srt` is the main type for working with subtitle data. You can either
180/// parse an existing SRT string with [`Srt::try_parse`] or build a new
181/// subtitle collection with [`Srt::new`] and [`Srt::add_subtitle`].
182///
183/// # Lifetime
184///
185/// The `'a` lifetime allows parsed subtitles to borrow text directly from
186/// the input string (zero-copy parsing). When building subtitles
187/// programmatically with owned `String` data, use `Srt<'static>`.
188///
189/// # Example
190///
191/// ```
192/// use skrt::{Srt, Timestamp};
193///
194/// let mut srt = Srt::new();
195///
196/// srt.add_subtitle(
197///     Timestamp::from_millis(0).unwrap(),
198///     Timestamp::from_millis(2000).unwrap(),
199///     "First subtitle&quot".into(),
200/// );
201///
202/// srt.add_subtitle(
203///     Timestamp::from_millis(2500).unwrap(),
204///     Timestamp::from_millis(5000).unwrap(),
205///     "Second subtitle".into(),
206/// );
207///
208/// println!("{}", srt.serialize());
209/// ```
210#[derive(Debug, Default, Clone, PartialEq, Eq)]
211pub struct Srt<'a> {
212    subtitles: Vec<Subtitle<'a>>,
213}
214
215/// A single subtitle entry.
216///
217/// Each subtitle has a sequence number, start and end timestamps, and text content. The text may
218/// contain multiple lines and can include basic HTML-like formatting tags (e.g., `<i>`, `<b>`)
219/// which are preserved as-is.
220///
221/// Subtitles are typically accessed through [`Srt::subtitles`] or [`Srt::iter`].
222#[derive(Debug, Clone, PartialEq, Eq)]
223pub struct Subtitle<'a> {
224    seq: usize,
225    start: Timestamp,
226    end: Timestamp,
227    text: Cow<'a, str>,
228}
229
230impl<'a> Subtitle<'a> {
231    /// Returns the sequence number of this subtitle.
232    ///
233    /// Sequence numbers start at 1 and increment for each subtitle, though parsed files may have
234    /// gaps or non-sequential numbering.
235    pub fn seq(&self) -> usize {
236        self.seq
237    }
238
239    /// Returns the start timestamp of this subtitle.
240    pub fn start(&self) -> Timestamp {
241        self.start
242    }
243
244    /// Returns the end timestamp of this subtitle.
245    pub fn end(&self) -> Timestamp {
246        self.end
247    }
248
249    /// Returns the text content of this subtitle.
250    ///
251    /// The text may contain newlines for multi-line subtitles and may
252    /// include formatting tags like `<i>` or `<b>`.
253    pub fn text(&self) -> &str {
254        &self.text
255    }
256
257    /// Sets the sequence number of this subtitle.
258    pub fn set_seq(&mut self, seq: usize) {
259        self.seq = seq;
260    }
261
262    /// Sets the start timestamp.
263    pub fn set_start(&mut self, start: Timestamp) {
264        self.start = start;
265    }
266
267    /// Sets the end timestamp.
268    pub fn set_end(&mut self, end: Timestamp) {
269        self.end = end;
270    }
271
272    /// Sets the subtitle text.
273    pub fn set_text(&mut self, text: Cow<'a, str>) {
274        self.text = text;
275    }
276}
277
278/// A timestamp representing a point in time within media.
279///
280/// Timestamps have millisecond precision and support a range from
281/// `00:00:00,000` to `99:59:59,999`.
282///
283/// # Display Format
284///
285/// When formatted with `Display`, timestamps use the standard SRT format:
286/// `HH:MM:SS,mmm` (e.g., `01:23:45,678`).
287///
288/// # Example
289///
290/// ```
291/// use skrt::Timestamp;
292///
293/// let ts = Timestamp::from_millis(5025000).unwrap(); // 1h 23m 45s
294/// assert_eq!("01:23:45,000", ts.to_string());
295/// assert_eq!(5025000, ts.to_millis());
296///
297/// // Shift the timestamp forward by 500ms
298/// let shifted = ts.shift_millis(500).unwrap();
299/// assert_eq!("01:23:45,500", shifted.to_string());
300/// ```
301#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Hash)]
302pub struct Timestamp {
303    hours: u16,
304    minutes: u16,
305    seconds: u16,
306    milliseconds: u16,
307}
308
309impl Ord for Timestamp {
310    fn cmp(&self, other: &Self) -> Ordering {
311        self.to_millis().cmp(&other.to_millis())
312    }
313}
314
315impl PartialOrd for Timestamp {
316    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
317        Some(self.cmp(other))
318    }
319}
320
321impl Display for Timestamp {
322    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
323        write!(
324            f,
325            "{:02}:{:02}:{:02},{:03}",
326            self.hours, self.minutes, self.seconds, self.milliseconds
327        )
328    }
329}
330
331impl Timestamp {
332    /// Creates a new timestamp from a total number of milliseconds.
333    ///
334    /// # Errors
335    ///
336    /// Returns [`SrtError::TimestampOutOfRange`] if `millis` exceeds
337    /// 359,999,999 (equivalent to `99:59:59,999`).
338    ///
339    /// # Example
340    ///
341    /// ```
342    /// use skrt::Timestamp;
343    ///
344    /// let ts = Timestamp::from_millis(3661001).unwrap();
345    /// assert_eq!("01:01:01,001", ts.to_string());
346    /// ```
347    pub fn from_millis(mut millis: u64) -> Result<Self> {
348        let hours = millis / (3_600_000);
349        millis -= hours * 3_600_000;
350        let minutes = millis / (60_000);
351        millis -= minutes * 60_000;
352        let seconds = millis / 1_000;
353        millis -= seconds * 1_000;
354
355        if hours > 99 {
356            return Err(SrtError::TimestampOutOfRange);
357        }
358
359        Ok(Timestamp {
360            hours: hours as u16,
361            minutes: minutes as u16,
362            seconds: seconds as u16,
363            milliseconds: millis as u16,
364        })
365    }
366
367    /// Converts this timestamp to a total number of milliseconds.
368    ///
369    /// # Example
370    ///
371    /// ```
372    /// use skrt::Timestamp;
373    ///
374    /// let ts = Timestamp::from_millis(12345).unwrap();
375    /// assert_eq!(12345, ts.to_millis());
376    /// ```
377    pub fn to_millis(&self) -> u64 {
378        self.hours as u64 * 3_600_000
379            + self.minutes as u64 * 60_000
380            + self.seconds as u64 * 1_000
381            + self.milliseconds as u64
382    }
383
384    /// Returns a new timestamp shifted by the given number of milliseconds.
385    ///
386    /// Positive values shift the timestamp forward (later in time), and
387    /// negative values shift it backward (earlier in time).
388    ///
389    /// # Errors
390    ///
391    /// - Returns [`SrtError::NegativeTimestamp`] if the shift would result
392    ///   in a negative timestamp.
393    /// - Returns [`SrtError::TimestampOutOfRange`] if the shift would exceed
394    ///   the maximum timestamp value.
395    ///
396    /// # Example
397    ///
398    /// ```
399    /// use skrt::Timestamp;
400    ///
401    /// let ts = Timestamp::from_millis(5000).unwrap();
402    ///
403    /// // Shift forward
404    /// let later = ts.shift_millis(1000).unwrap();
405    /// assert_eq!(6000, later.to_millis());
406    ///
407    /// // Shift backward
408    /// let earlier = ts.shift_millis(-2000).unwrap();
409    /// assert_eq!(3000, earlier.to_millis());
410    /// ```
411    pub fn shift_millis(&self, millis: i64) -> Result<Timestamp> {
412        let t1 = self.to_millis() as i64;
413
414        let t2 = match t1.checked_add(millis) {
415            Some(t) => t,
416            None => return Err(SrtError::TimestampOverflow),
417        };
418
419        if t2 < 0 {
420            return Err(SrtError::NegativeTimestamp);
421        }
422
423        Timestamp::from_millis(t2 as u64)
424    }
425}
426
427impl<'a> Srt<'a> {
428    /// Constructs an empty `Srt` with no subtitles.
429    ///
430    /// # Example
431    ///
432    /// ```
433    /// use skrt::Srt;
434    ///
435    /// let srt = Srt::new();
436    /// assert!(srt.subtitles().is_empty());
437    /// ```
438    pub fn new() -> Srt<'a> {
439        Srt {
440            subtitles: Vec::new(),
441        }
442    }
443
444    /// Returns a slice of all subtitles.
445    ///
446    /// # Example
447    ///
448    /// ```
449    /// use skrt::Srt;
450    ///
451    /// let data = "1\n00:00:00,000 --> 00:00:01,000\nHello\n\n";
452    /// let srt = Srt::try_parse(data).unwrap();
453    ///
454    /// for sub in srt.subtitles() {
455    ///     println!("{}: {}", sub.start(), sub.text());
456    /// }
457    /// ```
458    pub fn subtitles(&self) -> &[Subtitle<'a>] {
459        &self.subtitles
460    }
461
462    /// Returns an iterator over the subtitles.
463    ///
464    /// # Example
465    ///
466    /// ```
467    /// use skrt::Srt;
468    ///
469    /// let data = "1\n00:00:00,000 --> 00:00:01,000\nHello\n\n2\n00:00:02,000 --> 00:00:03,000\nWorld\n\n";
470    /// let srt = Srt::try_parse(data).unwrap();
471    ///
472    /// let texts: Vec<_> = srt.iter().map(|s| s.text()).collect();
473    /// assert_eq!(vec!["Hello", "World"], texts);
474    /// ```
475    pub fn iter(&self) -> std::slice::Iter<'_, Subtitle<'a>> {
476        self.subtitles.iter()
477    }
478
479    /// Returns a mutable iterator over the subtitles.
480    ///
481    /// This allows in-place modification of subtitles, such as adjusting
482    /// timestamps or updating text.
483    ///
484    /// # Example
485    ///
486    /// ```
487    /// use skrt::{Srt, Timestamp};
488    ///
489    /// let data = "1\n00:00:00,000 --> 00:00:01,000\nHello\n\n";
490    /// let mut srt = Srt::try_parse(data).unwrap();
491    ///
492    /// // Shift all subtitles forward by 5 seconds
493    /// for sub in srt.iter_mut() {
494    ///     sub.set_start(sub.start().shift_millis(5000).unwrap());
495    ///     sub.set_end(sub.end().shift_millis(5000).unwrap());
496    /// }
497    ///
498    /// assert_eq!(5000, srt.subtitles()[0].start().to_millis());
499    /// ```
500    pub fn iter_mut<'b>(&'b mut self) -> std::slice::IterMut<'b, Subtitle<'a>> {
501        self.subtitles.iter_mut()
502    }
503
504    /// Adds a new subtitle to the end of the collection.
505    ///
506    /// The sequence number is automatically assigned based on the current
507    /// number of subtitles (i.e., the new subtitle gets the next number
508    /// in sequence).
509    ///
510    /// # Example
511    ///
512    /// ```
513    /// use skrt::{Srt, Timestamp};
514    ///
515    /// let mut srt = Srt::new();
516    ///
517    /// srt.add_subtitle(
518    ///     Timestamp::from_millis(0).unwrap(),
519    ///     Timestamp::from_millis(1000).unwrap(),
520    ///     "First".into(),
521    /// );
522    ///
523    /// srt.add_subtitle(
524    ///     Timestamp::from_millis(1000).unwrap(),
525    ///     Timestamp::from_millis(2000).unwrap(),
526    ///     "Second".into(),
527    /// );
528    ///
529    /// assert_eq!(2, srt.subtitles().len());
530    /// ```
531    pub fn add_subtitle(&mut self, start: Timestamp, end: Timestamp, text: Cow<'a, str>) {
532        let sub = Subtitle {
533            seq: self.subtitles.len() + 1,
534            start,
535            end,
536            text,
537        };
538
539        self.subtitles.push(sub);
540    }
541
542    /// Parses an SRT-formatted string.
543    ///
544    /// This performs zero-copy parsing where possible. Subtitle text is
545    /// borrowed directly from the input string.
546    ///
547    /// # Supported Variations
548    ///
549    /// - LF (`\n`) and CRLF (`\r\n`) line endings
550    /// - Optional UTF-8 BOM at the start
551    /// - Trailing whitespace after timestamp lines
552    /// - Missing final blank line
553    ///
554    /// # Errors
555    ///
556    /// Returns an [`SrtError`] if the input is malformed. The error includes
557    /// position information for debugging.
558    ///
559    /// # Example
560    ///
561    /// ```
562    /// use skrt::Srt;
563    ///
564    /// let data = r#"1
565    /// 00:00:01,000 --> 00:00:04,000
566    /// Hello!
567    ///
568    /// "#;
569    ///
570    /// let srt = Srt::try_parse(data).unwrap();
571    /// assert_eq!(1, srt.subtitles().len());
572    /// ```
573    pub fn try_parse(mut srt: &str) -> Result<Srt<'_>> {
574        let mut subtitles = Vec::new();
575
576        let original = srt;
577        let pos = |remaining: &str| original.len() - remaining.len();
578
579        srt = strip_prefix_bom(srt);
580        while !srt.is_empty() {
581            // sequence number
582            let end = srt
583                .find(|c: char| !c.is_ascii_digit())
584                .ok_or(SrtError::InvalidSequenceNumber { position: pos(srt) })?;
585            let seq = srt[..end]
586                .parse()
587                .map_err(|_| SrtError::InvalidSequenceNumber { position: pos(srt) })?;
588            srt = &srt[end..];
589            let discard = newline_discard(srt, pos(srt))?;
590            srt = &srt[discard..];
591
592            // time start
593            let start_time = parse_time(srt, pos(srt))?;
594            srt = &srt[12..];
595
596            // time separator
597            let b = srt.as_bytes();
598            if b.len() < 5 {
599                return Err(SrtError::UnexpectedEof);
600            }
601            if !matches!(&b[0..5], b" --> ") {
602                return Err(SrtError::ExpectedTimeSeparator { position: pos(srt) });
603            }
604            srt = &srt[5..];
605
606            // time end
607            let end_time = parse_time(srt, pos(srt))?;
608            srt = &srt[12..];
609            let discard = newline_discard(srt, pos(srt))?;
610            srt = &srt[discard..];
611
612            // text
613            let mut it = srt.chars();
614            let mut end = 0;
615            let mut discard = 0;
616            while let Some(c) = it.next() {
617                if c == '\n' {
618                    // check for consecutive LF
619                    let mut it2 = it.clone();
620                    let c2 = it2.next();
621                    if let Some('\n') = c2 {
622                        discard = 2;
623                        break;
624                    }
625                } else if c == '\r' {
626                    // check for consecutive CRLF
627                    let mut it2 = it.clone();
628                    let (c2, c3, c4) = (it2.next(), it2.next(), it2.next());
629                    if let (Some('\n'), Some('\r'), Some('\n')) = (c2, c3, c4) {
630                        discard = 4;
631                        break;
632                    }
633                }
634
635                end += c.len_utf8();
636            }
637            let text = &srt[..end];
638            srt = &srt[end + discard..];
639
640            subtitles.push(Subtitle {
641                seq,
642                start: start_time,
643                end: end_time,
644                text: text.into(),
645            });
646        }
647
648        Ok(Srt { subtitles })
649    }
650
651    /// Reassigns sequence numbers starting from 1.
652    ///
653    /// This is useful after modifying the subtitle list (e.g., removing
654    /// or reordering entries) to ensure sequence numbers are contiguous.
655    ///
656    /// # Example
657    ///
658    /// ```
659    /// use skrt::{Srt, Timestamp};
660    ///
661    /// let mut srt = Srt::new();
662    /// srt.add_subtitle(
663    ///     Timestamp::from_millis(0).unwrap(),
664    ///     Timestamp::from_millis(1000).unwrap(),
665    ///     "Hello".into(),
666    /// );
667    ///
668    /// // After some modifications...
669    /// srt.resequence();
670    ///
671    /// assert_eq!(1, srt.subtitles()[0].seq());
672    /// ```
673    pub fn resequence(&mut self) {
674        for (i, sub) in self.subtitles.iter_mut().enumerate() {
675            sub.seq = i + 1;
676        }
677    }
678
679    /// Serializes the subtitles to an SRT-formatted string.
680    ///
681    /// The output uses LF line endings. Each subtitle block is separated
682    /// by a blank line.
683    ///
684    /// # Example
685    ///
686    /// ```
687    /// use skrt::{Srt, Timestamp};
688    ///
689    /// let mut srt = Srt::new();
690    /// srt.add_subtitle(
691    ///     Timestamp::from_millis(1000).unwrap(),
692    ///     Timestamp::from_millis(2000).unwrap(),
693    ///     "Hello, world!".into(),
694    /// );
695    ///
696    /// let output = srt.serialize();
697    /// assert!(output.contains("00:00:01,000 --> 00:00:02,000"));
698    /// assert!(output.contains("Hello, world!"));
699    /// ```
700    pub fn serialize(&self) -> String {
701        let mut s = String::new();
702
703        for subtitle in &self.subtitles {
704            let _ = writeln!(&mut s, "{}", subtitle.seq);
705            let _ = writeln!(&mut s, "{} --> {}", subtitle.start, subtitle.end);
706            let _ = writeln!(&mut s, "{}\n", subtitle.text);
707        }
708
709        s
710    }
711}
712
713/// Consuming iterator implementation for `Srt`.
714///
715/// This allows using `Srt` directly in a `for` loop, consuming the
716/// collection and yielding owned [`Subtitle`] values.
717///
718/// # Example
719///
720/// ```
721/// use skrt::Srt;
722///
723/// let data = "1\n00:00:00,000 --> 00:00:01,000\nHello\n\n2\n00:00:02,000 --> 00:00:03,000\nWorld\n\n";
724/// let srt = Srt::try_parse(data).unwrap();
725///
726/// for subtitle in srt {
727///     println!("{}: {}", subtitle.seq(), subtitle.text());
728/// }
729/// ```
730impl<'a> IntoIterator for Srt<'a> {
731    type Item = Subtitle<'a>;
732    type IntoIter = std::vec::IntoIter<Subtitle<'a>>;
733
734    fn into_iter(self) -> Self::IntoIter {
735        self.subtitles.into_iter()
736    }
737}
738
739impl<'a, 'b> IntoIterator for &'b Srt<'a> {
740    type Item = &'b Subtitle<'a>;
741    type IntoIter = std::slice::Iter<'b, Subtitle<'a>>;
742
743    fn into_iter(self) -> Self::IntoIter {
744        self.iter()
745    }
746}
747
748impl<'a, 'b> IntoIterator for &'b mut Srt<'a> {
749    type Item = &'b mut Subtitle<'a>;
750    type IntoIter = std::slice::IterMut<'b, Subtitle<'a>>;
751
752    fn into_iter(self) -> Self::IntoIter {
753        self.iter_mut()
754    }
755}
756
757fn parse_time(s: &str, position: usize) -> Result<Timestamp> {
758    let b = s.as_bytes();
759    if b.len() < 12 {
760        return Err(SrtError::UnexpectedEof);
761    }
762
763    let mut valid = b[0].is_ascii_digit();
764    valid &= b[1].is_ascii_digit();
765    valid &= matches!(b[2], b':');
766    valid &= matches!(b[3], b'0'..=b'5');
767    valid &= b[4].is_ascii_digit();
768    valid &= matches!(b[5], b':');
769    valid &= matches!(b[6], b'0'..=b'5');
770    valid &= b[7].is_ascii_digit();
771    valid &= matches!(b[8], b',');
772    valid &= b[9].is_ascii_digit();
773    valid &= b[10].is_ascii_digit();
774    valid &= b[11].is_ascii_digit();
775    if !valid {
776        return Err(SrtError::InvalidTimestamp { position });
777    }
778
779    Ok(Timestamp {
780        hours: (b[0] as u16 - b'0' as u16) * 10 + (b[1] as u16 - b'0' as u16),
781        minutes: (b[3] as u16 - b'0' as u16) * 10 + (b[4] as u16 - b'0' as u16),
782        seconds: (b[6] as u16 - b'0' as u16) * 10 + (b[7] as u16 - b'0' as u16),
783        milliseconds: (b[9] as u16 - b'0' as u16) * 100
784            + (b[10] as u16 - b'0' as u16) * 10
785            + (b[11] as u16 - b'0' as u16),
786    })
787}
788
789fn newline_discard(s: &str, position: usize) -> Result<usize> {
790    let mut discard = 0;
791    let mut it = s.chars();
792    while let Some(c) = it.next() {
793        match c {
794            ' ' | '\t' => discard += 1,
795            '\r' => {
796                if let Some('\n') = it.next() {
797                    discard += 2;
798                    break;
799                }
800                return Err(SrtError::ExpectedNewline {
801                    position: position + discard,
802                });
803            }
804            '\n' => {
805                discard += 1;
806                break;
807            }
808            _ => {
809                return Err(SrtError::ExpectedNewline {
810                    position: position + discard,
811                });
812            }
813        }
814    }
815    Ok(discard)
816}
817
818fn strip_prefix_bom(s: &str) -> &str {
819    s.strip_prefix("\u{feff}").unwrap_or(s)
820}
821
822#[cfg(test)]
823mod tests {
824    use super::*;
825
826    #[test]
827    fn parse_empty_input() {
828        let srt = Srt::try_parse("").unwrap();
829        assert!(srt.subtitles.is_empty());
830    }
831
832    #[test]
833    fn parse_with_bom() {
834        let data = "\u{feff}1\n00:00:00,000 --> 00:00:01,000\nHello\n\n";
835        let srt = Srt::try_parse(data).unwrap();
836        assert_eq!(1, srt.subtitles.len());
837    }
838
839    #[test]
840    fn parse_crlf_line_endings() {
841        let data = "1\r\n00:00:00,000 --> 00:00:01,000\r\nHello\r\n\r\n";
842        let srt = Srt::try_parse(data).unwrap();
843        assert_eq!(1, srt.subtitles.len());
844        assert_eq!("Hello", srt.subtitles[0].text);
845    }
846
847    #[test]
848    fn parse_single_subtitle() {
849        let data = "1\n00:00:00,000 --> 00:00:01,000\nSingle line\n\n";
850        let srt = Srt::try_parse(data).unwrap();
851        assert_eq!(1, srt.subtitles.len());
852        assert_eq!("Single line", srt.subtitles[0].text);
853    }
854
855    #[test]
856    fn parse_multiline_text() {
857        let data = "1\n00:00:00,000 --> 00:00:01,000\nLine 1\nLine 2\nLine 3\n\n";
858        let srt = Srt::try_parse(data).unwrap();
859        assert_eq!("Line 1\nLine 2\nLine 3", srt.subtitles[0].text);
860    }
861
862    #[test]
863    fn parse_trailing_whitespace_after_timestamp() {
864        let data = "1\n00:00:00,000 --> 00:00:01,000   \nHello\n\n";
865        let srt = Srt::try_parse(data).unwrap();
866        assert_eq!(1, srt.subtitles.len());
867    }
868
869    #[test]
870    fn parse_no_trailing_newline() {
871        // File ends without double newline after last subtitle
872        let data = "1\n00:00:00,000 --> 00:00:01,000\nHello";
873        let srt = Srt::try_parse(data).unwrap();
874        assert_eq!(1, srt.subtitles.len());
875        assert_eq!("Hello", srt.subtitles[0].text);
876    }
877
878    #[test]
879    fn parse_error_invalid_sequence_number() {
880        let data = "abc\n00:00:00,000 --> 00:00:01,000\nHello\n\n";
881        let err = Srt::try_parse(data).unwrap_err();
882        assert!(matches!(err, SrtError::InvalidSequenceNumber { .. }));
883    }
884
885    #[test]
886    fn parse_error_missing_sequence_number() {
887        let data = "\n00:00:00,000 --> 00:00:01,000\nHello\n\n";
888        let err = Srt::try_parse(data).unwrap_err();
889        assert!(matches!(err, SrtError::InvalidSequenceNumber { .. }));
890    }
891
892    #[test]
893    fn parse_error_invalid_timestamp_format() {
894        let data = "1\n0:00:00,000 --> 00:00:01,000\nHello\n\n";
895        let err = Srt::try_parse(data).unwrap_err();
896        assert!(matches!(err, SrtError::InvalidTimestamp { .. }));
897    }
898
899    #[test]
900    fn parse_error_invalid_minutes() {
901        let data = "1\n00:60:00,000 --> 00:00:01,000\nHello\n\n";
902        let err = Srt::try_parse(data).unwrap_err();
903        assert!(matches!(err, SrtError::InvalidTimestamp { .. }));
904    }
905
906    #[test]
907    fn parse_error_invalid_seconds() {
908        let data = "1\n00:00:60,000 --> 00:00:01,000\nHello\n\n";
909        let err = Srt::try_parse(data).unwrap_err();
910        assert!(matches!(err, SrtError::InvalidTimestamp { .. }));
911    }
912
913    #[test]
914    fn parse_error_missing_separator() {
915        let data = "1\n00:00:00,000 -> 00:00:01,000\nHello\n\n";
916        let err = Srt::try_parse(data).unwrap_err();
917        assert!(matches!(err, SrtError::ExpectedTimeSeparator { .. }));
918    }
919
920    #[test]
921    fn parse_error_truncated_input() {
922        let data = "1\n00:00:00,000 --> 00:00";
923        let err = Srt::try_parse(data).unwrap_err();
924        assert!(matches!(err, SrtError::UnexpectedEof));
925    }
926
927    #[test]
928    fn parse_error_position_is_accurate() {
929        let data = "1\n00:00:00,000 --> 00:XX:01,000\nHello\n\n";
930        let err = Srt::try_parse(data).unwrap_err();
931        // "1\n00:00:00,000 --> " ; 19 bytes
932        assert!(matches!(err, SrtError::InvalidTimestamp { position: 19 }));
933    }
934
935    #[test]
936    fn srt_new_is_empty() {
937        let srt = Srt::new();
938        assert!(srt.subtitles.is_empty());
939    }
940
941    #[test]
942    fn srt_add_subtitle() {
943        let mut srt = Srt::new();
944        srt.add_subtitle(
945            Timestamp::from_millis(0).unwrap(),
946            Timestamp::from_millis(1000).unwrap(),
947            "Hello".into(),
948        );
949        srt.add_subtitle(
950            Timestamp::from_millis(1000).unwrap(),
951            Timestamp::from_millis(2000).unwrap(),
952            "World".into(),
953        );
954
955        assert_eq!(2, srt.subtitles.len());
956        assert_eq!(1, srt.subtitles[0].seq);
957        assert_eq!(2, srt.subtitles[1].seq);
958    }
959
960    #[test]
961    fn srt_serialize_roundtrip() {
962        let mut srt = Srt::new();
963        srt.add_subtitle(
964            Timestamp::from_millis(500).unwrap(),
965            Timestamp::from_millis(1500).unwrap(),
966            "Test subtitle".into(),
967        );
968
969        let serialized = srt.serialize();
970        let parsed = Srt::try_parse(&serialized).unwrap();
971
972        assert_eq!(srt, parsed);
973    }
974
975    #[test]
976    fn timestamp_zero() {
977        let ts = Timestamp::default();
978        assert_eq!(0, ts.to_millis());
979        assert_eq!("00:00:00,000", ts.to_string());
980    }
981
982    #[test]
983    fn timestamp_max_valid() {
984        let ts = Timestamp::from_millis(359_999_999).unwrap(); // 99:59:59,999
985        assert_eq!(99, ts.hours);
986        assert_eq!(59, ts.minutes);
987        assert_eq!(59, ts.seconds);
988        assert_eq!(999, ts.milliseconds);
989        assert_eq!("99:59:59,999", ts.to_string());
990    }
991
992    #[test]
993    fn timestamp_just_over_max() {
994        let result = Timestamp::from_millis(360_000_000); // 100:00:00,000
995        assert!(matches!(result, Err(SrtError::TimestampOutOfRange)));
996    }
997
998    #[test]
999    fn timestamp_one_millisecond() {
1000        let ts = Timestamp::from_millis(1).unwrap();
1001        assert_eq!(0, ts.hours);
1002        assert_eq!(0, ts.minutes);
1003        assert_eq!(0, ts.seconds);
1004        assert_eq!(1, ts.milliseconds);
1005    }
1006
1007    #[test]
1008    fn timestamp_one_second() {
1009        let ts = Timestamp::from_millis(1000).unwrap();
1010        assert_eq!(1, ts.seconds);
1011        assert_eq!(0, ts.milliseconds);
1012    }
1013
1014    #[test]
1015    fn timestamp_one_minute() {
1016        let ts = Timestamp::from_millis(60_000).unwrap();
1017        assert_eq!(1, ts.minutes);
1018        assert_eq!(0, ts.seconds);
1019    }
1020
1021    #[test]
1022    fn timestamp_one_hour() {
1023        let ts = Timestamp::from_millis(3_600_000).unwrap();
1024        assert_eq!(1, ts.hours);
1025        assert_eq!(0, ts.minutes);
1026    }
1027
1028    #[test]
1029    fn timestamp_shift_to_zero() {
1030        let ts = Timestamp::from_millis(1000).unwrap();
1031        let shifted = ts.shift_millis(-1000).unwrap();
1032        assert_eq!(0, shifted.to_millis());
1033    }
1034
1035    #[test]
1036    fn timestamp_shift_negative_result() {
1037        let ts = Timestamp::from_millis(1000).unwrap();
1038        let err = ts.shift_millis(-1001).unwrap_err();
1039        assert!(matches!(err, SrtError::NegativeTimestamp));
1040    }
1041
1042    #[test]
1043    fn timestamp_shift_overflow() {
1044        let ts = Timestamp::from_millis(359_999_999).unwrap();
1045        let err = ts.shift_millis(1).unwrap_err();
1046        assert!(matches!(err, SrtError::TimestampOutOfRange));
1047    }
1048
1049    #[test]
1050    fn timestamp_from_millis() {
1051        let ts = Timestamp::from_millis(177428182).unwrap();
1052
1053        assert_eq!(
1054            Timestamp {
1055                hours: 49,
1056                minutes: 17,
1057                seconds: 8,
1058                milliseconds: 182
1059            },
1060            ts
1061        );
1062    }
1063
1064    #[test]
1065    fn timestamp_from_millis_overflow() {
1066        let ts = Timestamp::from_millis(9999999999999999);
1067        assert!(ts.is_err());
1068    }
1069
1070    #[test]
1071    fn timestamp_to_millis() {
1072        let ts = Timestamp {
1073            hours: 49,
1074            minutes: 17,
1075            seconds: 8,
1076            milliseconds: 182,
1077        };
1078
1079        assert_eq!(177428182, ts.to_millis());
1080    }
1081
1082    #[test]
1083    fn timestamp_shift_millis() {
1084        let t1 = Timestamp::from_millis(12345).unwrap();
1085
1086        assert_eq!(12346, t1.shift_millis(1).unwrap().to_millis());
1087        assert_eq!(12344, t1.shift_millis(-1).unwrap().to_millis());
1088        assert_eq!(0, t1.shift_millis(-12345).unwrap().to_millis());
1089        assert!(t1.shift_millis(-12346).is_err());
1090        assert!(t1.shift_millis(9999999999999999).is_err());
1091        assert!(t1.shift_millis(i64::MAX).is_err());
1092    }
1093
1094    #[test]
1095    fn timestamp_ordering() {
1096        assert!(Timestamp::from_millis(0).unwrap() < Timestamp::from_millis(1).unwrap());
1097        assert!(Timestamp::from_millis(1).unwrap() > Timestamp::from_millis(0).unwrap());
1098        assert!(Timestamp::from_millis(1).unwrap() == Timestamp::from_millis(1).unwrap());
1099        assert!(
1100            Timestamp::from_millis(1234567).unwrap() < Timestamp::from_millis(1234568).unwrap()
1101        );
1102        assert!(
1103            Timestamp::from_millis(1234568).unwrap() > Timestamp::from_millis(1234567).unwrap()
1104        );
1105        assert!(
1106            Timestamp::from_millis(1234568).unwrap() == Timestamp::from_millis(1234568).unwrap()
1107        );
1108    }
1109
1110    #[test]
1111    fn timestamp_display() {
1112        assert_eq!(
1113            "00:00:00,000",
1114            Timestamp::from_millis(0).unwrap().to_string()
1115        );
1116        assert_eq!(
1117            "00:00:00,001",
1118            Timestamp::from_millis(1).unwrap().to_string()
1119        );
1120        assert_eq!(
1121            "49:17:08,182",
1122            Timestamp::from_millis(177428182).unwrap().to_string()
1123        );
1124    }
1125
1126    #[test]
1127    fn error_is_error_trait() {
1128        fn assert_error<E: std::error::Error>() {}
1129        assert_error::<SrtError>();
1130    }
1131}