Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent out-of-memory crashes using line length limits #2902

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- `bat --squeeze-limit` to set the maximum number of empty consecutive when using `--squeeze-blank`, see #1441 (@eth-p) and #2665 (@einfachIrgendwer0815)
- `PrettyPrinter::squeeze_empty_lines` to support line squeezing for bat as a library, see #1441 (@eth-p) and #2665 (@einfachIrgendwer0815)
- Syntax highlighting for JavaScript files that start with `#!/usr/bin/env bun` #2913 (@sharunkumar)
- Add line length soft/hard limits to prevent out-of-memory events, see issue #636 and PR #2902 (@einfachIrgendwer0815)

## Bugfixes

Expand Down
11 changes: 11 additions & 0 deletions doc/long-help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,17 @@ Options:
--squeeze-limit <squeeze-limit>
Set the maximum number of consecutive empty lines to be printed.

--disable-line-limits
Disables all line limits. Short for `--soft-line-limit 0 --hard-line-limit 0`.

--soft-line-limit <BYTES>
Line length (in bytes) at which the line will be ignored. Zero disables this limit.
Default: 64 kB

--hard-line-limit <BYTES>
Line length (in bytes) at which bat will abort. Zero disables this limit.
Default: 256 kB

--style <components>
Configure which elements (line numbers, file headers, grid borders, Git modifications, ..)
to display in addition to the file contents. The argument is a comma-separated list of
Expand Down
2 changes: 2 additions & 0 deletions doc/short-help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ Options:
Display all supported highlighting themes.
-s, --squeeze-blank
Squeeze consecutive empty lines.
--disable-line-limits
Disables all line limits.
--style <components>
Comma-separated list of style elements to display (*default*, auto, full, plain, changes,
header, header-filename, header-filesize, grid, rule, numbers, snip).
Expand Down
8 changes: 4 additions & 4 deletions src/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,7 @@ mod tests {

let input = Input::ordinary_file(&file_path);
let dummy_stdin: &[u8] = &[];
let mut opened_input = input.open(dummy_stdin, None).unwrap();
let mut opened_input = input.open(dummy_stdin, None, None, None).unwrap();

self.get_syntax_name(None, &mut opened_input, &self.syntax_mapping)
}
Expand All @@ -481,7 +481,7 @@ mod tests {
let input = Input::from_reader(Box::new(BufReader::new(first_line.as_bytes())))
.with_name(Some(&file_path));
let dummy_stdin: &[u8] = &[];
let mut opened_input = input.open(dummy_stdin, None).unwrap();
let mut opened_input = input.open(dummy_stdin, None, None, None).unwrap();

self.get_syntax_name(None, &mut opened_input, &self.syntax_mapping)
}
Expand All @@ -501,7 +501,7 @@ mod tests {

fn syntax_for_stdin_with_content(&self, file_name: &str, content: &[u8]) -> String {
let input = Input::stdin().with_name(Some(file_name));
let mut opened_input = input.open(content, None).unwrap();
let mut opened_input = input.open(content, None, None, None).unwrap();

self.get_syntax_name(None, &mut opened_input, &self.syntax_mapping)
}
Expand Down Expand Up @@ -698,7 +698,7 @@ mod tests {

let input = Input::ordinary_file(&file_path_symlink);
let dummy_stdin: &[u8] = &[];
let mut opened_input = input.open(dummy_stdin, None).unwrap();
let mut opened_input = input.open(dummy_stdin, None, None, None).unwrap();

assert_eq!(
test.get_syntax_name(None, &mut opened_input, &test.syntax_mapping),
Expand Down
12 changes: 12 additions & 0 deletions src/bin/bat/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,18 @@ impl App {
} else {
None
},
soft_line_limit: self
.matches
.get_one::<usize>("soft-line-limit")
.copied()
.filter(|l| l != &0)
.filter(|_| !self.matches.get_flag("disable-line-limits")),
hard_line_limit: self
.matches
.get_one::<usize>("hard-line-limit")
.copied()
.filter(|l| l != &0)
.filter(|_| !self.matches.get_flag("disable-line-limits")),
})
}

Expand Down
38 changes: 38 additions & 0 deletions src/bin/bat/clap_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,44 @@ pub fn build_app(interactive_output: bool) -> Command {
.long_help("Set the maximum number of consecutive empty lines to be printed.")
.hide_short_help(true)
)
.arg(
Arg::new("disable-line-limits")
.long("disable-line-limits")
.action(ArgAction::SetTrue)
.overrides_with_all(["soft-line-limit", "hard-line-limit"])
.help("Disables all line limits.")
.long_help("Disables all line limits. Short for `--soft-line-limit 0 --hard-line-limit 0`.")
)
.arg(
Arg::new("soft-line-limit")
.long("soft-line-limit")
.value_name("BYTES")
.value_parser(|s: &str| s.parse::<usize>())
.default_value("65536")
.overrides_with("disable-line-limits")
.long_help(
"Line length (in bytes) at which the line will be ignored. \
Zero disables this limit.\n\
Default: 64 kB",
)
.hide_short_help(true)
.hide_default_value(true)
)
.arg(
Arg::new("hard-line-limit")
.long("hard-line-limit")
.value_name("BYTES")
.value_parser(|s: &str| s.parse::<usize>())
.default_value("262144")
.overrides_with("disable-line-limits")
.long_help(
"Line length (in bytes) at which bat will abort. \
Zero disables this limit.\n\
Default: 256 kB"
)
.hide_short_help(true)
.hide_default_value(true)
)
.arg(
Arg::new("style")
.long("style")
Expand Down
6 changes: 6 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ pub struct Config<'a> {

/// The maximum number of consecutive empty lines to display
pub squeeze_lines: Option<usize>,

/// Line length (in bytes) at which a line of input will be ignored
pub soft_line_limit: Option<usize>,

/// Line length (in bytes) at which an error will be thrown
pub hard_line_limit: Option<usize>,
}

#[cfg(all(feature = "minimal-application", feature = "paging"))]
Expand Down
43 changes: 34 additions & 9 deletions src/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::config::{Config, VisibleLines};
#[cfg(feature = "git")]
use crate::diff::{get_git_diff, LineChanges};
use crate::error::*;
use crate::input::{Input, InputReader, OpenedInput};
use crate::input::{Input, InputReader, OpenedInput, ReaderError};
#[cfg(feature = "lessopen")]
use crate::lessopen::LessOpenPreprocessor;
#[cfg(feature = "git")]
Expand Down Expand Up @@ -142,7 +142,12 @@ impl<'b> Controller<'b> {
}

#[cfg(not(feature = "lessopen"))]
input.open(stdin, stdout_identifier)?
input.open(
stdin,
stdout_identifier,
self.config.soft_line_limit,
self.config.hard_line_limit,
)?
};
#[cfg(feature = "git")]
let line_changes = if self.config.visible_lines.diff_mode()
Expand Down Expand Up @@ -249,13 +254,30 @@ impl<'b> Controller<'b> {

let style_snip = self.config.style_components.snip();

while reader.read_line(&mut line_buffer)? {
loop {
let mut soft_limit_hit = false;
let read_result = reader.read_line(&mut line_buffer);
match read_result {
Ok(res) => {
if !res {
break;
}
}
Err(err) => match err {
ReaderError::IoError(io_err) => return Err(io_err.into()),
ReaderError::SoftLimitHit => soft_limit_hit = true,
ReaderError::HardLimitHit => return Err(Error::LineTooLong(line_number)),
},
};

match line_ranges.check(line_number) {
RangeCheckResult::BeforeOrBetweenRanges => {
// Call the printer in case we need to call the syntax highlighter
// for this line. However, set `out_of_range` to `true`.
printer.print_line(true, writer, line_number, &line_buffer)?;
mid_range = false;
if !soft_limit_hit {
// Call the printer in case we need to call the syntax highlighter
// for this line. However, set `out_of_range` to `true`.
printer.print_line(true, writer, line_number, &line_buffer)?;
mid_range = false;
}
}

RangeCheckResult::InRange => {
Expand All @@ -268,8 +290,11 @@ impl<'b> Controller<'b> {
printer.print_snip(writer)?;
}
}

printer.print_line(false, writer, line_number, &line_buffer)?;
if soft_limit_hit {
printer.print_replaced_line(writer, line_number, "<line too long>")?;
} else {
printer.print_line(false, writer, line_number, &line_buffer)?;
}
}
RangeCheckResult::AfterLastRange => {
break;
Expand Down
30 changes: 30 additions & 0 deletions src/decorations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,33 @@ impl Decoration for GridBorderDecoration {
self.cached.width
}
}

pub(crate) struct PlaceholderDecoration {
cached: DecorationText,
}

impl PlaceholderDecoration {
pub(crate) fn new(length: usize) -> Self {
Self {
cached: DecorationText {
text: " ".repeat(length),
width: length,
},
}
}
}

impl Decoration for PlaceholderDecoration {
fn generate(
&self,
_line_number: usize,
_continuation: bool,
_printer: &InteractivePrinter,
) -> DecorationText {
self.cached.clone()
}

fn width(&self) -> usize {
self.cached.width
}
}
2 changes: 2 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ pub enum Error {
InvalidPagerValueBat,
#[error("{0}")]
Msg(String),
#[error("Line {0} is too long")]
LineTooLong(usize),
#[cfg(feature = "lessopen")]
#[error(transparent)]
VarError(#[from] ::std::env::VarError),
Expand Down