use core::num::NonZeroU64;
use logos::{Lexer, Logos};
use crate::error::*;
use crate::types::{Millisecond, Minute, Second};
pub use types::{Entry, Header, Hour, Timestamp};
mod types;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ParseSrtError {
#[error(transparent)]
ParseMinute(#[from] ParseMinuteError),
#[error(transparent)]
ParseSecond(#[from] ParseSecondError),
#[error(transparent)]
ParseHour(#[from] ParseHourError),
#[error(transparent)]
ParseMillisecond(#[from] ParseMillisecondError),
#[error(transparent)]
ParseIndex(#[from] ParseIndexNumberError),
#[error("unclosed duration, missing end timestamp")]
UnclosedDuration,
#[error("unopened duration, missing start timestamp")]
UnopenedDuration,
#[error("expected header line (e.g. '00:00:01,000 --> 00:00:04,000') after index {0}")]
ExpectedHeader(NonZeroU64),
#[error("non-monotonic index: expected > {last}, got {got}")]
NonMonotonicIndex {
last: u64,
got: u64,
},
#[error("unexpected token: {0}")]
Unknown(&'static str),
}
impl Default for ParseSrtError {
fn default() -> Self {
Self::Unknown("unknown lexer error")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Options {
allow_missing_index: bool,
ignore_orphan_text: bool,
ignore_broken_header: bool,
monotonic_index: bool,
}
impl Options {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn strict() -> Self {
Self {
allow_missing_index: false,
ignore_orphan_text: false,
ignore_broken_header: false,
monotonic_index: true,
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn lossy() -> Self {
Self {
allow_missing_index: true,
ignore_orphan_text: true,
ignore_broken_header: true,
monotonic_index: false,
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn allow_missing_index(&self) -> bool {
self.allow_missing_index
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn with_allow_missing_index(mut self, value: bool) -> Self {
self.set_allow_missing_index(value);
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn set_allow_missing_index(&mut self, value: bool) -> &mut Self {
self.allow_missing_index = value;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn ignore_orphan_text(&self) -> bool {
self.ignore_orphan_text
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn with_ignore_orphan_text(mut self, value: bool) -> Self {
self.set_ignore_orphan_text(value);
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn set_ignore_orphan_text(&mut self, value: bool) -> &mut Self {
self.ignore_orphan_text = value;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn ignore_broken_header(&self) -> bool {
self.ignore_broken_header
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn with_ignore_broken_header(mut self, value: bool) -> Self {
self.set_ignore_broken_header(value);
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn set_ignore_broken_header(&mut self, value: bool) -> &mut Self {
self.ignore_broken_header = value;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn monotonic_index(&self) -> bool {
self.monotonic_index
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn with_monotonic_index(mut self, value: bool) -> Self {
self.set_monotonic_index(value);
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn set_monotonic_index(&mut self, value: bool) -> &mut Self {
self.monotonic_index = value;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
const fn is_tolerant(&self) -> bool {
self.allow_missing_index || self.ignore_orphan_text || self.ignore_broken_header
}
}
impl Default for Options {
fn default() -> Self {
Self::strict()
}
}
#[derive(Debug, Logos, PartialEq)]
#[logos(
error = ParseSrtError,
extras = Option<Self>,
)]
enum Token {
#[regex(
r"[0-9]{2,3}:[0-5][0-9]:[0-5][0-9],[0-9]{3}[ \t\x0C]+-->[ \t\x0C]+[0-9]{2,3}:[0-5][0-9]:[0-5][0-9],[0-9]{3}",
parse_header
)]
Header(Header),
#[regex(r"[0-9]+", parse_number, priority = 3)]
Number(NonZeroU64),
}
#[inline]
fn parse_number(s: &mut Lexer<'_, Token>) -> Result<NonZeroU64, ParseSrtError> {
let slice = s.slice().trim();
if slice.len() > 20 {
return Err(ParseIndexNumberError::Overflow.into());
}
if slice == "0" {
return Err(ParseIndexNumberError::Zero.into());
}
slice
.parse::<u64>()
.map_err(|e| ParseIndexNumberError::ParseInt(e).into())
.and_then(|num| NonZeroU64::new(num).ok_or(ParseIndexNumberError::Zero.into()))
}
#[inline]
fn parse_header(s: &mut Lexer<'_, Token>) -> Result<Header, ParseSrtError> {
let slice = s.slice();
let arrow = slice.find("-->").unwrap();
let start_str = slice[..arrow].trim();
let end_str = slice[arrow + 3..].trim();
let start = parse_timestamp_bytes(start_str.as_bytes())?;
let end = parse_timestamp_bytes(end_str.as_bytes())?;
Ok(Header::new(start, end))
}
#[inline]
fn parse_timestamp_bytes(b: &[u8]) -> Result<Timestamp, ParseSrtError> {
let len = b.len();
let millis = Millisecond(digit3(&b[len - 3..]));
let seconds = Second(digit2(&b[len - 6..len - 4]));
let minutes = Minute(digit2(&b[len - 9..len - 7]));
let hour_len = len - 10;
let hours = match hour_len {
2 => Hour(digit2(&b[..2]) as u16),
3 => Hour(digit3(&b[..3])),
_ => return Err(ParseHourError::NotPadded.into()),
};
Ok(Timestamp::from_hmsm(hours, minutes, seconds, millis))
}
#[cfg_attr(not(tarpaulin), inline(always))]
const fn digit2(b: &[u8]) -> u8 {
(b[0] - b'0') * 10 + (b[1] - b'0')
}
#[cfg_attr(not(tarpaulin), inline(always))]
const fn digit3(b: &[u8]) -> u16 {
(b[0] - b'0') as u16 * 100 + (b[1] - b'0') as u16 * 10 + (b[2] - b'0') as u16
}
struct StateBody {
header: Header,
start: usize,
end: usize,
}
impl StateBody {
#[cfg_attr(not(tarpaulin), inline(always))]
const fn new(header: Header, start: usize, end: usize) -> Self {
Self { header, start, end }
}
}
enum State {
Index,
Header(NonZeroU64),
Body(StateBody),
SkipToBlank,
Done,
}
pub struct Parser<'a> {
input: &'a str,
lines: Lines<'a>,
state: State,
opts: Options,
last_index: u64,
}
impl<'a> Parser<'a> {
pub fn strict(input: &'a str) -> Self {
Self::with_options(input, Options::strict())
}
pub fn lossy(input: &'a str) -> Self {
Self::with_options(input, Options::lossy())
}
pub fn with_options(input: &'a str, opts: Options) -> Self {
Self {
input,
lines: Lines::new(input),
state: State::Index,
opts,
last_index: 0,
}
}
}
impl<'a> Iterator for Parser<'a> {
type Item = Result<Entry<&'a str>, ParseSrtError>;
fn next(&mut self) -> Option<Self::Item> {
loop {
match self.state {
State::Done => return None,
State::SkipToBlank => {
let Some(line) = self.lines.next() else {
self.state = State::Done;
return None;
};
if line.trim_start_matches('\u{feff}').is_empty() {
self.state = State::Index;
}
}
State::Index => {
let Some(line) = self.lines.next() else {
self.state = State::Done;
return None;
};
let trimmed = line.trim_start_matches('\u{feff}');
if trimmed.is_empty() {
continue;
}
match lex(trimmed) {
Ok(Some(Token::Number(index))) => {
if self.opts.monotonic_index && index.get() <= self.last_index {
if self.opts.is_tolerant() {
self.state = State::SkipToBlank;
continue;
}
self.state = State::Done;
return Some(Err(ParseSrtError::NonMonotonicIndex {
last: self.last_index,
got: index.get(),
}));
}
self.state = State::Header(index);
}
Ok(Some(Token::Header(header))) if self.opts.allow_missing_index => {
let offset = line.as_ptr() as usize - self.input.as_ptr() as usize + line.len();
self.state = State::Body(StateBody::new(header, offset, offset));
}
Ok(_) | Err(_) if self.opts.ignore_orphan_text => {
continue;
}
Ok(Some(_)) | Ok(None) => {
self.state = State::Done;
return Some(Err(ParseSrtError::Unknown(
"expected subtitle index number",
)));
}
Err(e) => {
self.state = State::Done;
return Some(Err(e));
}
}
}
State::Header(index) => {
let Some(line) = self.lines.next() else {
self.state = State::Done;
return if self.opts.ignore_broken_header {
None
} else {
Some(Err(ParseSrtError::ExpectedHeader(index)))
};
};
let trimmed = line.trim_start_matches('\u{feff}');
match lex(trimmed) {
Ok(Some(Token::Header(mut header))) => {
header.set_index(index);
let offset = line.as_ptr() as usize - self.input.as_ptr() as usize + line.len();
self.state = State::Body(StateBody::new(header, offset, offset));
}
_ if self.opts.ignore_broken_header => {
if trimmed.is_empty() {
self.state = State::Index;
} else {
self.state = State::SkipToBlank;
}
}
Ok(_) => {
self.state = State::Done;
return Some(Err(ParseSrtError::ExpectedHeader(index)));
}
Err(e) => {
self.state = State::Done;
return Some(Err(e));
}
}
}
State::Body(ref mut body) => {
let StateBody { header, start, end } = body;
let Some(line) = self.lines.next() else {
let body = body_slice(self.input, *start, *end);
let entry = Entry::new(header.clone(), body);
if let Some(idx) = header.index() {
self.last_index = idx.get();
}
self.state = State::Done;
return Some(Ok(entry));
};
let trimmed = line.trim_start_matches('\u{feff}');
if trimmed.is_empty() {
let body = body_slice(self.input, *start, *end);
let entry = Entry::new(header.clone(), body);
if let Some(idx) = header.index() {
self.last_index = idx.get();
}
self.state = State::Index;
return Some(Ok(entry));
}
let line_offset = line.as_ptr() as usize - self.input.as_ptr() as usize;
if *start == *end {
*start = line_offset;
}
*end = line_offset + line.len();
}
}
}
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
fn body_slice(input: &str, start: usize, end: usize) -> &str {
if start >= end { "" } else { &input[start..end] }
}
fn lex(line: &str) -> Result<Option<Token>, ParseSrtError> {
match Token::lexer(line).next() {
Some(result) => result.map(Some),
None => Ok(None),
}
}
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
pub struct Writer<W> {
inner: W,
has_written: bool,
}
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
const _: () = {
use std::io::{self, Write};
impl<W: Write> Writer<W> {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn new(inner: W) -> Self {
Self {
inner,
has_written: false,
}
}
pub fn write<T: AsRef<str>>(&mut self, entry: &Entry<T>) -> io::Result<()> {
if self.has_written {
self.inner.write_all(b"\n")?;
}
self.has_written = true;
let header = entry.header_ref();
self.inner.write_all(header.encode().as_str().as_bytes())?;
let body = entry.body_ref().as_ref();
if !body.is_empty() {
self.inner.write_all(body.as_bytes())?;
}
self.inner.write_all(b"\n")
}
pub fn write_all<'a, T, I>(&mut self, entries: I) -> io::Result<()>
where
T: AsRef<str> + 'a,
I: IntoIterator<Item = &'a Entry<T>>,
{
for entry in entries {
self.write(entry)?;
}
Ok(())
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn into_inner(self) -> W {
self.inner
}
}
};
struct Lines<'a> {
input: &'a str,
pos: usize,
}
impl<'a> Lines<'a> {
fn new(input: &'a str) -> Self {
Self { input, pos: 0 }
}
}
impl<'a> Iterator for Lines<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
if self.pos >= self.input.len() {
return None;
}
let bytes = &self.input.as_bytes()[self.pos..];
#[cfg(all(feature = "memchr", not(miri)))]
let found = memchr::memchr(b'\n', bytes);
#[cfg(not(all(feature = "memchr", not(miri))))]
let found = bytes.iter().position(|&b| b == b'\n');
let line_end = found
.map(|i| {
let end = if i > 0 && bytes[i - 1] == b'\r' {
i - 1
} else {
i
};
(end, i + 1)
})
.unwrap_or((bytes.len(), bytes.len()));
let line = &self.input[self.pos..self.pos + line_end.0];
self.pos += line_end.1;
Some(line)
}
}