use crate::codecs::{self, DecodeError, Errors};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Signature<'a> {
None,
Kept(&'a [u8]),
Stripped(usize),
}
impl<'a> Signature<'a> {
#[must_use]
pub fn from_flags(sig_available: bool, strip_sig: bool, sig_payload: &'a [u8]) -> Self {
if strip_sig {
Signature::Stripped(sig_payload.len())
} else if sig_available {
Signature::Kept(sig_payload)
} else {
Signature::None
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ChunkSource<'a> {
ScaledText(&'a str),
Text(&'a str),
Deferred,
Bytes {
lenient: bool,
decoded: Option<&'a str>,
},
}
impl<'a> ChunkSource<'a> {
#[must_use]
pub fn select(
encoding: &str,
decoded: Option<&'a str>,
multi_byte_decoder: bool,
deferred_decoding: bool,
) -> Self {
match decoded {
Some(text) if encoding.starts_with("iso2022_") => ChunkSource::ScaledText(text),
Some(text) if !multi_byte_decoder => ChunkSource::Text(text),
_ if deferred_decoding => ChunkSource::Deferred,
_ => ChunkSource::Bytes {
lenient: multi_byte_decoder,
decoded,
},
}
}
}
fn char_slice_into(value: &str, start: usize, count: usize, out: &mut String) {
out.clear();
out.extend(value.chars().skip(start).take(count));
}
fn head(text: &str, count: usize) -> &str {
text.char_indices()
.nth(count)
.map_or(text, |(offset, _)| &text[..offset])
}
fn contains_near(decoded: &str, prefix: &str, offset: usize, payload_len: usize) -> bool {
let expected = offset.saturating_mul(decoded.len()) / payload_len.max(1);
let finder = memchr::memmem::Finder::new(prefix);
let found = |text: &str| finder.find(text.as_bytes()).is_some();
for radius in [2048, 32768 * 4] {
let mut start = expected.saturating_sub(radius).min(decoded.len());
let mut end = expected
.saturating_add(radius + prefix.len())
.min(decoded.len());
while !decoded.is_char_boundary(start) {
start -= 1;
}
while !decoded.is_char_boundary(end) {
end += 1;
}
if found(&decoded[start..end]) {
return true;
}
if start == 0 && end == decoded.len() {
return false;
}
}
found(decoded)
}
pub struct ChunkCutter<'a, I = std::vec::IntoIter<usize>> {
sequences: &'a [u8],
encoding: &'a str,
offsets: I,
chunk_size: usize,
signature: Signature<'a>,
source: ChunkSource<'a>,
decoded_len: Option<usize>,
prefixed: Vec<u8>,
done: bool,
}
impl<'a, I: Iterator<Item = usize> + Clone> ChunkCutter<'a, I> {
#[must_use]
pub fn new(
sequences: &'a [u8],
encoding: &'a str,
offsets: impl IntoIterator<IntoIter = I>,
chunk_size: usize,
signature: Signature<'a>,
source: ChunkSource<'a>,
) -> Self {
Self {
sequences,
encoding,
offsets: offsets.into_iter(),
chunk_size,
signature,
source,
decoded_len: None,
prefixed: Vec::new(),
done: false,
}
}
fn deferred_base(&self) -> &'a [u8] {
match self.signature {
Signature::Stripped(length) => &self.sequences[length..],
_ => self.sequences,
}
}
fn deferred_cut(&self, offset: usize) -> &'a [u8] {
let base = self.deferred_base();
&base[offset.min(base.len())..(offset + self.chunk_size).min(base.len())]
}
fn decode_cut(
&mut self,
start: usize,
end: usize,
errors: Errors,
out: &mut String,
) -> Result<(), DecodeError> {
let cut = &self.sequences[start.min(end)..end];
if let Signature::Kept(signature) = self.signature {
self.prefixed.clear();
self.prefixed.extend_from_slice(signature);
self.prefixed.extend_from_slice(cut);
codecs::decode_into(&self.prefixed, self.encoding, errors, out)
} else {
codecs::decode_into(cut, self.encoding, errors, out)
}
}
fn valid_cut(&self, start: usize, end: usize) -> Result<bool, DecodeError> {
let cut = &self.sequences[start.min(end)..end];
match self.signature {
Signature::Kept(signature) => {
let mut prefixed = Vec::with_capacity(signature.len() + cut.len());
prefixed.extend_from_slice(signature);
prefixed.extend_from_slice(cut);
codecs::is_valid(&prefixed, self.encoding)
}
_ => codecs::is_valid(cut, self.encoding),
}
}
fn chunk_end(&self, offset: usize) -> Option<usize> {
let end = offset + self.chunk_size;
(end <= self.sequences.len() + 8).then(|| end.min(self.sequences.len()))
}
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn validate(&self) -> Result<(), DecodeError> {
match self.source {
ChunkSource::ScaledText(_)
| ChunkSource::Text(_)
| ChunkSource::Bytes { lenient: true, .. } => {}
ChunkSource::Deferred => {
for offset in self.offsets.clone() {
let cut = self.deferred_cut(offset);
if cut.is_empty() {
break;
}
if !codecs::is_valid(cut, self.encoding)? {
return Err(DecodeError::Invalid);
}
}
}
ChunkSource::Bytes { lenient: false, .. } => {
for offset in self.offsets.clone() {
if let Some(end) = self.chunk_end(offset)
&& !self.valid_cut(offset, end)?
{
return Err(DecodeError::Invalid);
}
}
}
}
Ok(())
}
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn next_into(&mut self, out: &mut String) -> Option<Result<(), DecodeError>> {
if self.done {
return None;
}
let item = self.next_chunk(out);
if item.is_none() {
self.done = true;
}
item
}
fn next_chunk(&mut self, out: &mut String) -> Option<Result<(), DecodeError>> {
loop {
let offset = self.offsets.next()?;
let (lenient, decoded) = match self.source {
ChunkSource::ScaledText(text) => {
let decoded_len = *self.decoded_len.get_or_insert_with(|| text.chars().count());
char_slice_into(
text,
offset * decoded_len / self.sequences.len(),
self.chunk_size,
out,
);
return (!out.is_empty()).then_some(Ok(()));
}
ChunkSource::Text(text) => {
char_slice_into(text, offset, self.chunk_size, out);
return (!out.is_empty()).then_some(Ok(()));
}
ChunkSource::Deferred => {
let cut = self.deferred_cut(offset);
return (!cut.is_empty())
.then(|| codecs::decode_into(cut, self.encoding, Errors::Strict, out));
}
ChunkSource::Bytes { lenient, decoded } => (lenient, decoded),
};
let Some(end) = self.chunk_end(offset) else {
continue;
};
let errors = if lenient {
Errors::Ignore
} else {
Errors::Strict
};
let result = self.decode_cut(offset, end, errors, out);
return Some(match (result, decoded) {
(Ok(()), Some(decoded)) if lenient && offset > 0 => {
self.realign(out, decoded, offset, end)
}
(result, _) => result,
});
}
}
fn realign(
&mut self,
chunk: &mut String,
decoded: &str,
offset: usize,
end: usize,
) -> Result<(), DecodeError> {
let prefix_chars = self.chunk_size.min(16);
if contains_near(
decoded,
head(chunk, prefix_chars),
offset,
self.sequences.len(),
) {
return Ok(());
}
for delta in 0..4usize {
let start = if delta <= offset {
offset - delta
} else {
self.sequences.len().saturating_sub(delta - offset)
};
self.decode_cut(start, end, Errors::Ignore, chunk)?;
if contains_near(
decoded,
head(chunk, prefix_chars),
start,
self.sequences.len(),
) {
break;
}
}
Ok(())
}
}
impl<I: Iterator<Item = usize> + Clone> Iterator for ChunkCutter<'_, I> {
type Item = Result<String, DecodeError>;
fn next(&mut self) -> Option<Self::Item> {
let mut chunk = String::new();
self.next_into(&mut chunk)
.map(|result| result.map(|()| chunk))
}
}
pub fn cut_sequence_chunks(
sequences: &[u8],
encoding: &str,
offsets: Vec<usize>,
chunk_size: usize,
signature: Signature<'_>,
source: ChunkSource<'_>,
) -> Result<Vec<String>, DecodeError> {
ChunkCutter::new(sequences, encoding, offsets, chunk_size, signature, source).collect()
}