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
129 changes: 120 additions & 9 deletions src/uu/sort/src/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,88 @@ use crate::{
compare_by, open,
};
use itertools::Itertools;
use std::{cmp::Ordering, ffi::OsStr};
#[cfg(not(target_os = "wasi"))]
use std::{
cmp::Ordering,
ffi::OsStr,
io::Read,
iter,
sync::mpsc::{Receiver, SyncSender, sync_channel},
thread,
};
use uucore::error::UResult;

fn buffer_size(settings: &GlobalSettings) -> usize {
if settings.buffer_size < 100 * 1024 {
// when the buffer size is smaller than 100KiB we choose it instead of the default.
// this improves testability.
settings.buffer_size
} else {
100 * 1024
}
}

/// Given the chunks of a file (in order), find the first pair of adjacent
/// lines that violates the requested ordering and report it as a
/// [`SortError::Disorder`].
#[cfg(target_os = "wasi")]
fn check_chunks(
path: &OsStr,
settings: &GlobalSettings,
max_allowed_cmp: Ordering,
chunks: impl Iterator<Item = Chunk>,
) -> UResult<()> {
let mut prev_chunk: Option<Chunk> = None;
let mut line_idx = 0;
for chunk in chunks {
line_idx += 1;
if let Some(prev_chunk) = &prev_chunk {
// Check if the first element of the new chunk is greater than the last
// element from the previous chunk
let prev_last = prev_chunk.lines().last().unwrap();
let new_first = chunk.lines().first().unwrap();

if compare_by(
prev_last,
new_first,
settings,
prev_chunk.line_data(),
chunk.line_data(),
) > max_allowed_cmp
{
return Err(SortError::Disorder {
file: path.to_owned(),
line_number: line_idx,
line: String::from_utf8_lossy(new_first.line).into_owned(),
silent: settings.check_silent,
}
.into());
}
}

for (a, b) in chunk.lines().iter().tuple_windows() {
line_idx += 1;
if compare_by(a, b, settings, chunk.line_data(), chunk.line_data()) > max_allowed_cmp {
return Err(SortError::Disorder {
file: path.to_owned(),
line_number: line_idx,
line: String::from_utf8_lossy(b.line).into_owned(),
silent: settings.check_silent,
}
.into());
}
}

prev_chunk = Some(chunk);
}
Ok(())
}

/// Check if the file at `path` is ordered.
///
/// # Returns
///
/// The code we should exit with.
#[cfg(not(target_os = "wasi"))]
pub fn check(path: &OsStr, settings: &GlobalSettings) -> UResult<()> {
let max_allowed_cmp = if settings.unique {
// If `unique` is enabled, the previous line must compare _less_ to the next one.
Expand All @@ -42,13 +109,7 @@ pub fn check(path: &OsStr, settings: &GlobalSettings) -> UResult<()> {
move || reader(file, &recycled_receiver, &loaded_sender, &settings)
});
for _ in 0..2 {
let _ = recycled_sender.send(RecycledChunk::new(if settings.buffer_size < 100 * 1024 {
// when the buffer size is smaller than 100KiB we choose it instead of the default.
// this improves testability.
settings.buffer_size
} else {
100 * 1024
}));
let _ = recycled_sender.send(RecycledChunk::new(buffer_size(settings)));
}

let mut prev_chunk: Option<Chunk> = None;
Expand Down Expand Up @@ -114,7 +175,27 @@ pub fn check(path: &OsStr, settings: &GlobalSettings) -> UResult<()> {
result
}

/// Check if the file at `path` is ordered.
///
/// WASI has no thread support, so this reads every chunk up front on the
/// current thread instead of streaming them from a background reader thread.
///
/// # Returns
///
/// The code we should exit with.
#[cfg(target_os = "wasi")]
pub fn check(path: &OsStr, settings: &GlobalSettings) -> UResult<()> {
let max_allowed_cmp = if settings.unique {
Ordering::Less
} else {
Ordering::Equal
};
let chunks = read_all_chunks(path, settings)?;
check_chunks(path, settings, max_allowed_cmp, chunks.into_iter())
}

/// The function running on the reader thread.
#[cfg(not(target_os = "wasi"))]
fn reader(
mut file: Box<dyn Read + Send>,
receiver: &Receiver<RecycledChunk>,
Expand All @@ -139,3 +220,33 @@ fn reader(
}
Ok(())
}

/// Read every chunk of `path` up front, without any recycling or background
/// thread. Used on WASI, which has no thread support.
#[cfg(target_os = "wasi")]
fn read_all_chunks(path: &OsStr, settings: &GlobalSettings) -> UResult<Vec<Chunk>> {
let mut file = open(path)?;
let mut carry_over = vec![];
let mut chunks = Vec::new();
let (sender, receiver) = std::sync::mpsc::sync_channel(1);
loop {
let recycled = RecycledChunk::new(buffer_size(settings));
let should_continue = chunks::read(
&sender,
recycled,
None,
&mut carry_over,
&mut file,
&mut std::iter::empty(),
settings.line_ending.into(),
settings,
)?;
while let Ok(chunk) = receiver.try_recv() {
chunks.push(chunk);
}
if !should_continue {
break;
}
}
Ok(chunks)
}
7 changes: 7 additions & 0 deletions src/uu/sort/src/ext_sort/wasi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ pub fn ext_sort(
// moderately sized inputs; very large files may cause OOM.
let mut input = Vec::new();
for file in files {
// Insert the separator between files whose preceding content doesn't
// already end with one; otherwise the last line of one file would
// merge with the first line of the next, e.g. "a\nb" + "b" ->
// "a\nbb" instead of "a\nb" + '\n' + "b".
if !input.is_empty() && input.last() != Some(&separator) {
input.push(separator);
}
file?.read_to_end(&mut input)?;
}
if input.is_empty() {
Expand Down
Loading
Loading