use std::{
collections::BinaryHeap,
io::{self, Cursor, Read},
ops::ControlFlow,
};
use encoding_rs::{DecoderResult, Encoding, UTF_8, UTF_16LE};
use super::{ContentSelection, LinesRange};
use crate::{
Error, IncludeSource,
preprocessor::{SourceLineOrigin, tag},
};
pub(super) const MAX_SELECTED_BYTES: usize = 10 * 1024 * 1024;
const MAX_TAG_METADATA_BYTES: usize = 10 * 1024 * 1024;
pub(super) struct Selected {
pub text: String,
pub origins: Vec<SourceLineOrigin>,
pub issues: Vec<tag::Issue>,
}
pub(super) fn read(
source: IncludeSource,
encoding: Option<&str>,
name: &str,
selection: &ContentSelection,
) -> Result<Selected, Error> {
let mut selected = Selection::new(selection, name);
if selected.stop_after != Some(0) {
decode(source, encoding, name, |text| selected.feed(text))?;
}
selected.finish()
}
fn bom_prefix(reader: &mut dyn Read) -> io::Result<Vec<u8>> {
let mut prefix = Vec::with_capacity(3);
let mut next = [0];
loop {
match reader.read(&mut next) {
Ok(0) => break,
Ok(_) => {
let [byte] = next;
prefix.push(byte);
}
Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
Err(error) => return Err(error),
}
if !matches!(prefix.as_slice(), [0xef | 0xff | 0xfe] | [0xef, 0xbb]) {
break;
}
}
Ok(prefix)
}
fn decode(
source: IncludeSource,
label: Option<&str>,
name: &str,
mut consume: impl FnMut(&str) -> Result<ControlFlow<()>, Error>,
) -> Result<(), Error> {
#[cfg(feature = "network")]
let read_limit = source.read_limit;
let mut reader = source.into_reader();
let prefix = bom_prefix(reader.as_mut())?;
let explicit = label.and_then(|label| Encoding::for_label(label.as_bytes()));
if explicit == Some(UTF_16LE)
&& label.is_some_and(|s| s.eq_ignore_ascii_case("utf-16"))
&& !prefix.starts_with(&[0xff, 0xfe])
&& !prefix.starts_with(&[0xfe, 0xff])
{
return Err(Error::UnrecognizedEncodingInFile(name.to_string()));
}
let (encoding, skip) = explicit.map_or_else(
|| Encoding::for_bom(&prefix).unwrap_or((UTF_8, 0)),
|encoding| (encoding, 0),
);
let mut decoder = encoding.new_decoder();
#[cfg(feature = "network")]
let mut total = skip;
let mut reader = Cursor::new(prefix.into_iter().skip(skip).collect::<Vec<_>>()).chain(reader);
let mut input = [0; 8192];
let mut output = [0; 8192];
loop {
#[cfg(not(feature = "network"))]
let buffer = input.as_mut_slice();
#[cfg(feature = "network")]
let buffer = {
let available = read_limit.map_or(input.len(), |limit| {
input.len().min(limit.saturating_sub(total).max(1))
});
input.get_mut(..available).unwrap_or_default()
};
let count = match reader.read(buffer) {
Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
result => result?,
};
#[cfg(feature = "network")]
{
total = total.saturating_add(count);
if read_limit.is_some_and(|limit| total > limit) {
return Err(Error::HttpRequest(
"remote include response exceeds the 10 MiB transfer limit".to_string(),
));
}
}
let last = count == 0;
let mut bytes = input.get(..count).unwrap_or_default();
loop {
let (result, consumed, written) =
decoder.decode_to_utf8_without_replacement(bytes, &mut output, last);
let text = std::str::from_utf8(output.get(..written).unwrap_or_default())
.map_err(|_| Error::UnrecognizedEncodingInFile(name.to_string()))?;
if consume(text)?.is_break() {
return Ok(());
}
if matches!(result, DecoderResult::Malformed(..)) {
return Err(Error::UnrecognizedEncodingInFile(name.to_string()));
}
bytes = bytes.get(consumed..).unwrap_or_default();
if result == DecoderResult::InputEmpty {
break;
}
}
if last {
return Ok(());
}
}
}
#[derive(Default)]
struct Candidates {
lines: BinaryHeap<Retained>,
bytes: usize,
first_discarded_requirement: Option<usize>,
}
#[derive(Eq, PartialEq, Ord, PartialOrd)]
struct Retained {
required_end: usize,
number: usize,
offset: usize,
text: String,
}
impl Candidates {
fn discard(&mut self, required: usize) {
self.first_discarded_requirement = Some(
self.first_discarded_requirement
.map_or(required, |old| old.min(required)),
);
}
fn push(
&mut self,
line: &Line,
length: usize,
required_end: usize,
number: usize,
offset: usize,
) {
if length > MAX_SELECTED_BYTES {
self.discard(required_end);
return;
}
let text = line.prefix.get(..length).unwrap_or_default().to_string();
self.bytes += text.len() + 1;
self.lines.push(Retained {
required_end,
number,
offset,
text,
});
while self.bytes > MAX_SELECTED_BYTES + 1 {
if let Some(line) = self.lines.pop() {
self.bytes -= line.text.len() + 1;
self.discard(line.required_end);
}
}
}
fn oversized(&self, total_lines: usize) -> bool {
self.first_discarded_requirement
.is_some_and(|required| required <= total_lines)
}
fn finish(self, total_lines: usize, name: &str) -> Result<Selected, Error> {
if self.oversized(total_lines) {
return Err(Error::IncludeSourceTooLarge(name.to_string()));
}
let mut lines = self.lines.into_vec();
lines.retain(|line| line.required_end <= total_lines);
lines.sort_unstable_by_key(|line| line.number);
let mut text = String::with_capacity(self.bytes.saturating_sub(1));
let mut origins = Vec::with_capacity(lines.len());
for line in lines {
if !origins.is_empty() {
text.push('\n');
}
text.push_str(&line.text);
origins.push(SourceLineOrigin {
line: line.number,
offset: line.offset,
column_shift: 0,
});
}
Ok(Selected {
text,
origins,
issues: Vec::new(),
})
}
}
struct Selection<'a> {
selector: &'a ContentSelection,
name: &'a str,
stop_after: Option<usize>,
tags: Option<tag::Selector<'a>>,
raw: Candidates,
trimmed: Candidates,
rebuild: bool,
line: Option<Line>,
pending: Option<Line>,
number: usize,
raw_offset: usize,
trimmed_offset: usize,
issues: Vec<tag::Issue>,
checked_issues: usize,
issue_bytes: usize,
}
impl<'a> Selection<'a> {
fn new(selection: &'a ContentSelection, name: &'a str) -> Self {
Self {
selector: selection,
name,
stop_after: Self::last_requested_line(selection),
tags: if let ContentSelection::Tags(filters) = selection {
Some(tag::Selector::new(filters))
} else {
None
},
raw: Candidates::default(),
trimmed: Candidates::default(),
rebuild: false,
line: None,
pending: None,
number: 0,
raw_offset: 0,
trimmed_offset: 0,
issues: Vec::new(),
checked_issues: 0,
issue_bytes: 0,
}
}
fn last_requested_line(selection: &ContentSelection) -> Option<usize> {
let ContentSelection::Lines(ranges) = selection else {
return None;
};
ranges.iter().try_fold(0, |last, range| {
let end = match *range {
LinesRange::Single(line) => line,
LinesRange::Range(0, _) => 0,
LinesRange::Range(_, end) if end < 0 => return None,
LinesRange::Range(start, end) => usize::try_from(end)
.ok()
.filter(|end| *end >= start)
.unwrap_or(0),
};
Some(last.max(end))
})
}
fn requirement(&self) -> Option<usize> {
let number = self.number + 1;
match self.selector {
ContentSelection::All => Some(number),
ContentSelection::Tags(_) => self
.tags
.as_ref()
.is_some_and(tag::Selector::selected)
.then_some(number),
ContentSelection::Lines(ranges) => ranges
.iter()
.filter_map(|range| match *range {
LinesRange::Single(line) => (line == number).then_some(number),
LinesRange::Range(start, end) if start > 0 && number >= start => {
if end < 0 {
Some(number)
} else {
usize::try_from(end).ok().filter(|end| *end >= number)
}
}
LinesRange::Range(_, _) => None,
})
.min(),
}
}
fn feed(&mut self, text: &str) -> Result<ControlFlow<()>, Error> {
for ch in text.chars() {
if self.line.is_none() {
if let Some(line) = self.pending.take() {
self.accept(&line)?;
}
self.line = Some(Line::new(self.requirement(), self.tags.is_some()));
}
if ch == '\n' {
if let Some(mut line) = self.line.take() {
line.remove_cr();
self.rebuild |= line.needs_rebuild();
if matches!(self.selector, ContentSelection::Lines(_)) {
self.accept(&line)?;
if self.stop_after == Some(self.number) {
return Ok(ControlFlow::Break(()));
}
} else {
self.pending = Some(line);
}
}
} else if let Some(line) = &mut self.line {
line.push(ch);
if line.trimmed_len > MAX_SELECTED_BYTES
&& line.required_end.is_some_and(|end| end <= self.number + 1)
&& self.tags.is_none()
{
return Err(Error::IncludeSourceTooLarge(self.name.to_string()));
}
}
}
Ok(ControlFlow::Continue(()))
}
fn accept(&mut self, line: &Line) -> Result<(), Error> {
let required = self.requirement();
self.rebuild |= line.crlf;
self.number += 1;
let marker = line.marker(self.name)?;
if let (Some(tags), Some((directive, name))) = (&mut self.tags, marker) {
tags.marker(directive, name, self.number, |issue| {
self.issues.push(issue);
});
if tags.stack_bytes() > MAX_TAG_METADATA_BYTES {
return Err(self.metadata_error());
}
self.check_issues()?;
} else if let Some(required) = required {
if !matches!(self.selector, ContentSelection::Lines(_)) {
self.raw
.push(line, line.len, required, self.number, self.raw_offset);
}
self.trimmed.push(
line,
line.trimmed_len,
required,
self.number,
self.trimmed_offset,
);
if self.trimmed.oversized(self.number) {
return Err(Error::IncludeSourceTooLarge(self.name.to_string()));
}
}
self.raw_offset = self.raw_offset.saturating_add(line.len).saturating_add(1);
self.trimmed_offset = self
.trimmed_offset
.saturating_add(line.trimmed_len)
.saturating_add(1);
Ok(())
}
fn metadata_error(&self) -> Error {
Error::IncludeSelectionTooComplex(self.name.to_string())
}
fn check_issues(&mut self) -> Result<(), Error> {
self.issue_bytes += self.issues.iter().skip(self.checked_issues).map(|issue| match issue {
tag::Issue::UnexpectedEnd { name, .. } | tag::Issue::Unclosed { name, .. } => name.len(),
tag::Issue::MismatchedEnd { expected, found, .. } => expected.len() + found.len(),
tag::Issue::Missing { names } => names.iter().map(String::len).sum(),
} + std::mem::size_of::<tag::Issue>()).sum::<usize>();
self.checked_issues = self.issues.len();
if self.issue_bytes + self.tags.as_ref().map_or(0, tag::Selector::stack_bytes)
> MAX_TAG_METADATA_BYTES
{
return Err(self.metadata_error());
}
Ok(())
}
fn finish(mut self) -> Result<Selected, Error> {
if let Some(line) = self.line.take() {
self.rebuild |= line.needs_rebuild();
self.pending = Some(line);
}
if let Some(mut line) = self.pending.take() {
line.crlf = false;
if matches!(self.selector, ContentSelection::Lines(_))
|| if self.rebuild {
line.trimmed_len > 0
} else {
line.len > 0
}
{
self.accept(&line)?;
}
}
if let Some(tags) = self.tags.take() {
tags.finish(|issue| self.issues.push(issue));
self.check_issues()?;
}
let candidates = if self.rebuild || matches!(self.selector, ContentSelection::Lines(_)) {
self.trimmed
} else {
self.raw
};
let mut selected = candidates.finish(self.number, self.name)?;
selected.issues = self.issues;
Ok(selected)
}
}
struct Line {
prefix: String,
required_end: Option<usize>,
len: usize,
trimmed_len: usize,
last: Option<char>,
previous: Option<char>,
carriage_returns: usize,
crlf: bool,
probes: Option<[Probe; 2]>,
}
impl Line {
fn new(required_end: Option<usize>, tags: bool) -> Self {
Self {
prefix: String::new(),
required_end,
len: 0,
trimmed_len: 0,
last: None,
previous: None,
carriage_returns: 0,
crlf: false,
probes: tags.then(|| [Probe::new("tag::"), Probe::new("end::")]),
}
}
fn push(&mut self, ch: char) {
self.len = self.len.saturating_add(ch.len_utf8());
if !ch.is_whitespace() {
self.trimmed_len = self.len;
}
if self.required_end.is_some() && self.len <= MAX_SELECTED_BYTES {
self.prefix.push(ch);
}
self.previous = self.last.replace(ch);
self.carriage_returns += usize::from(ch == '\r');
if let Some(probes) = &mut self.probes {
for probe in probes {
probe.push(ch);
}
}
}
fn remove_cr(&mut self) {
if self.last == Some('\r') {
if self.prefix.len() == self.len {
self.prefix.pop();
}
self.len -= 1;
self.carriage_returns -= 1;
self.last = self.previous;
self.crlf = true;
}
}
fn needs_rebuild(&self) -> bool {
self.carriage_returns > 0 || matches!(self.last, Some(' ' | '\t'))
}
fn marker(&self, source: &str) -> Result<Option<(&'static str, &str)>, Error> {
if let Some(probes) = &self.probes {
for probe in probes {
if matches!(probe.state, ProbeState::Done(true)) {
if probe.overflow {
return Err(Error::IncludeSelectionTooComplex(source.to_string()));
}
return Ok(Some((
if probe.keyword == "tag::" {
"tag"
} else {
"end"
},
&probe.name,
)));
}
}
}
Ok(None)
}
}
struct Probe {
keyword: &'static str,
matched: usize,
previous: Option<char>,
word_before: bool,
state: ProbeState,
overflow: bool,
name: String,
}
enum ProbeState {
Searching,
Name,
Bracket,
Done(bool),
}
impl Probe {
fn new(keyword: &'static str) -> Self {
Self {
keyword,
matched: 0,
previous: None,
word_before: false,
state: ProbeState::Searching,
overflow: false,
name: String::new(),
}
}
fn push(&mut self, ch: char) {
let previous = self.previous.replace(ch);
match self.state {
ProbeState::Done(_) => (),
ProbeState::Searching => {
let expected = self
.keyword
.as_bytes()
.get(self.matched)
.copied()
.map(char::from);
if expected == Some(ch) {
if self.matched == 0 {
self.word_before =
previous.is_some_and(|c| c.is_alphanumeric() || c == '_');
}
self.matched += 1;
if self.matched == self.keyword.len() {
self.state = if self.word_before {
ProbeState::Done(false)
} else {
ProbeState::Name
};
}
} else {
self.matched = usize::from(self.keyword.starts_with(ch));
self.word_before = previous.is_some_and(|c| c.is_alphanumeric() || c == '_');
}
}
ProbeState::Bracket => {
self.state =
ProbeState::Done(ch == ']' && (!self.name.is_empty() || self.overflow));
}
ProbeState::Name => {
if ch == '[' {
self.state = ProbeState::Bracket;
} else if ch == ']' || ch.is_whitespace() {
self.state = ProbeState::Done(false);
} else if self.name.len() + ch.len_utf8() <= MAX_TAG_METADATA_BYTES {
self.name.push(ch);
} else {
self.overflow = true;
}
}
}
}
}
#[cfg(test)]
mod tests;