Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/uu/wc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ readme.workspace = true

[lib]
path = "src/wc.rs"
test = false
doctest = false

[dependencies]
Expand Down
5 changes: 5 additions & 0 deletions src/uu/wc/locales/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ wc-error-cannot-open-for-reading = cannot open { $path } for reading
wc-error-read-error = { $path }: read error
wc-error-failed-to-print-result = failed to print result for { $title }
wc-error-failed-to-print-total = failed to print total
wc-error-overflow = Value too large for defined data type
wc-error-total-bytes-overflow = total bytes: { wc-error-overflow }
wc-error-total-chars-overflow = total characters: { wc-error-overflow }
wc-error-total-lines-overflow = total lines: { wc-error-overflow }
wc-error-total-words-overflow = total words: { wc-error-overflow }

# Decoder error messages
decoder-error-invalid-byte-sequence = invalid byte sequence: { $bytes }
Expand Down
26 changes: 13 additions & 13 deletions src/uu/wc/src/count_fast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,23 @@ const BUF_SIZE: usize = 64 * 1024;
/// caller will fall back to a simpler method.
#[inline]
#[cfg(any(target_os = "linux", target_os = "android"))]
fn count_bytes_using_splice(fd: &impl rustix::fd::AsFd) -> Result<usize, usize> {
fn count_bytes_using_splice(fd: &impl rustix::fd::AsFd) -> Result<u64, u64> {
use uucore::pipes::{MAX_ROOTLESS_PIPE_SIZE, pipe, splice};
let null_file = uucore::pipes::dev_null().ok_or(0_usize)?;
let null_file = uucore::pipes::dev_null().ok_or(0_u64)?;
let mut byte_count = 0;
// no need to increase pipe size of input fd since
// - sender with splice probably increased size already
// - sender without splice is bottleneck of our wc -c
loop {
match splice(fd, &null_file, MAX_ROOTLESS_PIPE_SIZE) {
Ok(0) => return Ok(byte_count),
Ok(res) => byte_count += res,
Ok(res) => byte_count += res as u64,
Err(_) => break, // input is not pipe. needs additional pipe...
}
}
let (pipe_rd, pipe_wr) = pipe::<false>().map_err(|_| byte_count)?;
while let s @ 1.. = splice(fd, &pipe_wr, MAX_ROOTLESS_PIPE_SIZE).map_err(|_| byte_count)? {
byte_count += s;
byte_count += s as u64;
// pipe to null is not blocked. So this returns the same length at most cases
// next splice does not hang if we discarded 1+ pages
splice(&pipe_rd, &null_file, s).map_err(|_| byte_count)?;
Expand All @@ -61,7 +61,7 @@ fn count_bytes_using_splice(fd: &impl rustix::fd::AsFd) -> Result<usize, usize>
/// 3. Otherwise, we just read normally, but without the overhead of counting
/// other things such as lines and words.
#[inline]
pub(crate) fn count_bytes_fast<T: WordCountable>(handle: &mut T) -> (usize, Option<io::Error>) {
pub(crate) fn count_bytes_fast<T: WordCountable>(handle: &mut T) -> (u64, Option<io::Error>) {
let mut byte_count = 0;

#[cfg(unix)]
Expand Down Expand Up @@ -109,10 +109,10 @@ pub(crate) fn count_bytes_fast<T: WordCountable>(handle: &mut T) -> (usize, Opti
&& stat.st_size > 0
{
let sys_page_size = rustix::param::page_size();
if !(stat.st_size as usize).is_multiple_of(sys_page_size) {
if !(stat.st_size as u64).is_multiple_of(sys_page_size as u64) {
// regular file or file from /proc, /sys and similar pseudo-filesystems
// with size that is NOT a multiple of system page size
return (stat.st_size as usize, None);
return (stat.st_size as u64, None);
} else if let Some(file) = handle.inner_file() {
// On some platforms `stat.st_blksize` and `stat.st_size`
// are of different types: i64 vs i32
Expand All @@ -125,7 +125,7 @@ pub(crate) fn count_bytes_fast<T: WordCountable>(handle: &mut T) -> (usize, Opti
stat.st_size as i64 - stat.st_size as i64 % (stat.st_blksize as i64 + 1);

if let Ok(n) = file.seek(SeekFrom::Start(offset as u64)) {
byte_count = n as usize;
byte_count = n;
}
}
}
Expand All @@ -143,7 +143,7 @@ pub(crate) fn count_bytes_fast<T: WordCountable>(handle: &mut T) -> (usize, Opti
{
let attributes = metadata.file_attributes();
if (attributes & FILE_ATTRIBUTE_ARCHIVE) != 0 || (attributes & FILE_ATTRIBUTE_NORMAL) != 0 {
return (metadata.file_size() as usize, None);
return (metadata.file_size(), None);
}
}

Expand All @@ -153,7 +153,7 @@ pub(crate) fn count_bytes_fast<T: WordCountable>(handle: &mut T) -> (usize, Opti
loop {
match handle.read(&mut buf) {
Ok(0) => return (byte_count, None),
Ok(n) => byte_count += n,
Ok(n) => byte_count += n as u64,
Err(e) if e.kind() == ErrorKind::Interrupted => (),
Err(e) => return (byte_count, Some(e)),
}
Expand Down Expand Up @@ -201,21 +201,21 @@ pub(crate) fn count_bytes_chars_and_lines_fast<
Ok(0) => return (total, None),
Ok(n) => {
if COUNT_BYTES {
total.bytes += n;
total.bytes += n as u64;
}
if COUNT_CHARS {
total.chars += if simd_allowed {
bytecount::num_chars(&buf[..n])
} else {
bytecount::naive_num_chars(&buf[..n])
};
} as u64;
}
if COUNT_LINES {
total.lines += if simd_allowed {
bytecount::count(&buf[..n], b'\n')
} else {
bytecount::naive_count(&buf[..n], b'\n')
};
} as u64;
}
}
Err(e) if e.kind() == ErrorKind::Interrupted => (),
Expand Down
44 changes: 36 additions & 8 deletions src/uu/wc/src/wc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ use uucore::{
hardware::{HardwareFeature, HasHardwareFeatures as _, SimdPolicy},
parser::shortcut_value_parser::ShortcutValueParser,
quoting_style::{self, QuotingStyle},
show,
show, show_error,
};

use crate::{
count_fast::{count_bytes_chars_and_lines_fast, count_bytes_fast},
countable::WordCountable,
word_count::WordCount,
word_count::{CountOverflow, WordCount},
};

/// The minimum character width for formatting counts when reading from stdin.
Expand Down Expand Up @@ -575,7 +575,7 @@ fn process_chunk<
>(
total: &mut WordCount,
text: &str,
current_len: &mut usize,
current_len: &mut u64,
in_word: &mut bool,
is_posixly_correct: bool,
) {
Expand Down Expand Up @@ -606,7 +606,7 @@ fn process_chunk<
*current_len += 8;
}
_ => {
*current_len += ch.width().unwrap_or(0);
*current_len += ch.width().unwrap_or(0) as u64;
}
}
}
Expand All @@ -617,7 +617,7 @@ fn process_chunk<
total.chars += 1;
}
}
total.bytes += text.len();
total.bytes += text.len() as u64;

total.max_line_length = max(*current_len, total.max_line_length);
}
Expand All @@ -629,7 +629,7 @@ fn handle_error(
) -> Option<io::Error> {
match error {
BufReadDecoderError::InvalidByteSequence(bytes) => {
total.bytes += bytes.len();
total.bytes += bytes.len() as u64;
if !(*in_word) {
*in_word = true;
total.words += 1;
Expand Down Expand Up @@ -739,7 +739,7 @@ fn compute_number_width(inputs: &Inputs, settings: &Settings) -> usize {
Input::Path(path) => {
if let Ok(meta) = fs::metadata(path) {
if meta.is_file() {
total += meta.len();
total = total.saturating_add(meta.len());
} else {
minimum_width = MINIMUM_WIDTH;
}
Expand Down Expand Up @@ -895,6 +895,7 @@ pub(crate) fn wc_simd_allowed(policy: &SimdPolicy) -> bool {

fn wc(inputs: &Inputs, settings: &Settings) -> UResult<()> {
let mut total_word_count = WordCount::default();
let mut total_overflow = CountOverflow::default();
let mut num_inputs: usize = 0;

let (number_width, are_stats_visible) = match settings.total_when {
Expand Down Expand Up @@ -973,7 +974,7 @@ fn wc(inputs: &Inputs, settings: &Settings) -> UResult<()> {
continue;
}
};
total_word_count += word_count;
total_word_count.accumulate(word_count, &mut total_overflow);
if are_stats_visible {
let maybe_title = input.to_title();
let maybe_title_str = maybe_title.as_deref();
Expand All @@ -991,6 +992,33 @@ fn wc(inputs: &Inputs, settings: &Settings) -> UResult<()> {
}

if settings.total_when.is_total_row_visible(num_inputs) {
for (overflowed, enabled, message) in [
(
total_overflow.lines,
settings.show_lines,
"wc-error-total-lines-overflow",
),
(
total_overflow.words,
settings.show_words,
"wc-error-total-words-overflow",
),
(
total_overflow.chars,
settings.show_chars,
"wc-error-total-chars-overflow",
),
(
total_overflow.bytes,
settings.show_bytes,
"wc-error-total-bytes-overflow",
),
] {
if enabled && overflowed {
show_error!("{}", translate!(message));
uucore::error::set_exit_code(1);
}
}
let wc_total_msg = translate!("wc-total");
let title = are_stats_visible.then_some(OsStr::new(&wc_total_msg));
if let Err(err) = print_stats(settings, &total_word_count, title, number_width) {
Expand Down
138 changes: 120 additions & 18 deletions src/uu/wc/src/word_count.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,135 @@
// file that was distributed with this source code.

use std::cmp::max;
use std::ops::{Add, AddAssign};

#[derive(Debug, Default, Copy, Clone)]
pub struct WordCount {
pub bytes: usize,
pub chars: usize,
pub lines: usize,
pub words: usize,
pub max_line_length: usize,
pub bytes: u64,
pub chars: u64,
pub lines: u64,
pub words: u64,
pub max_line_length: u64,
}

impl Add for WordCount {
type Output = Self;
#[derive(Debug, Default, PartialEq, Eq)]
pub struct CountOverflow {
pub bytes: bool,
pub chars: bool,
pub lines: bool,
pub words: bool,
}

fn add(self, other: Self) -> Self {
Self {
bytes: self.bytes + other.bytes,
chars: self.chars + other.chars,
lines: self.lines + other.lines,
words: self.words + other.words,
max_line_length: max(self.max_line_length, other.max_line_length),
impl WordCount {
/// Accumulate counts, saturating each independently and retaining overflow flags.
pub fn accumulate(&mut self, other: Self, overflow: &mut CountOverflow) {
for (total, count, overflowed) in [
(&mut self.bytes, other.bytes, &mut overflow.bytes),
(&mut self.chars, other.chars, &mut overflow.chars),
(&mut self.lines, other.lines, &mut overflow.lines),
(&mut self.words, other.words, &mut overflow.words),
] {
if let Some(sum) = total.checked_add(count) {
*total = sum;
} else {
*total = u64::MAX;
*overflowed = true;
}
}
self.max_line_length = max(self.max_line_length, other.max_line_length);
}
}

impl AddAssign for WordCount {
fn add_assign(&mut self, other: Self) {
*self = *self + other;
#[cfg(test)]
mod tests {
use super::{CountOverflow, WordCount};

#[test]
fn accumulate_saturates_each_count_independently() {
let mut total = WordCount {
bytes: u64::MAX - 1,
chars: u64::MAX - 2,
lines: u64::MAX - 3,
words: u64::MAX - 4,
max_line_length: 20,
};
let mut overflow = CountOverflow::default();
total.accumulate(
WordCount {
bytes: 1,
chars: 2,
lines: 3,
words: 4,
max_line_length: 10,
},
&mut overflow,
);
assert_eq!(overflow, CountOverflow::default());
total.accumulate(
WordCount {
bytes: 1,
..WordCount::default()
},
&mut overflow,
);
assert_eq!(
overflow,
CountOverflow {
bytes: true,
..CountOverflow::default()
}
);
total.accumulate(
WordCount {
chars: 1,
lines: 1,
words: 1,
..WordCount::default()
},
&mut overflow,
);
assert_eq!(
overflow,
CountOverflow {
bytes: true,
chars: true,
lines: true,
words: true
}
);
assert_eq!(
(total.bytes, total.chars, total.lines, total.words),
(u64::MAX, u64::MAX, u64::MAX, u64::MAX)
);
assert_eq!(total.max_line_length, 20);
}

#[test]
fn accumulate_keeps_other_counts_after_overflow() {
let mut total = WordCount {
bytes: u64::MAX,
..WordCount::default()
};
let mut overflow = CountOverflow::default();
total.accumulate(
WordCount {
bytes: 1,
chars: 3,
lines: 2,
words: 1,
max_line_length: 5,
},
&mut overflow,
);
assert_eq!(
overflow,
CountOverflow {
bytes: true,
..CountOverflow::default()
}
);
assert_eq!(
(total.chars, total.lines, total.words, total.max_line_length),
(3, 2, 1, 5)
);
}
}
Loading
Loading