mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-29 12:07:46 +00:00
Merge branch 'main' into cp-symbolic-link-loop
This commit is contained in:
commit
349320ae61
19 changed files with 1631 additions and 1183 deletions
52
.github/workflows/GnuComment.yml
vendored
Normal file
52
.github/workflows/GnuComment.yml
vendored
Normal file
|
@ -0,0 +1,52 @@
|
|||
name: GnuComment
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["GnuTests"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
post-comment:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.event.workflow_run.event == 'pull_request'
|
||||
steps:
|
||||
- name: 'Download artifact'
|
||||
uses: actions/github-script@v3.1.0
|
||||
with:
|
||||
script: |
|
||||
// List all artifacts from GnuTests
|
||||
var artifacts = await github.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: ${{ github.event.workflow_run.id }},
|
||||
});
|
||||
|
||||
// Download the "comment" artifact, which contains a PR number (NR) and result.txt
|
||||
var matchArtifact = artifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name == "comment"
|
||||
})[0];
|
||||
var download = await github.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: matchArtifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
var fs = require('fs');
|
||||
fs.writeFileSync('${{ github.workspace }}/comment.zip', Buffer.from(download.data));
|
||||
- run: unzip comment.zip
|
||||
|
||||
- name: 'Comment on PR'
|
||||
uses: actions/github-script@v3
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
var fs = require('fs');
|
||||
var issue_number = Number(fs.readFileSync('./NR'));
|
||||
await github.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue_number,
|
||||
body: String(fs.readFileSync('./result.txt')),
|
||||
});
|
34
.github/workflows/GnuTests.yml
vendored
34
.github/workflows/GnuTests.yml
vendored
|
@ -14,7 +14,8 @@ jobs:
|
|||
permissions:
|
||||
actions: read # for dawidd6/action-download-artifact to query and download artifacts
|
||||
contents: read # for actions/checkout to fetch code
|
||||
pull-requests: read # for dawidd6/action-download-artifact to query commit hash
|
||||
issues: write # to publish comment
|
||||
pull-requests: write # for dawidd6/action-download-artifact to query commit hash & publish comment
|
||||
name: Run GNU tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
@ -186,6 +187,15 @@ jobs:
|
|||
REF_LOG_FILE='${{ steps.vars.outputs.path_reference }}/test-logs/test-suite.log'
|
||||
REF_SUMMARY_FILE='${{ steps.vars.outputs.path_reference }}/test-summary/gnu-result.json'
|
||||
REPO_DEFAULT_BRANCH='${{ steps.vars.outputs.repo_default_branch }}'
|
||||
|
||||
mkdir -p ${{ steps.vars.outputs.path_reference }}
|
||||
|
||||
COMMENT_DIR="${{ steps.vars.outputs.path_reference }}/comment"
|
||||
mkdir -p ${COMMENT_DIR}
|
||||
echo ${{ github.event.number }} > ${COMMENT_DIR}/NR
|
||||
COMMENT_LOG="${COMMENT_DIR}/result.txt"
|
||||
touch ${COMMENT_LOG}
|
||||
|
||||
if test -f "${REF_LOG_FILE}"; then
|
||||
echo "Reference SHA1/ID: $(sha1sum -- "${REF_SUMMARY_FILE}")"
|
||||
REF_ERROR=$(sed -n "s/^ERROR: \([[:print:]]\+\).*/\1/p" "${REF_LOG_FILE}" | sort)
|
||||
|
@ -195,28 +205,36 @@ jobs:
|
|||
for LINE in ${REF_FAILING}
|
||||
do
|
||||
if ! grep -Fxq ${LINE}<<<"${NEW_FAILING}"; then
|
||||
echo "::warning ::Congrats! The gnu test ${LINE} is no longer failing!"
|
||||
MSG="Congrats! The gnu test ${LINE} is no longer failing!"
|
||||
echo "::warning ::$MSG"
|
||||
echo $MSG >> ${COMMENT_LOG}
|
||||
fi
|
||||
done
|
||||
for LINE in ${NEW_FAILING}
|
||||
do
|
||||
if ! grep -Fxq ${LINE}<<<"${REF_FAILING}"
|
||||
then
|
||||
echo "::error ::GNU test failed: ${LINE}. ${LINE} is passing on '${{ steps.vars.outputs.repo_default_branch }}'. Maybe you have to rebase?"
|
||||
MSG="GNU test failed: ${LINE}. ${LINE} is passing on '${{ steps.vars.outputs.repo_default_branch }}'. Maybe you have to rebase?"
|
||||
echo "::error ::$MSG"
|
||||
echo $MSG >> ${COMMENT_LOG}
|
||||
have_new_failures="true"
|
||||
fi
|
||||
done
|
||||
for LINE in ${REF_ERROR}
|
||||
do
|
||||
if ! grep -Fxq ${LINE}<<<"${NEW_ERROR}"; then
|
||||
echo "::warning ::Congrats! The gnu test ${LINE} is no longer ERROR!"
|
||||
MSG="Congrats! The gnu test ${LINE} is no longer ERROR!"
|
||||
echo "::warning ::$MSG"
|
||||
echo $MSG >> ${COMMENT_LOG}
|
||||
fi
|
||||
done
|
||||
for LINE in ${NEW_ERROR}
|
||||
do
|
||||
if ! grep -Fxq ${LINE}<<<"${REF_ERROR}"
|
||||
then
|
||||
echo "::error ::GNU test error: ${LINE}. ${LINE} is passing on '${{ steps.vars.outputs.repo_default_branch }}'. Maybe you have to rebase?"
|
||||
MSG="GNU test error: ${LINE}. ${LINE} is passing on '${{ steps.vars.outputs.repo_default_branch }}'. Maybe you have to rebase?"
|
||||
echo "::error ::$MSG"
|
||||
echo $MSG >> ${COMMENT_LOG}
|
||||
have_new_failures="true"
|
||||
fi
|
||||
done
|
||||
|
@ -225,6 +243,12 @@ jobs:
|
|||
echo "::warning ::Skipping test failure comparison; no prior reference test logs are available."
|
||||
fi
|
||||
if test -n "${have_new_failures}" ; then exit -1 ; fi
|
||||
- name: Upload comparison log (for GnuComment workflow)
|
||||
if: success() || failure() # run regardless of prior step success/failure
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: comment
|
||||
path: ${{ steps.vars.outputs.path_reference }}/comment/
|
||||
- name: Compare test summary VS reference
|
||||
if: success() || failure() # run regardless of prior step success/failure
|
||||
shell: bash
|
||||
|
|
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2834,6 +2834,7 @@ version = "0.0.15"
|
|||
dependencies = [
|
||||
"clap",
|
||||
"libc",
|
||||
"nix",
|
||||
"uucore",
|
||||
"winapi",
|
||||
]
|
||||
|
|
|
@ -22,15 +22,11 @@ use std::env;
|
|||
#[cfg(not(windows))]
|
||||
use std::ffi::CString;
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use std::io::Read;
|
||||
use std::io::{self, stderr, stdin, Write};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::{FileTypeExt, PermissionsExt};
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::path::{Path, PathBuf, StripPrefixError};
|
||||
use std::str::FromStr;
|
||||
use std::string::ToString;
|
||||
|
@ -49,6 +45,9 @@ use uucore::fs::{
|
|||
};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
mod platform;
|
||||
use platform::copy_on_write;
|
||||
|
||||
quick_error! {
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
|
@ -224,16 +223,6 @@ pub struct Options {
|
|||
verbose: bool,
|
||||
}
|
||||
|
||||
// From /usr/include/linux/fs.h:
|
||||
// #define FICLONE _IOW(0x94, 9, int)
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
// Use a macro as libc::ioctl expects u32 or u64 depending on the arch
|
||||
macro_rules! FICLONE {
|
||||
() => {
|
||||
0x40049409
|
||||
};
|
||||
}
|
||||
|
||||
static ABOUT: &str = "Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.";
|
||||
static LONG_HELP: &str = "";
|
||||
static EXIT_ERR: i32 = 1;
|
||||
|
@ -1598,26 +1587,7 @@ fn copy_helper(
|
|||
} else if source_is_symlink {
|
||||
copy_link(source, dest, symlinked_files)?;
|
||||
} else {
|
||||
#[cfg(target_os = "macos")]
|
||||
copy_on_write_macos(
|
||||
source,
|
||||
dest,
|
||||
options.reflink_mode,
|
||||
options.sparse_mode,
|
||||
context,
|
||||
)?;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
copy_on_write_linux(
|
||||
source,
|
||||
dest,
|
||||
options.reflink_mode,
|
||||
options.sparse_mode,
|
||||
context,
|
||||
)?;
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))]
|
||||
copy_no_cow_fallback(
|
||||
copy_on_write(
|
||||
source,
|
||||
dest,
|
||||
options.reflink_mode,
|
||||
|
@ -1673,180 +1643,6 @@ fn copy_link(
|
|||
symlink_file(&link, &dest, &context_for(&link, &dest), symlinked_files)
|
||||
}
|
||||
|
||||
/// Copies `source` to `dest` for systems without copy-on-write
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))]
|
||||
fn copy_no_cow_fallback(
|
||||
source: &Path,
|
||||
dest: &Path,
|
||||
reflink_mode: ReflinkMode,
|
||||
sparse_mode: SparseMode,
|
||||
context: &str,
|
||||
) -> CopyResult<()> {
|
||||
if reflink_mode != ReflinkMode::Never {
|
||||
return Err("--reflink is only supported on linux and macOS"
|
||||
.to_string()
|
||||
.into());
|
||||
}
|
||||
if sparse_mode != SparseMode::Auto {
|
||||
return Err("--sparse is only supported on linux".to_string().into());
|
||||
}
|
||||
|
||||
fs::copy(source, dest).context(context)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Use the Linux `ioctl_ficlone` API to do a copy-on-write clone.
|
||||
///
|
||||
/// If `fallback` is true and there is a failure performing the clone,
|
||||
/// then this function performs a standard [`std::fs::copy`]. Otherwise,
|
||||
/// this function returns an error.
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
fn clone<P>(source: P, dest: P, fallback: bool) -> std::io::Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let src_file = File::open(&source)?;
|
||||
let dst_file = File::create(&dest)?;
|
||||
let src_fd = src_file.as_raw_fd();
|
||||
let dst_fd = dst_file.as_raw_fd();
|
||||
let result = unsafe { libc::ioctl(dst_fd, FICLONE!(), src_fd) };
|
||||
if result != 0 {
|
||||
if fallback {
|
||||
std::fs::copy(source, dest).map(|_| ())
|
||||
} else {
|
||||
Err(std::io::Error::last_os_error())
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform a sparse copy from one file to another.
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
fn sparse_copy<P>(source: P, dest: P) -> std::io::Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
use std::os::unix::prelude::MetadataExt;
|
||||
|
||||
let mut src_file = File::open(source)?;
|
||||
let dst_file = File::create(dest)?;
|
||||
let dst_fd = dst_file.as_raw_fd();
|
||||
|
||||
let size: usize = src_file.metadata()?.size().try_into().unwrap();
|
||||
if unsafe { libc::ftruncate(dst_fd, size.try_into().unwrap()) } < 0 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
|
||||
let blksize = dst_file.metadata()?.blksize();
|
||||
let mut buf: Vec<u8> = vec![0; blksize.try_into().unwrap()];
|
||||
let mut current_offset: usize = 0;
|
||||
|
||||
// TODO Perhaps we can employ the "fiemap ioctl" API to get the
|
||||
// file extent mappings:
|
||||
// https://www.kernel.org/doc/html/latest/filesystems/fiemap.html
|
||||
while current_offset < size {
|
||||
let this_read = src_file.read(&mut buf)?;
|
||||
if buf.iter().any(|&x| x != 0) {
|
||||
unsafe {
|
||||
libc::pwrite(
|
||||
dst_fd,
|
||||
buf.as_ptr() as *const libc::c_void,
|
||||
this_read,
|
||||
current_offset.try_into().unwrap(),
|
||||
)
|
||||
};
|
||||
}
|
||||
current_offset += this_read;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copies `source` to `dest` using copy-on-write if possible.
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
fn copy_on_write_linux(
|
||||
source: &Path,
|
||||
dest: &Path,
|
||||
reflink_mode: ReflinkMode,
|
||||
sparse_mode: SparseMode,
|
||||
context: &str,
|
||||
) -> CopyResult<()> {
|
||||
let result = match (reflink_mode, sparse_mode) {
|
||||
(ReflinkMode::Never, _) => std::fs::copy(source, dest).map(|_| ()),
|
||||
(ReflinkMode::Auto, SparseMode::Always) => sparse_copy(source, dest),
|
||||
(ReflinkMode::Auto, _) => clone(source, dest, true),
|
||||
(ReflinkMode::Always, SparseMode::Auto) => clone(source, dest, false),
|
||||
(ReflinkMode::Always, _) => {
|
||||
return Err("`--reflink=always` can be used only with --sparse=auto".into())
|
||||
}
|
||||
};
|
||||
result.context(context)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copies `source` to `dest` using copy-on-write if possible.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn copy_on_write_macos(
|
||||
source: &Path,
|
||||
dest: &Path,
|
||||
reflink_mode: ReflinkMode,
|
||||
sparse_mode: SparseMode,
|
||||
context: &str,
|
||||
) -> CopyResult<()> {
|
||||
if sparse_mode != SparseMode::Auto {
|
||||
return Err("--sparse is only supported on linux".to_string().into());
|
||||
}
|
||||
|
||||
// Extract paths in a form suitable to be passed to a syscall.
|
||||
// The unwrap() is safe because they come from the command-line and so contain non nul
|
||||
// character.
|
||||
let src = CString::new(source.as_os_str().as_bytes()).unwrap();
|
||||
let dst = CString::new(dest.as_os_str().as_bytes()).unwrap();
|
||||
|
||||
// clonefile(2) was introduced in macOS 10.12 so we cannot statically link against it
|
||||
// for backward compatibility.
|
||||
let clonefile = CString::new("clonefile").unwrap();
|
||||
let raw_pfn = unsafe { libc::dlsym(libc::RTLD_NEXT, clonefile.as_ptr()) };
|
||||
|
||||
let mut error = 0;
|
||||
if !raw_pfn.is_null() {
|
||||
// Call clonefile(2).
|
||||
// Safety: Casting a C function pointer to a rust function value is one of the few
|
||||
// blessed uses of `transmute()`.
|
||||
unsafe {
|
||||
let pfn: extern "C" fn(
|
||||
src: *const libc::c_char,
|
||||
dst: *const libc::c_char,
|
||||
flags: u32,
|
||||
) -> libc::c_int = std::mem::transmute(raw_pfn);
|
||||
error = pfn(src.as_ptr(), dst.as_ptr(), 0);
|
||||
if std::io::Error::last_os_error().kind() == std::io::ErrorKind::AlreadyExists {
|
||||
// clonefile(2) fails if the destination exists. Remove it and try again. Do not
|
||||
// bother to check if removal worked because we're going to try to clone again.
|
||||
let _ = fs::remove_file(dest);
|
||||
error = pfn(src.as_ptr(), dst.as_ptr(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if raw_pfn.is_null() || error != 0 {
|
||||
// clonefile(2) is either not supported or it errored out (possibly because the FS does not
|
||||
// support COW).
|
||||
match reflink_mode {
|
||||
ReflinkMode::Always => {
|
||||
return Err(
|
||||
format!("failed to clone {:?} from {:?}: {}", source, dest, error).into(),
|
||||
)
|
||||
}
|
||||
ReflinkMode::Auto => fs::copy(source, dest).context(context)?,
|
||||
ReflinkMode::Never => fs::copy(source, dest).context(context)?,
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate an error message if `target` is not the correct `target_type`
|
||||
pub fn verify_target_type(target: &Path, target_type: &TargetType) -> CopyResult<()> {
|
||||
match (target_type, target.is_dir()) {
|
||||
|
|
110
src/uu/cp/src/platform/linux.rs
Normal file
110
src/uu/cp/src/platform/linux.rs
Normal file
|
@ -0,0 +1,110 @@
|
|||
// * This file is part of the uutils coreutils package.
|
||||
// *
|
||||
// * For the full copyright and license information, please view the LICENSE
|
||||
// * file that was distributed with this source code.
|
||||
// spell-checker:ignore ficlone reflink ftruncate pwrite fiemap
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::path::Path;
|
||||
|
||||
use quick_error::ResultExt;
|
||||
|
||||
use crate::{CopyResult, ReflinkMode, SparseMode};
|
||||
|
||||
// From /usr/include/linux/fs.h:
|
||||
// #define FICLONE _IOW(0x94, 9, int)
|
||||
// Use a macro as libc::ioctl expects u32 or u64 depending on the arch
|
||||
macro_rules! FICLONE {
|
||||
() => {
|
||||
0x40049409
|
||||
};
|
||||
}
|
||||
|
||||
/// Use the Linux `ioctl_ficlone` API to do a copy-on-write clone.
|
||||
///
|
||||
/// If `fallback` is true and there is a failure performing the clone,
|
||||
/// then this function performs a standard [`std::fs::copy`]. Otherwise,
|
||||
/// this function returns an error.
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
fn clone<P>(source: P, dest: P, fallback: bool) -> std::io::Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let src_file = File::open(&source)?;
|
||||
let dst_file = File::create(&dest)?;
|
||||
let src_fd = src_file.as_raw_fd();
|
||||
let dst_fd = dst_file.as_raw_fd();
|
||||
let result = unsafe { libc::ioctl(dst_fd, FICLONE!(), src_fd) };
|
||||
if result != 0 {
|
||||
if fallback {
|
||||
std::fs::copy(source, dest).map(|_| ())
|
||||
} else {
|
||||
Err(std::io::Error::last_os_error())
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform a sparse copy from one file to another.
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
fn sparse_copy<P>(source: P, dest: P) -> std::io::Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
use std::os::unix::prelude::MetadataExt;
|
||||
|
||||
let mut src_file = File::open(source)?;
|
||||
let dst_file = File::create(dest)?;
|
||||
let dst_fd = dst_file.as_raw_fd();
|
||||
|
||||
let size: usize = src_file.metadata()?.size().try_into().unwrap();
|
||||
if unsafe { libc::ftruncate(dst_fd, size.try_into().unwrap()) } < 0 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
|
||||
let blksize = dst_file.metadata()?.blksize();
|
||||
let mut buf: Vec<u8> = vec![0; blksize.try_into().unwrap()];
|
||||
let mut current_offset: usize = 0;
|
||||
|
||||
// TODO Perhaps we can employ the "fiemap ioctl" API to get the
|
||||
// file extent mappings:
|
||||
// https://www.kernel.org/doc/html/latest/filesystems/fiemap.html
|
||||
while current_offset < size {
|
||||
let this_read = src_file.read(&mut buf)?;
|
||||
if buf.iter().any(|&x| x != 0) {
|
||||
unsafe {
|
||||
libc::pwrite(
|
||||
dst_fd,
|
||||
buf.as_ptr() as *const libc::c_void,
|
||||
this_read,
|
||||
current_offset.try_into().unwrap(),
|
||||
)
|
||||
};
|
||||
}
|
||||
current_offset += this_read;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copies `source` to `dest` using copy-on-write if possible.
|
||||
pub(crate) fn copy_on_write(
|
||||
source: &Path,
|
||||
dest: &Path,
|
||||
reflink_mode: ReflinkMode,
|
||||
sparse_mode: SparseMode,
|
||||
context: &str,
|
||||
) -> CopyResult<()> {
|
||||
let result = match (reflink_mode, sparse_mode) {
|
||||
(ReflinkMode::Never, _) => std::fs::copy(source, dest).map(|_| ()),
|
||||
(ReflinkMode::Auto, SparseMode::Always) => sparse_copy(source, dest),
|
||||
(ReflinkMode::Auto, _) => clone(source, dest, true),
|
||||
(ReflinkMode::Always, SparseMode::Auto) => clone(source, dest, false),
|
||||
(ReflinkMode::Always, _) => {
|
||||
return Err("`--reflink=always` can be used only with --sparse=auto".into())
|
||||
}
|
||||
};
|
||||
result.context(context)?;
|
||||
Ok(())
|
||||
}
|
74
src/uu/cp/src/platform/macos.rs
Normal file
74
src/uu/cp/src/platform/macos.rs
Normal file
|
@ -0,0 +1,74 @@
|
|||
// * This file is part of the uutils coreutils package.
|
||||
// *
|
||||
// * For the full copyright and license information, please view the LICENSE
|
||||
// * file that was distributed with this source code.
|
||||
// spell-checker:ignore reflink
|
||||
use std::ffi::CString;
|
||||
use std::fs;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::path::Path;
|
||||
|
||||
use quick_error::ResultExt;
|
||||
|
||||
use crate::{CopyResult, ReflinkMode, SparseMode};
|
||||
|
||||
/// Copies `source` to `dest` using copy-on-write if possible.
|
||||
pub(crate) fn copy_on_write(
|
||||
source: &Path,
|
||||
dest: &Path,
|
||||
reflink_mode: ReflinkMode,
|
||||
sparse_mode: SparseMode,
|
||||
context: &str,
|
||||
) -> CopyResult<()> {
|
||||
if sparse_mode != SparseMode::Auto {
|
||||
return Err("--sparse is only supported on linux".to_string().into());
|
||||
}
|
||||
|
||||
// Extract paths in a form suitable to be passed to a syscall.
|
||||
// The unwrap() is safe because they come from the command-line and so contain non nul
|
||||
// character.
|
||||
let src = CString::new(source.as_os_str().as_bytes()).unwrap();
|
||||
let dst = CString::new(dest.as_os_str().as_bytes()).unwrap();
|
||||
|
||||
// clonefile(2) was introduced in macOS 10.12 so we cannot statically link against it
|
||||
// for backward compatibility.
|
||||
let clonefile = CString::new("clonefile").unwrap();
|
||||
let raw_pfn = unsafe { libc::dlsym(libc::RTLD_NEXT, clonefile.as_ptr()) };
|
||||
|
||||
let mut error = 0;
|
||||
if !raw_pfn.is_null() {
|
||||
// Call clonefile(2).
|
||||
// Safety: Casting a C function pointer to a rust function value is one of the few
|
||||
// blessed uses of `transmute()`.
|
||||
unsafe {
|
||||
let pfn: extern "C" fn(
|
||||
src: *const libc::c_char,
|
||||
dst: *const libc::c_char,
|
||||
flags: u32,
|
||||
) -> libc::c_int = std::mem::transmute(raw_pfn);
|
||||
error = pfn(src.as_ptr(), dst.as_ptr(), 0);
|
||||
if std::io::Error::last_os_error().kind() == std::io::ErrorKind::AlreadyExists {
|
||||
// clonefile(2) fails if the destination exists. Remove it and try again. Do not
|
||||
// bother to check if removal worked because we're going to try to clone again.
|
||||
let _ = fs::remove_file(dest);
|
||||
error = pfn(src.as_ptr(), dst.as_ptr(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if raw_pfn.is_null() || error != 0 {
|
||||
// clonefile(2) is either not supported or it errored out (possibly because the FS does not
|
||||
// support COW).
|
||||
match reflink_mode {
|
||||
ReflinkMode::Always => {
|
||||
return Err(
|
||||
format!("failed to clone {:?} from {:?}: {}", source, dest, error).into(),
|
||||
)
|
||||
}
|
||||
ReflinkMode::Auto => fs::copy(source, dest).context(context)?,
|
||||
ReflinkMode::Never => fs::copy(source, dest).context(context)?,
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
18
src/uu/cp/src/platform/mod.rs
Normal file
18
src/uu/cp/src/platform/mod.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
// * This file is part of the uutils coreutils package.
|
||||
// *
|
||||
// * For the full copyright and license information, please view the LICENSE
|
||||
// * file that was distributed with this source code.
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) use self::macos::copy_on_write;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
mod linux;
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub(crate) use self::linux::copy_on_write;
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))]
|
||||
mod other;
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))]
|
||||
pub(crate) use self::other::copy_on_write;
|
33
src/uu/cp/src/platform/other.rs
Normal file
33
src/uu/cp/src/platform/other.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
// * This file is part of the uutils coreutils package.
|
||||
// *
|
||||
// * For the full copyright and license information, please view the LICENSE
|
||||
// * file that was distributed with this source code.
|
||||
// spell-checker:ignore reflink
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use quick_error::ResultExt;
|
||||
|
||||
use crate::{CopyResult, ReflinkMode, SparseMode};
|
||||
|
||||
/// Copies `source` to `dest` for systems without copy-on-write
|
||||
pub(crate) fn copy_on_write(
|
||||
source: &Path,
|
||||
dest: &Path,
|
||||
reflink_mode: ReflinkMode,
|
||||
sparse_mode: SparseMode,
|
||||
context: &str,
|
||||
) -> CopyResult<()> {
|
||||
if reflink_mode != ReflinkMode::Never {
|
||||
return Err("--reflink is only supported on linux and macOS"
|
||||
.to_string()
|
||||
.into());
|
||||
}
|
||||
if sparse_mode != SparseMode::Auto {
|
||||
return Err("--sparse is only supported on linux".to_string().into());
|
||||
}
|
||||
|
||||
fs::copy(source, dest).context(context)?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -14,7 +14,7 @@ use clap::{
|
|||
builder::{NonEmptyStringValueParser, ValueParser},
|
||||
crate_version, Arg, Command,
|
||||
};
|
||||
use glob::Pattern;
|
||||
use glob::{MatchOptions, Pattern};
|
||||
use lscolors::LsColors;
|
||||
use number_prefix::NumberPrefix;
|
||||
use once_cell::unsync::OnceCell;
|
||||
|
@ -41,6 +41,7 @@ use term_grid::{Cell, Direction, Filling, Grid, GridOptions};
|
|||
use unicode_width::UnicodeWidthStr;
|
||||
#[cfg(unix)]
|
||||
use uucore::libc::{S_IXGRP, S_IXOTH, S_IXUSR};
|
||||
use uucore::parse_glob;
|
||||
use uucore::quoting_style::{escape_name, QuotingStyle};
|
||||
use uucore::{
|
||||
display::Quotable,
|
||||
|
@ -765,7 +766,7 @@ impl Config {
|
|||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
match Pattern::new(pattern) {
|
||||
match parse_glob::from_str(pattern) {
|
||||
Ok(p) => {
|
||||
ignore_patterns.push(p);
|
||||
}
|
||||
|
@ -779,7 +780,7 @@ impl Config {
|
|||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
match Pattern::new(pattern) {
|
||||
match parse_glob::from_str(pattern) {
|
||||
Ok(p) => {
|
||||
ignore_patterns.push(p);
|
||||
}
|
||||
|
@ -1877,16 +1878,18 @@ fn should_display(entry: &DirEntry, config: &Config) -> bool {
|
|||
return false;
|
||||
}
|
||||
|
||||
// check if explicitly ignored
|
||||
for pattern in &config.ignore_patterns {
|
||||
if pattern.matches(entry.file_name().to_str().unwrap()) {
|
||||
return false;
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
// else default to display
|
||||
true
|
||||
// check if it is among ignore_patterns
|
||||
let options = MatchOptions {
|
||||
// setting require_literal_leading_dot to match behavior in GNU ls
|
||||
require_literal_leading_dot: true,
|
||||
require_literal_separator: false,
|
||||
case_sensitive: true,
|
||||
};
|
||||
let file_name = entry.file_name().into_string().unwrap();
|
||||
!config
|
||||
.ignore_patterns
|
||||
.iter()
|
||||
.any(|p| p.matches_with(&file_name, options))
|
||||
}
|
||||
|
||||
fn enter_directory(
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
use crate::number::DynamicWidthNumber;
|
||||
use crate::number::FixedWidthNumber;
|
||||
use crate::number::Number;
|
||||
use uucore::error::{UResult, USimpleError};
|
||||
|
||||
/// The format to use for suffixes in the filename for each output chunk.
|
||||
#[derive(Clone, Copy)]
|
||||
|
@ -119,19 +120,28 @@ impl<'a> FilenameIterator<'a> {
|
|||
additional_suffix: &'a str,
|
||||
suffix_length: usize,
|
||||
suffix_type: SuffixType,
|
||||
) -> FilenameIterator<'a> {
|
||||
suffix_start: usize,
|
||||
) -> UResult<FilenameIterator<'a>> {
|
||||
let radix = suffix_type.radix();
|
||||
let number = if suffix_length == 0 {
|
||||
Number::DynamicWidth(DynamicWidthNumber::new(radix))
|
||||
Number::DynamicWidth(DynamicWidthNumber::new(radix, suffix_start))
|
||||
} else {
|
||||
Number::FixedWidth(FixedWidthNumber::new(radix, suffix_length))
|
||||
Number::FixedWidth(
|
||||
FixedWidthNumber::new(radix, suffix_length, suffix_start).map_err(|_| {
|
||||
USimpleError::new(
|
||||
1,
|
||||
"numerical suffix start value is too large for the suffix length",
|
||||
)
|
||||
})?,
|
||||
)
|
||||
};
|
||||
FilenameIterator {
|
||||
|
||||
Ok(FilenameIterator {
|
||||
prefix,
|
||||
additional_suffix,
|
||||
number,
|
||||
first_iteration: true,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -161,36 +171,36 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_filename_iterator_alphabetic_fixed_width() {
|
||||
let mut it = FilenameIterator::new("chunk_", ".txt", 2, SuffixType::Alphabetic);
|
||||
let mut it = FilenameIterator::new("chunk_", ".txt", 2, SuffixType::Alphabetic, 0).unwrap();
|
||||
assert_eq!(it.next().unwrap(), "chunk_aa.txt");
|
||||
assert_eq!(it.next().unwrap(), "chunk_ab.txt");
|
||||
assert_eq!(it.next().unwrap(), "chunk_ac.txt");
|
||||
|
||||
let mut it = FilenameIterator::new("chunk_", ".txt", 2, SuffixType::Alphabetic);
|
||||
let mut it = FilenameIterator::new("chunk_", ".txt", 2, SuffixType::Alphabetic, 0).unwrap();
|
||||
assert_eq!(it.nth(26 * 26 - 1).unwrap(), "chunk_zz.txt");
|
||||
assert_eq!(it.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filename_iterator_numeric_fixed_width() {
|
||||
let mut it = FilenameIterator::new("chunk_", ".txt", 2, SuffixType::Decimal);
|
||||
let mut it = FilenameIterator::new("chunk_", ".txt", 2, SuffixType::Decimal, 0).unwrap();
|
||||
assert_eq!(it.next().unwrap(), "chunk_00.txt");
|
||||
assert_eq!(it.next().unwrap(), "chunk_01.txt");
|
||||
assert_eq!(it.next().unwrap(), "chunk_02.txt");
|
||||
|
||||
let mut it = FilenameIterator::new("chunk_", ".txt", 2, SuffixType::Decimal);
|
||||
let mut it = FilenameIterator::new("chunk_", ".txt", 2, SuffixType::Decimal, 0).unwrap();
|
||||
assert_eq!(it.nth(10 * 10 - 1).unwrap(), "chunk_99.txt");
|
||||
assert_eq!(it.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filename_iterator_alphabetic_dynamic_width() {
|
||||
let mut it = FilenameIterator::new("chunk_", ".txt", 0, SuffixType::Alphabetic);
|
||||
let mut it = FilenameIterator::new("chunk_", ".txt", 0, SuffixType::Alphabetic, 0).unwrap();
|
||||
assert_eq!(it.next().unwrap(), "chunk_aa.txt");
|
||||
assert_eq!(it.next().unwrap(), "chunk_ab.txt");
|
||||
assert_eq!(it.next().unwrap(), "chunk_ac.txt");
|
||||
|
||||
let mut it = FilenameIterator::new("chunk_", ".txt", 0, SuffixType::Alphabetic);
|
||||
let mut it = FilenameIterator::new("chunk_", ".txt", 0, SuffixType::Alphabetic, 0).unwrap();
|
||||
assert_eq!(it.nth(26 * 25 - 1).unwrap(), "chunk_yz.txt");
|
||||
assert_eq!(it.next().unwrap(), "chunk_zaaa.txt");
|
||||
assert_eq!(it.next().unwrap(), "chunk_zaab.txt");
|
||||
|
@ -198,14 +208,49 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_filename_iterator_numeric_dynamic_width() {
|
||||
let mut it = FilenameIterator::new("chunk_", ".txt", 0, SuffixType::Decimal);
|
||||
let mut it = FilenameIterator::new("chunk_", ".txt", 0, SuffixType::Decimal, 0).unwrap();
|
||||
assert_eq!(it.next().unwrap(), "chunk_00.txt");
|
||||
assert_eq!(it.next().unwrap(), "chunk_01.txt");
|
||||
assert_eq!(it.next().unwrap(), "chunk_02.txt");
|
||||
|
||||
let mut it = FilenameIterator::new("chunk_", ".txt", 0, SuffixType::Decimal);
|
||||
let mut it = FilenameIterator::new("chunk_", ".txt", 0, SuffixType::Decimal, 0).unwrap();
|
||||
assert_eq!(it.nth(10 * 9 - 1).unwrap(), "chunk_89.txt");
|
||||
assert_eq!(it.next().unwrap(), "chunk_9000.txt");
|
||||
assert_eq!(it.next().unwrap(), "chunk_9001.txt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filename_iterator_numeric_suffix_decimal() {
|
||||
let mut it = FilenameIterator::new("chunk_", ".txt", 0, SuffixType::Decimal, 5).unwrap();
|
||||
assert_eq!(it.next().unwrap(), "chunk_05.txt");
|
||||
assert_eq!(it.next().unwrap(), "chunk_06.txt");
|
||||
assert_eq!(it.next().unwrap(), "chunk_07.txt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filename_iterator_numeric_suffix_hex() {
|
||||
let mut it =
|
||||
FilenameIterator::new("chunk_", ".txt", 0, SuffixType::Hexadecimal, 9).unwrap();
|
||||
assert_eq!(it.next().unwrap(), "chunk_09.txt");
|
||||
assert_eq!(it.next().unwrap(), "chunk_0a.txt");
|
||||
assert_eq!(it.next().unwrap(), "chunk_0b.txt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filename_iterator_numeric_suffix_err() {
|
||||
let mut it = FilenameIterator::new("chunk_", ".txt", 3, SuffixType::Decimal, 999).unwrap();
|
||||
assert_eq!(it.next().unwrap(), "chunk_999.txt");
|
||||
assert!(it.next().is_none());
|
||||
|
||||
let it = FilenameIterator::new("chunk_", ".txt", 3, SuffixType::Decimal, 1000);
|
||||
assert!(it.is_err());
|
||||
|
||||
let mut it =
|
||||
FilenameIterator::new("chunk_", ".txt", 3, SuffixType::Hexadecimal, 0xfff).unwrap();
|
||||
assert_eq!(it.next().unwrap(), "chunk_fff.txt");
|
||||
assert!(it.next().is_none());
|
||||
|
||||
let it = FilenameIterator::new("chunk_", ".txt", 3, SuffixType::Hexadecimal, 0x1000);
|
||||
assert!(it.is_err());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -100,10 +100,10 @@ impl Number {
|
|||
/// differently and we only intend to use these numbers for display
|
||||
/// purposes and not for mathematical purposes.
|
||||
#[allow(dead_code)]
|
||||
fn digits(&self) -> &Vec<u8> {
|
||||
fn digits(&self) -> Vec<u8> {
|
||||
match self {
|
||||
Self::FixedWidth(number) => &number.digits,
|
||||
Self::DynamicWidth(number) => &number.digits,
|
||||
Self::FixedWidth(number) => number.digits.clone(),
|
||||
Self::DynamicWidth(number) => number.digits(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -175,7 +175,7 @@ impl Display for Number {
|
|||
///
|
||||
/// # Displaying
|
||||
///
|
||||
/// This number is only displayable if `radix` is 10, 26, or 26. If
|
||||
/// This number is only displayable if `radix` is 10, 16, or 26. If
|
||||
/// `radix` is 10 or 16, then the digits are concatenated and
|
||||
/// displayed as a fixed-width decimal or hexadecimal number,
|
||||
/// respectively. If `radix` is 26, then each digit is translated to
|
||||
|
@ -189,10 +189,21 @@ pub struct FixedWidthNumber {
|
|||
|
||||
impl FixedWidthNumber {
|
||||
/// Instantiate a number of the given radix and width.
|
||||
pub fn new(radix: u8, width: usize) -> Self {
|
||||
Self {
|
||||
radix,
|
||||
digits: vec![0; width],
|
||||
pub fn new(radix: u8, width: usize, mut suffix_start: usize) -> Result<Self, Overflow> {
|
||||
let mut digits = vec![0_u8; width];
|
||||
|
||||
for i in (0..digits.len()).rev() {
|
||||
let remainder = (suffix_start % (radix as usize)) as u8;
|
||||
suffix_start /= radix as usize;
|
||||
digits[i] = remainder;
|
||||
if suffix_start == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if suffix_start != 0 {
|
||||
Err(Overflow)
|
||||
} else {
|
||||
Ok(Self { radix, digits })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -229,25 +240,12 @@ impl FixedWidthNumber {
|
|||
|
||||
impl Display for FixedWidthNumber {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
match self.radix {
|
||||
10 => {
|
||||
let digits: String = self.digits.iter().map(|d| (b'0' + d) as char).collect();
|
||||
write!(f, "{}", digits)
|
||||
}
|
||||
16 => {
|
||||
let digits: String = self
|
||||
.digits
|
||||
.iter()
|
||||
.map(|d| (if *d < 10 { b'0' + d } else { b'a' + (d - 10) }) as char)
|
||||
.collect();
|
||||
write!(f, "{}", digits)
|
||||
}
|
||||
26 => {
|
||||
let digits: String = self.digits.iter().map(|d| (b'a' + d) as char).collect();
|
||||
write!(f, "{}", digits)
|
||||
}
|
||||
_ => Err(fmt::Error),
|
||||
}
|
||||
let digits: String = self
|
||||
.digits
|
||||
.iter()
|
||||
.map(|d| map_digit(self.radix, *d))
|
||||
.collect();
|
||||
write!(f, "{}", digits)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -293,105 +291,74 @@ impl Display for FixedWidthNumber {
|
|||
#[derive(Clone)]
|
||||
pub struct DynamicWidthNumber {
|
||||
radix: u8,
|
||||
digits: Vec<u8>,
|
||||
current: usize,
|
||||
}
|
||||
|
||||
impl DynamicWidthNumber {
|
||||
/// Instantiate a number of the given radix, starting with width 2.
|
||||
///
|
||||
/// This associated function returns a new instance of the struct
|
||||
/// with the given radix and a width of two digits, both 0.
|
||||
pub fn new(radix: u8) -> Self {
|
||||
pub fn new(radix: u8, suffix_start: usize) -> Self {
|
||||
Self {
|
||||
radix,
|
||||
digits: vec![0, 0],
|
||||
current: suffix_start,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set all digits to zero.
|
||||
fn reset(&mut self) {
|
||||
for i in 0..self.digits.len() {
|
||||
self.digits[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Increment this number.
|
||||
///
|
||||
/// This method adds one to this number. The first time that the
|
||||
/// most significant digit would achieve its highest possible
|
||||
/// value (that is, `radix - 1`), then all the digits get reset to
|
||||
/// 0 and the number of digits increases by one.
|
||||
///
|
||||
/// This method never returns an error.
|
||||
fn increment(&mut self) -> Result<(), Overflow> {
|
||||
for i in (0..self.digits.len()).rev() {
|
||||
// Increment the current digit.
|
||||
self.digits[i] += 1;
|
||||
|
||||
// If the digit overflows, then set it to 0 and continue
|
||||
// to the next iteration to increment the next most
|
||||
// significant digit. Otherwise, terminate the loop, since
|
||||
// there will be no further changes to any higher order
|
||||
// digits.
|
||||
if self.digits[i] == self.radix {
|
||||
self.digits[i] = 0;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If the most significant digit is at its maximum value, then
|
||||
// add another digit and reset all digits zero.
|
||||
if self.digits[0] == self.radix - 1 {
|
||||
self.digits.push(0);
|
||||
self.reset();
|
||||
}
|
||||
self.current += 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn digits(&self) -> Vec<u8> {
|
||||
let radix = self.radix as usize;
|
||||
let mut remaining = self.current;
|
||||
let mut sub_value = (radix - 1) * radix;
|
||||
let mut num_fill_chars = 2;
|
||||
|
||||
// Convert the number into "num_fill_chars" and "remaining"
|
||||
while remaining >= sub_value {
|
||||
remaining -= sub_value;
|
||||
sub_value *= radix;
|
||||
num_fill_chars += 1;
|
||||
}
|
||||
|
||||
// Convert the "remainder" to digits
|
||||
let mut digits = Vec::new();
|
||||
while remaining > 0 {
|
||||
digits.push((remaining % radix) as u8);
|
||||
remaining /= radix;
|
||||
}
|
||||
// Left pad the vec
|
||||
digits.resize(num_fill_chars, 0);
|
||||
digits.reverse();
|
||||
digits
|
||||
}
|
||||
}
|
||||
|
||||
fn map_digit(radix: u8, d: u8) -> char {
|
||||
(match radix {
|
||||
10 => b'0' + d,
|
||||
16 => {
|
||||
if d < 10 {
|
||||
b'0' + d
|
||||
} else {
|
||||
b'a' + (d - 10)
|
||||
}
|
||||
}
|
||||
26 => b'a' + d,
|
||||
_ => 0,
|
||||
}) as char
|
||||
}
|
||||
|
||||
impl Display for DynamicWidthNumber {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
match self.radix {
|
||||
10 => {
|
||||
let num_fill_chars = self.digits.len() - 2;
|
||||
let digits: String = self.digits.iter().map(|d| (b'0' + d) as char).collect();
|
||||
write!(
|
||||
f,
|
||||
"{empty:9<num_fill_chars$}{digits}",
|
||||
empty = "",
|
||||
num_fill_chars = num_fill_chars,
|
||||
digits = digits,
|
||||
)
|
||||
}
|
||||
16 => {
|
||||
let num_fill_chars = self.digits.len() - 2;
|
||||
let digits: String = self
|
||||
.digits
|
||||
.iter()
|
||||
.map(|d| (if *d < 10 { b'0' + d } else { b'a' + (d - 10) }) as char)
|
||||
.collect();
|
||||
write!(
|
||||
f,
|
||||
"{empty:f<num_fill_chars$}{digits}",
|
||||
empty = "",
|
||||
num_fill_chars = num_fill_chars,
|
||||
digits = digits,
|
||||
)
|
||||
}
|
||||
26 => {
|
||||
let num_fill_chars = self.digits.len() - 2;
|
||||
let digits: String = self.digits.iter().map(|d| (b'a' + d) as char).collect();
|
||||
write!(
|
||||
f,
|
||||
"{empty:z<num_fill_chars$}{digits}",
|
||||
empty = "",
|
||||
num_fill_chars = num_fill_chars,
|
||||
digits = digits,
|
||||
)
|
||||
}
|
||||
_ => Err(fmt::Error),
|
||||
}
|
||||
let digits: String = self
|
||||
.digits()
|
||||
.iter()
|
||||
.map(|d| map_digit(self.radix, *d))
|
||||
.collect();
|
||||
let fill: String = (0..digits.len() - 2)
|
||||
.map(|_| map_digit(self.radix, self.radix - 1))
|
||||
.collect();
|
||||
write!(f, "{fill}{digits}")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -404,35 +371,36 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_dynamic_width_number_increment() {
|
||||
let mut n = Number::DynamicWidth(DynamicWidthNumber::new(3));
|
||||
assert_eq!(n.digits(), &vec![0, 0]);
|
||||
println!("Here");
|
||||
let mut n = Number::DynamicWidth(DynamicWidthNumber::new(3, 0));
|
||||
assert_eq!(n.digits(), vec![0, 0]);
|
||||
|
||||
n.increment().unwrap();
|
||||
assert_eq!(n.digits(), &vec![0, 1]);
|
||||
assert_eq!(n.digits(), vec![0, 1]);
|
||||
|
||||
n.increment().unwrap();
|
||||
assert_eq!(n.digits(), &vec![0, 2]);
|
||||
assert_eq!(n.digits(), vec![0, 2]);
|
||||
|
||||
n.increment().unwrap();
|
||||
assert_eq!(n.digits(), &vec![1, 0]);
|
||||
assert_eq!(n.digits(), vec![1, 0]);
|
||||
|
||||
n.increment().unwrap();
|
||||
assert_eq!(n.digits(), &vec![1, 1]);
|
||||
assert_eq!(n.digits(), vec![1, 1]);
|
||||
|
||||
n.increment().unwrap();
|
||||
assert_eq!(n.digits(), &vec![1, 2]);
|
||||
assert_eq!(n.digits(), vec![1, 2]);
|
||||
|
||||
n.increment().unwrap();
|
||||
assert_eq!(n.digits(), &vec![0, 0, 0]);
|
||||
assert_eq!(n.digits(), vec![0, 0, 0]);
|
||||
|
||||
n.increment().unwrap();
|
||||
assert_eq!(n.digits(), &vec![0, 0, 1]);
|
||||
assert_eq!(n.digits(), vec![0, 0, 1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dynamic_width_number_display_alphabetic() {
|
||||
fn num(n: usize) -> Number {
|
||||
let mut number = Number::DynamicWidth(DynamicWidthNumber::new(26));
|
||||
let mut number = Number::DynamicWidth(DynamicWidthNumber::new(26, 0));
|
||||
for _ in 0..n {
|
||||
number.increment().unwrap();
|
||||
}
|
||||
|
@ -456,7 +424,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_dynamic_width_number_display_numeric_decimal() {
|
||||
fn num(n: usize) -> Number {
|
||||
let mut number = Number::DynamicWidth(DynamicWidthNumber::new(10));
|
||||
let mut number = Number::DynamicWidth(DynamicWidthNumber::new(10, 0));
|
||||
for _ in 0..n {
|
||||
number.increment().unwrap();
|
||||
}
|
||||
|
@ -477,7 +445,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_dynamic_width_number_display_numeric_hexadecimal() {
|
||||
fn num(n: usize) -> Number {
|
||||
let mut number = Number::DynamicWidth(DynamicWidthNumber::new(16));
|
||||
let mut number = Number::DynamicWidth(DynamicWidthNumber::new(16, 0));
|
||||
for _ in 0..n {
|
||||
number.increment().unwrap();
|
||||
}
|
||||
|
@ -500,31 +468,31 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_fixed_width_number_increment() {
|
||||
let mut n = Number::FixedWidth(FixedWidthNumber::new(3, 2));
|
||||
assert_eq!(n.digits(), &vec![0, 0]);
|
||||
let mut n = Number::FixedWidth(FixedWidthNumber::new(3, 2, 0).unwrap());
|
||||
assert_eq!(n.digits(), vec![0, 0]);
|
||||
n.increment().unwrap();
|
||||
assert_eq!(n.digits(), &vec![0, 1]);
|
||||
assert_eq!(n.digits(), vec![0, 1]);
|
||||
n.increment().unwrap();
|
||||
assert_eq!(n.digits(), &vec![0, 2]);
|
||||
assert_eq!(n.digits(), vec![0, 2]);
|
||||
n.increment().unwrap();
|
||||
assert_eq!(n.digits(), &vec![1, 0]);
|
||||
assert_eq!(n.digits(), vec![1, 0]);
|
||||
n.increment().unwrap();
|
||||
assert_eq!(n.digits(), &vec![1, 1]);
|
||||
assert_eq!(n.digits(), vec![1, 1]);
|
||||
n.increment().unwrap();
|
||||
assert_eq!(n.digits(), &vec![1, 2]);
|
||||
assert_eq!(n.digits(), vec![1, 2]);
|
||||
n.increment().unwrap();
|
||||
assert_eq!(n.digits(), &vec![2, 0]);
|
||||
assert_eq!(n.digits(), vec![2, 0]);
|
||||
n.increment().unwrap();
|
||||
assert_eq!(n.digits(), &vec![2, 1]);
|
||||
assert_eq!(n.digits(), vec![2, 1]);
|
||||
n.increment().unwrap();
|
||||
assert_eq!(n.digits(), &vec![2, 2]);
|
||||
assert_eq!(n.digits(), vec![2, 2]);
|
||||
assert!(n.increment().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fixed_width_number_display_alphabetic() {
|
||||
fn num(n: usize) -> Result<Number, Overflow> {
|
||||
let mut number = Number::FixedWidth(FixedWidthNumber::new(26, 2));
|
||||
let mut number = Number::FixedWidth(FixedWidthNumber::new(26, 2, 0).unwrap());
|
||||
for _ in 0..n {
|
||||
number.increment()?;
|
||||
}
|
||||
|
@ -549,7 +517,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_fixed_width_number_display_numeric_decimal() {
|
||||
fn num(n: usize) -> Result<Number, Overflow> {
|
||||
let mut number = Number::FixedWidth(FixedWidthNumber::new(10, 2));
|
||||
let mut number = Number::FixedWidth(FixedWidthNumber::new(10, 2, 0).unwrap());
|
||||
for _ in 0..n {
|
||||
number.increment()?;
|
||||
}
|
||||
|
@ -568,7 +536,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_fixed_width_number_display_numeric_hexadecimal() {
|
||||
fn num(n: usize) -> Result<Number, Overflow> {
|
||||
let mut number = Number::FixedWidth(FixedWidthNumber::new(16, 2));
|
||||
let mut number = Number::FixedWidth(FixedWidthNumber::new(16, 2, 0).unwrap());
|
||||
for _ in 0..n {
|
||||
number.increment()?;
|
||||
}
|
||||
|
@ -583,4 +551,32 @@ mod tests {
|
|||
assert_eq!(format!("{}", num(16 * 16 - 1).unwrap()), "ff");
|
||||
assert!(num(16 * 16).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fixed_width_number_start_suffix() {
|
||||
fn num(n: usize) -> Result<Number, Overflow> {
|
||||
let mut number = Number::FixedWidth(FixedWidthNumber::new(16, 2, 0x14)?);
|
||||
for _ in 0..n {
|
||||
number.increment()?;
|
||||
}
|
||||
Ok(number)
|
||||
}
|
||||
|
||||
assert_eq!(format!("{}", num(0).unwrap()), "14");
|
||||
assert_eq!(format!("{}", num(0xf).unwrap()), "23");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dynamic_width_number_start_suffix() {
|
||||
fn num(n: usize) -> Result<Number, Overflow> {
|
||||
let mut number = Number::DynamicWidth(DynamicWidthNumber::new(10, 8));
|
||||
for _ in 0..n {
|
||||
number.increment()?;
|
||||
}
|
||||
Ok(number)
|
||||
}
|
||||
|
||||
assert_eq!(format!("{}", num(0).unwrap()), "08");
|
||||
assert_eq!(format!("{}", num(8).unwrap()), "16");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -144,7 +144,7 @@ pub fn uu_app<'a>() -> Command<'a> {
|
|||
.takes_value(true)
|
||||
.value_name("N")
|
||||
.default_value(OPT_DEFAULT_SUFFIX_LENGTH)
|
||||
.help("use suffixes of length N (default 2)"),
|
||||
.help("use suffixes of fixed length N. 0 implies dynamic length."),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(OPT_HEX_SUFFIXES)
|
||||
|
@ -400,13 +400,23 @@ impl Strategy {
|
|||
}
|
||||
|
||||
/// Parse the suffix type from the command-line arguments.
|
||||
fn suffix_type_from(matches: &ArgMatches) -> SuffixType {
|
||||
fn suffix_type_from(matches: &ArgMatches) -> Result<(SuffixType, usize), SettingsError> {
|
||||
if matches.value_source(OPT_NUMERIC_SUFFIXES) == Some(ValueSource::CommandLine) {
|
||||
SuffixType::Decimal
|
||||
let suffix_start = matches.value_of(OPT_NUMERIC_SUFFIXES);
|
||||
let suffix_start = suffix_start.ok_or(SettingsError::SuffixNotParsable(String::new()))?;
|
||||
let suffix_start = suffix_start
|
||||
.parse()
|
||||
.map_err(|_| SettingsError::SuffixNotParsable(suffix_start.to_string()))?;
|
||||
Ok((SuffixType::Decimal, suffix_start))
|
||||
} else if matches.value_source(OPT_HEX_SUFFIXES) == Some(ValueSource::CommandLine) {
|
||||
SuffixType::Hexadecimal
|
||||
let suffix_start = matches.value_of(OPT_HEX_SUFFIXES);
|
||||
let suffix_start = suffix_start.ok_or(SettingsError::SuffixNotParsable(String::new()))?;
|
||||
let suffix_start = usize::from_str_radix(suffix_start, 16)
|
||||
.map_err(|_| SettingsError::SuffixNotParsable(suffix_start.to_string()))?;
|
||||
Ok((SuffixType::Hexadecimal, suffix_start))
|
||||
} else {
|
||||
SuffixType::Alphabetic
|
||||
// no numeric/hex suffix
|
||||
Ok((SuffixType::Alphabetic, 0))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -418,6 +428,7 @@ struct Settings {
|
|||
prefix: String,
|
||||
suffix_type: SuffixType,
|
||||
suffix_length: usize,
|
||||
suffix_start: usize,
|
||||
additional_suffix: String,
|
||||
input: String,
|
||||
/// When supplied, a shell command to output to instead of xaa, xab …
|
||||
|
@ -497,7 +508,7 @@ impl Settings {
|
|||
return Err(SettingsError::SuffixContainsSeparator(additional_suffix));
|
||||
}
|
||||
let strategy = Strategy::from(matches).map_err(SettingsError::Strategy)?;
|
||||
let suffix_type = suffix_type_from(matches);
|
||||
let (suffix_type, suffix_start) = suffix_type_from(matches)?;
|
||||
let suffix_length_str = matches.get_one::<String>(OPT_SUFFIX_LENGTH).unwrap();
|
||||
let suffix_length: usize = suffix_length_str
|
||||
.parse()
|
||||
|
@ -517,6 +528,7 @@ impl Settings {
|
|||
.parse()
|
||||
.map_err(|_| SettingsError::SuffixNotParsable(suffix_length_str.to_string()))?,
|
||||
suffix_type,
|
||||
suffix_start,
|
||||
additional_suffix,
|
||||
verbose: matches.value_source("verbose") == Some(ValueSource::CommandLine),
|
||||
strategy,
|
||||
|
@ -589,7 +601,8 @@ impl<'a> ByteChunkWriter<'a> {
|
|||
&settings.additional_suffix,
|
||||
settings.suffix_length,
|
||||
settings.suffix_type,
|
||||
);
|
||||
settings.suffix_start,
|
||||
)?;
|
||||
let filename = filename_iterator
|
||||
.next()
|
||||
.ok_or_else(|| USimpleError::new(1, "output file suffixes exhausted"))?;
|
||||
|
@ -717,7 +730,8 @@ impl<'a> LineChunkWriter<'a> {
|
|||
&settings.additional_suffix,
|
||||
settings.suffix_length,
|
||||
settings.suffix_type,
|
||||
);
|
||||
settings.suffix_start,
|
||||
)?;
|
||||
let filename = filename_iterator
|
||||
.next()
|
||||
.ok_or_else(|| USimpleError::new(1, "output file suffixes exhausted"))?;
|
||||
|
@ -825,7 +839,8 @@ impl<'a> LineBytesChunkWriter<'a> {
|
|||
&settings.additional_suffix,
|
||||
settings.suffix_length,
|
||||
settings.suffix_type,
|
||||
);
|
||||
settings.suffix_start,
|
||||
)?;
|
||||
let filename = filename_iterator
|
||||
.next()
|
||||
.ok_or_else(|| USimpleError::new(1, "output file suffixes exhausted"))?;
|
||||
|
@ -1022,7 +1037,8 @@ where
|
|||
&settings.additional_suffix,
|
||||
settings.suffix_length,
|
||||
settings.suffix_type,
|
||||
);
|
||||
settings.suffix_start,
|
||||
)?;
|
||||
|
||||
// Create one writer for each chunk. This will create each
|
||||
// of the underlying files (if not in `--filter` mode).
|
||||
|
@ -1098,7 +1114,8 @@ where
|
|||
&settings.additional_suffix,
|
||||
settings.suffix_length,
|
||||
settings.suffix_type,
|
||||
);
|
||||
settings.suffix_start,
|
||||
)?;
|
||||
|
||||
// Create one writer for each chunk. This will create each
|
||||
// of the underlying files (if not in `--filter` mode).
|
||||
|
|
|
@ -19,6 +19,9 @@ clap = { version = "3.2", features = ["wrap_help", "cargo"] }
|
|||
libc = "0.2.132"
|
||||
uucore = { version=">=0.0.15", package="uucore", path="../../uucore", features=["wide"] }
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
|
||||
nix = "0.25"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
winapi = { version = "0.3", features = ["errhandlingapi", "fileapi", "handleapi", "std", "winbase", "winerror"] }
|
||||
|
||||
|
|
|
@ -10,6 +10,12 @@
|
|||
extern crate libc;
|
||||
|
||||
use clap::{crate_version, Arg, Command};
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use nix::errno::Errno;
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use nix::fcntl::{open, OFlag};
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use nix::sys::stat::Mode;
|
||||
use std::path::Path;
|
||||
use uucore::display::Quotable;
|
||||
use uucore::error::{UResult, USimpleError};
|
||||
|
@ -169,12 +175,45 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
|||
.map(|v| v.map(ToString::to_string).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
if matches.is_present(options::DATA) && files.is_empty() {
|
||||
return Err(USimpleError::new(1, "--data needs at least one argument"));
|
||||
}
|
||||
|
||||
for f in &files {
|
||||
if !Path::new(&f).exists() {
|
||||
return Err(USimpleError::new(
|
||||
1,
|
||||
format!("cannot stat {}: No such file or directory", f.quote()),
|
||||
));
|
||||
// Use the Nix open to be able to set the NONBLOCK flags for fifo files
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
{
|
||||
match open(Path::new(&f), OFlag::O_NONBLOCK, Mode::empty()) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
if e == Errno::ENOENT {
|
||||
return Err(USimpleError::new(
|
||||
1,
|
||||
format!("cannot stat {}: No such file or directory", f.quote()),
|
||||
));
|
||||
}
|
||||
if e == Errno::EACCES {
|
||||
if Path::new(&f).is_dir() {
|
||||
return Err(USimpleError::new(
|
||||
1,
|
||||
format!("error opening {}: Permission denied", f.quote()),
|
||||
));
|
||||
} else {
|
||||
// ignore the issue
|
||||
// ./target/debug/coreutils sync --data file
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
{
|
||||
if !Path::new(&f).exists() {
|
||||
return Err(USimpleError::new(
|
||||
1,
|
||||
format!("cannot stat {}: No such file or directory", f.quote()),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2681,6 +2681,73 @@ fn test_ls_ignore_backups() {
|
|||
.stdout_does_not_contain(".somehiddenbackup~");
|
||||
}
|
||||
|
||||
// This test fails on windows, see details at #3985
|
||||
#[cfg(not(windows))]
|
||||
#[test]
|
||||
fn test_ls_ignore_explicit_period() {
|
||||
// In ls ignore patterns, leading periods must be explicitly specified
|
||||
let scene = TestScenario::new(util_name!());
|
||||
|
||||
let at = &scene.fixtures;
|
||||
at.touch(".hidden.yml");
|
||||
at.touch("regular.yml");
|
||||
|
||||
scene
|
||||
.ucmd()
|
||||
.arg("-a")
|
||||
.arg("--ignore")
|
||||
.arg("?hidden.yml")
|
||||
.succeeds()
|
||||
.stdout_contains(".hidden.yml")
|
||||
.stdout_contains("regular.yml");
|
||||
|
||||
scene
|
||||
.ucmd()
|
||||
.arg("-a")
|
||||
.arg("--ignore")
|
||||
.arg("*.yml")
|
||||
.succeeds()
|
||||
.stdout_contains(".hidden.yml")
|
||||
.stdout_does_not_contain("regular.yml");
|
||||
|
||||
// Leading period is explicitly specified
|
||||
scene
|
||||
.ucmd()
|
||||
.arg("-a")
|
||||
.arg("--ignore")
|
||||
.arg(".*.yml")
|
||||
.succeeds()
|
||||
.stdout_does_not_contain(".hidden.yml")
|
||||
.stdout_contains("regular.yml");
|
||||
}
|
||||
|
||||
// This test fails on windows, see details at #3985
|
||||
#[cfg(not(windows))]
|
||||
#[test]
|
||||
fn test_ls_ignore_negation() {
|
||||
let scene = TestScenario::new(util_name!());
|
||||
|
||||
let at = &scene.fixtures;
|
||||
at.touch("apple");
|
||||
at.touch("boy");
|
||||
|
||||
scene
|
||||
.ucmd()
|
||||
.arg("--ignore")
|
||||
.arg("[!a]*")
|
||||
.succeeds()
|
||||
.stdout_contains("apple")
|
||||
.stdout_does_not_contain("boy");
|
||||
|
||||
scene
|
||||
.ucmd()
|
||||
.arg("--ignore")
|
||||
.arg("[^a]*")
|
||||
.succeeds()
|
||||
.stdout_contains("apple")
|
||||
.stdout_does_not_contain("boy");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ls_directory() {
|
||||
let scene = TestScenario::new(util_name!());
|
||||
|
|
|
@ -701,3 +701,29 @@ fn test_multiple_of_input_chunk() {
|
|||
}
|
||||
assert_eq!(glob.collate(), at.read_bytes(name));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_numeric_suffix() {
|
||||
let (at, mut ucmd) = at_and_ucmd!();
|
||||
ucmd.args(&["-n", "4", "--numeric-suffixes", "9", "threebytes.txt"])
|
||||
.succeeds()
|
||||
.no_stdout()
|
||||
.no_stderr();
|
||||
assert_eq!(at.read("x09"), "a");
|
||||
assert_eq!(at.read("x10"), "b");
|
||||
assert_eq!(at.read("x11"), "c");
|
||||
assert_eq!(at.read("x12"), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hex_suffix() {
|
||||
let (at, mut ucmd) = at_and_ucmd!();
|
||||
ucmd.args(&["-n", "4", "--hex-suffixes", "9", "threebytes.txt"])
|
||||
.succeeds()
|
||||
.no_stdout()
|
||||
.no_stderr();
|
||||
assert_eq!(at.read("x09"), "a");
|
||||
assert_eq!(at.read("x0a"), "b");
|
||||
assert_eq!(at.read("x0b"), "c");
|
||||
assert_eq!(at.read("x0c"), "");
|
||||
}
|
||||
|
|
|
@ -44,3 +44,40 @@ fn test_sync_no_existing_files() {
|
|||
.fails()
|
||||
.stderr_contains("cannot stat");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sync_data_but_not_file() {
|
||||
new_ucmd!()
|
||||
.arg("--data")
|
||||
.fails()
|
||||
.stderr_contains("sync: --data needs at least one argument");
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
#[cfg(feature = "chmod")]
|
||||
#[test]
|
||||
fn test_sync_no_permission_dir() {
|
||||
let ts = TestScenario::new(util_name!());
|
||||
let at = &ts.fixtures;
|
||||
let dir = "foo";
|
||||
at.mkdir_all(dir);
|
||||
|
||||
ts.ccmd("chmod").arg("0").arg(dir).succeeds();
|
||||
let result = ts.ucmd().arg("--data").arg(dir).fails();
|
||||
result.stderr_contains("sync: error opening 'foo': Permission denied");
|
||||
let result = ts.ucmd().arg(dir).fails();
|
||||
result.stderr_contains("sync: error opening 'foo': Permission denied");
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[cfg(feature = "chmod")]
|
||||
#[test]
|
||||
fn test_sync_no_permission_file() {
|
||||
let ts = TestScenario::new(util_name!());
|
||||
let at = &ts.fixtures;
|
||||
let f = "file";
|
||||
at.touch(f);
|
||||
|
||||
ts.ccmd("chmod").arg("0200").arg(f).succeeds();
|
||||
ts.ucmd().arg(f).succeeds();
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -311,4 +311,30 @@ mod tests {
|
|||
);
|
||||
assert!(random_string.as_bytes().ends_with(&[0]));
|
||||
}
|
||||
|
||||
/// Originally used to exclude an error within the `random` module. The two
|
||||
/// affected tests timed out on windows, but only in the ci. These tests are
|
||||
/// also the source for the concrete numbers. The timed out tests are
|
||||
/// `test_tail.rs::test_pipe_when_lines_option_given_input_size_has_multiple_size_of_buffer_size`
|
||||
/// `test_tail.rs::test_pipe_when_bytes_option_given_input_size_has_multiple_size_of_buffer_size`.
|
||||
#[test]
|
||||
fn test_generate_random_strings_when_length_is_around_critical_buffer_sizes() {
|
||||
let length = 8192 * 3;
|
||||
let random_string = RandomString::generate(AlphanumericNewline, length);
|
||||
assert_eq!(length, random_string.len());
|
||||
|
||||
let length = 8192 * 3 + 1;
|
||||
let random_string =
|
||||
RandomString::generate_with_delimiter(&Alphanumeric, b'\n', 100, true, length);
|
||||
assert_eq!(length, random_string.len());
|
||||
assert_eq!(
|
||||
100,
|
||||
random_string
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.filter(|p| **p == b'\n')
|
||||
.count()
|
||||
);
|
||||
assert!(!random_string.as_bytes().ends_with(&[0]));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue