diff --git a/.cargo/config b/.cargo/config index 0a8fd3d00..26008597f 100644 --- a/.cargo/config +++ b/.cargo/config @@ -9,3 +9,12 @@ rustflags = [ "-Wclippy::single_char_pattern", "-Wclippy::explicit_iter_loop", ] + +[build] +# See https://github.com/time-rs/time/issues/293#issuecomment-1005002386. The +# unsoundness here is not in the `time` library, but in the Rust stdlib, and as +# such it needs to be fixed there. +rustflags = "--cfg unsound_local_offset" + +[target.'cfg(target_os = "linux")'] +rustflags = ["--cfg", "unsound_local_offset"] diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ef7519b88..cfcddbb14 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,3 +5,8 @@ updates: schedule: interval: "daily" open-pull-requests-limit: 5 + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 5 diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 967e77b60..079dc1f29 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -24,7 +24,7 @@ jobs: name: Style/cargo-deny runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: EmbarkStudios/cargo-deny-action@v1 style_deps: @@ -43,7 +43,7 @@ jobs: - { os: macos-latest , features: "feat_Tier1,feat_require_unix,feat_require_unix_utmpx" } - { os: windows-latest , features: feat_os_windows } steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v1 - name: Initialize workflow variables id: vars @@ -101,7 +101,7 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v1 - name: Initialize workflow variables id: vars @@ -165,7 +165,7 @@ jobs: - { os: macos-latest , features: feat_os_macos } - { os: windows-latest , features: feat_os_windows } steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v1 - name: Initialize workflow variables id: vars @@ -223,7 +223,7 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v1 - name: Initialize workflow variables id: vars @@ -275,7 +275,7 @@ jobs: # - { os: macos-latest , features: feat_os_macos } # - { os: windows-latest , features: feat_os_windows } steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v1 - name: Initialize workflow variables id: vars @@ -320,7 +320,7 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v1 - name: Initialize workflow variables id: vars @@ -384,9 +384,9 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - args: ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} -p uucore -p coreutils + args: -v ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} -p uucore -p coreutils env: - RUSTFLAGS: "-Awarnings" + RUSTFLAGS: "-Awarnings --cfg unsound_local_offset" deps: name: Dependencies @@ -397,7 +397,7 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v1 - name: Install `rust` toolchain uses: actions-rs/toolchain@v1 @@ -422,7 +422,7 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v1 - name: Install `rust` toolchain uses: actions-rs/toolchain@v1 @@ -452,7 +452,7 @@ jobs: - { os: macos-latest , features: feat_os_macos } - { os: windows-latest , features: feat_os_windows } steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v1 - name: Install `rust` toolchain uses: actions-rs/toolchain@v1 @@ -478,7 +478,7 @@ jobs: - { os: macos-latest , features: feat_os_macos } - { os: windows-latest , features: feat_os_windows } steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v1 - name: Install `rust` toolchain uses: actions-rs/toolchain@v1 @@ -502,7 +502,7 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v1 - name: Install dependencies shell: bash @@ -534,7 +534,7 @@ jobs: --arg size "$SIZE" \ --arg multisize "$SIZEMULTI" \ '{($date): { sha: $sha, size: $size, multisize: $multisize, }}' > size-result.json - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 with: name: size-result path: size-result.json @@ -568,7 +568,7 @@ jobs: - { os: windows-latest , target: x86_64-pc-windows-gnu , features: feat_os_windows } ## note: requires rust >= 1.43.0 to link correctly - { os: windows-latest , target: x86_64-pc-windows-msvc , features: feat_os_windows } steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v1 - name: Initialize workflow variables id: vars @@ -762,7 +762,7 @@ jobs: args: --target=${{ matrix.job.target }} ${{ steps.vars.outputs.CARGO_TEST_OPTIONS}} ${{ matrix.job.cargo-options }} ${{ steps.dep_vars.outputs.CARGO_UTILITY_LIST_OPTIONS }} toolchain: ${{ env.RUST_MIN_SRV }} - name: Archive executable artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: ${{ env.PROJECT_NAME }}-${{ matrix.job.target }} path: target/${{ matrix.job.target }}/release/${{ env.PROJECT_NAME }}${{ steps.vars.outputs.EXE_suffix }} @@ -820,7 +820,7 @@ jobs: job: - { os: ubuntu-latest } steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v1 - name: Install/setup prerequisites shell: bash @@ -857,9 +857,9 @@ jobs: env: TERMUX: v0.118.0 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: AVD cache - uses: actions/cache@v2 + uses: actions/cache@v3 id: avd-cache with: path: | @@ -911,7 +911,7 @@ jobs: env: mem: 2048 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v1 - name: Prepare, build and test ## spell-checker:ignore (ToDO) sshfs usesh vmactions @@ -979,7 +979,7 @@ jobs: - { os: macos-latest , features: macos } - { os: windows-latest , features: windows } steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v1 # - name: Reattach HEAD ## may be needed for accurate code coverage info # run: git checkout ${{ github.head_ref }} @@ -1100,7 +1100,7 @@ jobs: grcov . --output-type lcov --output-path "${COVERAGE_REPORT_FILE}" --branch --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" echo ::set-output name=report::${COVERAGE_REPORT_FILE} - name: Upload coverage results (to Codecov.io) - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 # if: steps.vars.outputs.HAS_CODECOV_TOKEN with: # token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/FixPR.yml b/.github/workflows/FixPR.yml index 2a5382e27..fc38c172a 100644 --- a/.github/workflows/FixPR.yml +++ b/.github/workflows/FixPR.yml @@ -28,7 +28,7 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v1 - name: Initialize job variables id: vars @@ -80,7 +80,7 @@ jobs: ## * using the 'stable' toolchain is necessary to avoid "unexpected '--filter-platform'" errors RUSTUP_TOOLCHAIN=stable cargo-tree tree --locked --all --no-dev-dependencies --no-indent --features ${{ matrix.job.features }} | grep -vE "$PWD" | sort --unique - name: Commit any changes (to '${{ env.BRANCH_TARGET }}') - uses: EndBug/add-and-commit@v7 + uses: EndBug/add-and-commit@v9 with: branch: ${{ env.BRANCH_TARGET }} default_author: github_actions @@ -100,7 +100,7 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v1 - name: Initialize job variables id: vars @@ -130,7 +130,7 @@ jobs: # `cargo fmt` of tests find tests -name "*.rs" -print0 | xargs -0 cargo fmt -- - name: Commit any changes (to '${{ env.BRANCH_TARGET }}') - uses: EndBug/add-and-commit@v7 + uses: EndBug/add-and-commit@v9 with: branch: ${{ env.BRANCH_TARGET }} default_author: github_actions diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index a20ddd8a8..3afaa1723 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -41,11 +41,11 @@ jobs: TEST_FULL_SUMMARY_FILE='gnu-full-result.json' outputs SUITE_LOG_FILE TEST_FILESET_PREFIX TEST_FILESET_SUFFIX TEST_LOGS_GLOB TEST_SUMMARY_FILE TEST_FULL_SUMMARY_FILE - name: Checkout code (uutil) - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: '${{ steps.vars.outputs.path_UUTILS }}' - name: Checkout code (GNU coreutils) - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'coreutils/coreutils' path: '${{ steps.vars.outputs.path_GNU }}' @@ -146,22 +146,22 @@ jobs: # Compress logs before upload (fails otherwise) gzip ${{ steps.vars.outputs.TEST_LOGS_GLOB }} - name: Reserve SHA1/ID of 'test-summary' - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: "${{ steps.summary.outputs.HASH }}" path: "${{ steps.vars.outputs.TEST_SUMMARY_FILE }}" - name: Reserve test results summary - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: test-summary path: "${{ steps.vars.outputs.TEST_SUMMARY_FILE }}" - name: Reserve test logs - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: test-logs path: "${{ steps.vars.outputs.TEST_LOGS_GLOB }}" - name: Upload full json results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: gnu-full-result.json path: ${{ steps.vars.outputs.TEST_FULL_SUMMARY_FILE }} @@ -174,8 +174,8 @@ jobs: REPO_DEFAULT_BRANCH='${{ steps.vars.outputs.repo_default_branch }}' if test -f "${REF_LOG_FILE}"; then echo "Reference SHA1/ID: $(sha1sum -- "${REF_SUMMARY_FILE}")" - REF_ERROR=$(sed -n "s/^FAIL: \([[:print:]]\+\).*/\1/p" "${REF_LOG_FILE}" | sort) - NEW_ERROR=$(sed -n "s/^FAIL: \([[:print:]]\+\).*/\1/p" '${{ steps.vars.outputs.path_GNU_tests }}/test-suite.log' | sort) + REF_ERROR=$(sed -n "s/^ERROR: \([[:print:]]\+\).*/\1/p" "${REF_LOG_FILE}" | sort) + NEW_ERROR=$(sed -n "s/^ERROR: \([[:print:]]\+\).*/\1/p" '${{ steps.vars.outputs.path_GNU_tests }}/test-suite.log' | sort) REF_FAILING=$(sed -n "s/^FAIL: \([[:print:]]\+\).*/\1/p" "${REF_LOG_FILE}" | sort) NEW_FAILING=$(sed -n "s/^FAIL: \([[:print:]]\+\).*/\1/p" '${{ steps.vars.outputs.path_GNU_tests }}/test-suite.log' | sort) for LINE in ${REF_FAILING} @@ -229,11 +229,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code uutil - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: 'uutils' - name: Checkout GNU coreutils - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'coreutils/coreutils' path: 'gnu' @@ -293,7 +293,7 @@ jobs: grcov . --output-type lcov --output-path "${COVERAGE_REPORT_FILE}" --branch --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" echo ::set-output name=report::${COVERAGE_REPORT_FILE} - name: Upload coverage results (to Codecov.io) - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v3 with: file: ${{ steps.coverage.outputs.report }} flags: gnutests diff --git a/Cargo.lock b/Cargo.lock index ee06b62eb..537e34e26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -224,7 +224,7 @@ dependencies = [ "libc", "num-integer", "num-traits", - "time", + "time 0.1.44", "winapi 0.3.9", ] @@ -273,9 +273,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "3.1.3" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7ca9141e27e6ebc52e3c378b0c07f3cea52db46ed1cc5861735fb697b56356" +checksum = "da92e6facd8d73c22745a5d3cbb59bdf8e46e3235c923e516527d8e81eec14a4" dependencies = [ "clap 3.1.15", ] @@ -335,7 +335,7 @@ dependencies = [ "sha1", "tempfile", "textwrap 0.15.0", - "time", + "time 0.3.9", "unindent", "unix_socket", "users", @@ -871,7 +871,7 @@ checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" dependencies = [ "cfg-if 1.0.0", "libc", - "wasi 0.10.2+wasi-snapshot-preview1", + "wasi 0.10.0+wasi-snapshot-preview1", ] [[package]] @@ -985,6 +985,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + [[package]] name = "keccak" version = "0.1.0" @@ -1196,9 +1202,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", ] @@ -1213,6 +1219,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aba1801fb138d8e85e11d0fc70baf4fe1cdfffda7c6cd34a854905df588e5ed0" +dependencies = [ + "libc", +] + [[package]] name = "number_prefix" version = "0.4.0" @@ -1944,14 +1959,33 @@ dependencies = [ [[package]] name = "time" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ "libc", + "wasi 0.10.0+wasi-snapshot-preview1", "winapi 0.3.9", ] +[[package]] +name = "time" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" +dependencies = [ + "itoa", + "libc", + "num_threads", + "time-macros", +] + +[[package]] +name = "time-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" + [[package]] name = "typenum" version = "1.15.0" @@ -1993,9 +2027,9 @@ checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" [[package]] name = "unindent" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514672a55d7380da379785a4d70ca8386c8883ff7eaae877be4d2081cebe73d8" +checksum = "52fee519a3e570f7df377a06a1a7775cdbfb7aa460be7e08de2b1f0e69973a44" [[package]] name = "unix_socket" @@ -2400,6 +2434,7 @@ dependencies = [ "file_diff", "filetime", "libc", + "time 0.3.9", "uucore", ] @@ -2875,7 +2910,7 @@ version = "0.0.13" dependencies = [ "clap 3.1.15", "filetime", - "time", + "time 0.3.9", "uucore", "winapi 0.3.9", ] @@ -3042,7 +3077,7 @@ dependencies = [ "once_cell", "os_display", "thiserror", - "time", + "time 0.3.9", "uucore_procs", "walkdir", "wild", @@ -3090,9 +3125,9 @@ dependencies = [ [[package]] name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" +version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[package]] name = "wasi" @@ -3208,9 +3243,9 @@ checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" [[package]] name = "xattr" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c" +checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" dependencies = [ "libc", ] diff --git a/Cargo.toml b/Cargo.toml index 1e6bef627..d9e942b21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -391,7 +391,7 @@ rand = "0.8" regex = "1.5" sha1 = { version="0.10", features=["std"] } tempfile = "3" -time = "0.1" +time = {version="0.3", features=["local-offset"]} unindent = "0.1" uucore = { version=">=0.0.11", package="uucore", path="src/uucore", features=["entries", "process"] } walkdir = "2.2" diff --git a/deny.toml b/deny.toml index 04c640822..8154bbf90 100644 --- a/deny.toml +++ b/deny.toml @@ -11,6 +11,8 @@ unmaintained = "warn" yanked = "warn" notice = "warn" ignore = [ + "RUSTSEC-2020-0159", + "RUSTSEC-2020-0071", #"RUSTSEC-0000-0000", ] @@ -62,7 +64,7 @@ highlight = "all" # spell-checker: disable skip = [ # getrandom - { name = "wasi", version="0.10.2+wasi-snapshot-preview1" }, + { name = "wasi", version="0.10.0+wasi-snapshot-preview1" }, # blake2d_simd { name = "arrayvec", version = "=0.7.2" }, # flimit/unix_socket @@ -84,6 +86,8 @@ skip = [ { name = "memchr", version = "=1.0.2" }, { name = "quote", version = "=0.3.15" }, { name = "unicode-xid", version = "=0.0.4" }, + # chrono + { name = "time", version = "=0.1.44" }, ] # spell-checker: enable diff --git a/src/uu/cp/Cargo.toml b/src/uu/cp/Cargo.toml index f9036101a..3f3c2e317 100644 --- a/src/uu/cp/Cargo.toml +++ b/src/uu/cp/Cargo.toml @@ -34,7 +34,7 @@ ioctl-sys = "0.8" winapi = { version="0.3", features=["fileapi"] } [target.'cfg(unix)'.dependencies] -xattr="0.2.1" +xattr="0.2.3" exacl= { version = "0.8.0", optional=true } [[bin]] diff --git a/src/uu/df/src/blocks.rs b/src/uu/df/src/blocks.rs index f49650472..594c18acc 100644 --- a/src/uu/df/src/blocks.rs +++ b/src/uu/df/src/blocks.rs @@ -5,9 +5,12 @@ //! Types for representing and displaying block sizes. use crate::OPT_BLOCKSIZE; use clap::ArgMatches; -use std::fmt; +use std::{env, fmt}; -use uucore::parse_size::{parse_size, ParseSizeError}; +use uucore::{ + display::Quotable, + parse_size::{parse_size, ParseSizeError}, +}; /// The first ten powers of 1024. const IEC_BASES: [u128; 10] = [ @@ -73,7 +76,7 @@ fn to_magnitude_and_suffix_1024(n: u128) -> Result { Err(()) } -/// Convert a number, except multiples of 1024, into a string like "12kB" or "34MB". +/// Convert a number into a string like "12kB" or "34MB". /// /// Powers of 1000 become "1kB", "1MB", "1GB", etc. /// @@ -110,7 +113,7 @@ fn to_magnitude_and_suffix_not_powers_of_1024(n: u128) -> Result { /// /// If the number is too large to represent. fn to_magnitude_and_suffix(n: u128) -> Result { - if n % 1024 == 0 { + if n % 1024 == 0 && n % 1000 != 0 { to_magnitude_and_suffix_1024(n) } else { to_magnitude_and_suffix_not_powers_of_1024(n) @@ -159,6 +162,7 @@ pub(crate) enum HumanReadable { /// size. /// /// The default variant is `Bytes(1024)`. +#[derive(Debug, PartialEq)] pub(crate) enum BlockSize { /// A fixed number of bytes. /// @@ -166,16 +170,35 @@ pub(crate) enum BlockSize { Bytes(u64), } +impl BlockSize { + /// Returns the associated value + pub(crate) fn as_u64(&self) -> u64 { + match *self { + Self::Bytes(n) => n, + } + } +} + impl Default for BlockSize { fn default() -> Self { - Self::Bytes(1024) + if env::var("POSIXLY_CORRECT").is_ok() { + Self::Bytes(512) + } else { + Self::Bytes(1024) + } } } pub(crate) fn block_size_from_matches(matches: &ArgMatches) -> Result { if matches.is_present(OPT_BLOCKSIZE) { let s = matches.value_of(OPT_BLOCKSIZE).unwrap(); - Ok(BlockSize::Bytes(parse_size(s)?)) + let bytes = parse_size(s)?; + + if bytes > 0 { + Ok(BlockSize::Bytes(bytes)) + } else { + Err(ParseSizeError::ParseFailure(format!("{}", s.quote()))) + } } else { Ok(Default::default()) } @@ -195,6 +218,8 @@ impl fmt::Display for BlockSize { #[cfg(test)] mod tests { + use std::env; + use crate::blocks::{to_magnitude_and_suffix, BlockSize}; #[test] @@ -239,10 +264,25 @@ mod tests { assert_eq!(to_magnitude_and_suffix(1_000_000_001).unwrap(), "1.1GB"); } + #[test] + fn test_to_magnitude_and_suffix_multiples_of_1000_and_1024() { + assert_eq!(to_magnitude_and_suffix(128_000).unwrap(), "128kB"); + assert_eq!(to_magnitude_and_suffix(1000 * 1024).unwrap(), "1.1MB"); + assert_eq!(to_magnitude_and_suffix(1_000_000_000_000).unwrap(), "1TB"); + } + #[test] fn test_block_size_display() { assert_eq!(format!("{}", BlockSize::Bytes(1024)), "1K"); assert_eq!(format!("{}", BlockSize::Bytes(2 * 1024)), "2K"); assert_eq!(format!("{}", BlockSize::Bytes(3 * 1024 * 1024)), "3M"); } + + #[test] + fn test_default_block_size() { + assert_eq!(BlockSize::Bytes(1024), BlockSize::default()); + env::set_var("POSIXLY_CORRECT", "1"); + assert_eq!(BlockSize::Bytes(512), BlockSize::default()); + env::remove_var("POSIXLY_CORRECT"); + } } diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index 344314198..a11dcf7a0 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -12,6 +12,7 @@ mod filesystem; mod table; use blocks::{HumanReadable, SizeFormat}; +use table::HeaderMode; use uucore::display::Quotable; use uucore::error::{UError, UResult, USimpleError}; use uucore::fsext::{read_fs_list, MountInfo}; @@ -32,6 +33,13 @@ use crate::table::Table; static ABOUT: &str = "Show information about the file system on which each FILE resides,\n\ or all file systems by default."; const USAGE: &str = "{} [OPTION]... [FILE]..."; +const LONG_HELP: &str = "Display values are in units of the first available SIZE from --block-size, +and the DF_BLOCK_SIZE, BLOCK_SIZE and BLOCKSIZE environment variables. +Otherwise, units default to 1024 bytes (or 512 if POSIXLY_CORRECT is set). + +SIZE is an integer and optional unit (example: 10M is 10*1024*1024). +Units are K, M, G, T, P, E, Z, Y (powers of 1024) or KB, MB,... (powers +of 1000)."; static OPT_HELP: &str = "help"; static OPT_ALL: &str = "all"; @@ -65,6 +73,7 @@ struct Options { show_all_fs: bool, size_format: SizeFormat, block_size: BlockSize, + header_mode: HeaderMode, /// Optional list of filesystem types to include in the output table. /// @@ -92,6 +101,7 @@ impl Default for Options { show_all_fs: Default::default(), block_size: Default::default(), size_format: Default::default(), + header_mode: Default::default(), include: Default::default(), exclude: Default::default(), show_total: Default::default(), @@ -169,6 +179,21 @@ impl Options { ), ParseSizeError::ParseFailure(s) => OptionsError::InvalidBlockSize(s), })?, + header_mode: { + if matches.is_present(OPT_HUMAN_READABLE_BINARY) + || matches.is_present(OPT_HUMAN_READABLE_DECIMAL) + { + HeaderMode::HumanReadable + } else if matches.is_present(OPT_PORTABILITY) { + HeaderMode::PosixPortability + // is_present() doesn't work here, it always returns true because OPT_OUTPUT has + // default values and hence is always present + } else if matches.occurrences_of(OPT_OUTPUT) > 0 { + HeaderMode::Output + } else { + HeaderMode::Default + } + }, size_format: { if matches.is_present(OPT_HUMAN_READABLE_BINARY) { SizeFormat::HumanReadable(HumanReadable::Binary) @@ -427,6 +452,7 @@ pub fn uu_app<'a>() -> Command<'a> { .version(crate_version!()) .about(ABOUT) .override_usage(format_usage(USAGE)) + .after_help(LONG_HELP) .infer_long_args(true) .arg( Arg::new(OPT_HELP) @@ -445,6 +471,7 @@ pub fn uu_app<'a>() -> Command<'a> { .short('B') .long("block-size") .takes_value(true) + .value_name("SIZE") .overrides_with_all(&[OPT_KILO, OPT_BLOCKSIZE]) .help( "scale sizes by SIZE before printing them; e.g.\ @@ -501,6 +528,7 @@ pub fn uu_app<'a>() -> Command<'a> { Arg::new(OPT_OUTPUT) .long("output") .takes_value(true) + .value_name("FIELD_LIST") .min_values(0) .require_equals(true) .use_value_delimiter(true) @@ -510,7 +538,7 @@ pub fn uu_app<'a>() -> Command<'a> { .default_values(&["source", "size", "used", "avail", "pcent", "target"]) .conflicts_with_all(&[OPT_INODES, OPT_PORTABILITY, OPT_PRINT_TYPE]) .help( - "use the output format defined by FIELD_LIST,\ + "use the output format defined by FIELD_LIST, \ or print all fields if FIELD_LIST is omitted.", ), ) @@ -533,6 +561,7 @@ pub fn uu_app<'a>() -> Command<'a> { .long("type") .allow_invalid_utf8(true) .takes_value(true) + .value_name("TYPE") .multiple_occurrences(true) .help("limit listing to file systems of type TYPE"), ) @@ -549,6 +578,7 @@ pub fn uu_app<'a>() -> Command<'a> { .long("exclude-type") .allow_invalid_utf8(true) .takes_value(true) + .value_name("TYPE") .use_value_delimiter(true) .multiple_occurrences(true) .help("limit listing to file systems not of type TYPE"), diff --git a/src/uu/df/src/table.rs b/src/uu/df/src/table.rs index 64c5033ed..a9a37cfeb 100644 --- a/src/uu/df/src/table.rs +++ b/src/uu/df/src/table.rs @@ -289,6 +289,23 @@ impl<'a> RowFormatter<'a> { } } +/// A HeaderMode defines what header labels should be shown. +pub(crate) enum HeaderMode { + Default, + // the user used -h or -H + HumanReadable, + // the user used -P + PosixPortability, + // the user used --output + Output, +} + +impl Default for HeaderMode { + fn default() -> Self { + Self::Default + } +} + /// The data of the header row. struct Header {} @@ -302,15 +319,22 @@ impl Header { for column in &options.columns { let header = match column { Column::Source => String::from("Filesystem"), - Column::Size => match options.size_format { - SizeFormat::HumanReadable(_) => String::from("Size"), - SizeFormat::StaticBlockSize => { - format!("{}-blocks", options.block_size) + Column::Size => match options.header_mode { + HeaderMode::HumanReadable => String::from("Size"), + HeaderMode::PosixPortability => { + format!("{}-blocks", options.block_size.as_u64()) } + _ => format!("{}-blocks", options.block_size), }, Column::Used => String::from("Used"), - Column::Avail => String::from("Available"), - Column::Pcent => String::from("Use%"), + Column::Avail => match options.header_mode { + HeaderMode::HumanReadable | HeaderMode::Output => String::from("Avail"), + _ => String::from("Available"), + }, + Column::Pcent => match options.header_mode { + HeaderMode::PosixPortability => String::from("Capacity"), + _ => String::from("Use%"), + }, Column::Target => String::from("Mounted on"), Column::Itotal => String::from("Inodes"), Column::Iused => String::from("IUsed"), @@ -428,7 +452,7 @@ mod tests { use crate::blocks::{HumanReadable, SizeFormat}; use crate::columns::Column; - use crate::table::{Header, Row, RowFormatter}; + use crate::table::{Header, HeaderMode, Row, RowFormatter}; use crate::{BlockSize, Options}; const COLUMNS_WITH_FS_TYPE: [Column; 7] = [ @@ -449,6 +473,30 @@ mod tests { Column::Target, ]; + impl Default for Row { + fn default() -> Self { + Self { + file: Some("/path/to/file".to_string()), + fs_device: "my_device".to_string(), + fs_type: "my_type".to_string(), + fs_mount: "my_mount".to_string(), + + bytes: 100, + bytes_used: 25, + bytes_avail: 75, + bytes_usage: Some(0.25), + + #[cfg(target_os = "macos")] + bytes_capacity: Some(0.5), + + inodes: 10, + inodes_used: 2, + inodes_free: 8, + inodes_usage: Some(0.2), + } + } + } + #[test] fn test_default_header() { let options = Default::default(); @@ -524,37 +572,49 @@ mod tests { } #[test] - fn test_header_with_human_readable_binary() { + fn test_human_readable_header() { let options = Options { - size_format: SizeFormat::HumanReadable(HumanReadable::Binary), + header_mode: HeaderMode::HumanReadable, + ..Default::default() + }; + assert_eq!( + Header::get_headers(&options), + vec!("Filesystem", "Size", "Used", "Avail", "Use%", "Mounted on") + ); + } + + #[test] + fn test_posix_portability_header() { + let options = Options { + header_mode: HeaderMode::PosixPortability, ..Default::default() }; assert_eq!( Header::get_headers(&options), vec!( "Filesystem", - "Size", + "1024-blocks", "Used", "Available", - "Use%", + "Capacity", "Mounted on" ) ); } #[test] - fn test_header_with_human_readable_si() { + fn test_output_header() { let options = Options { - size_format: SizeFormat::HumanReadable(HumanReadable::Decimal), + header_mode: HeaderMode::Output, ..Default::default() }; assert_eq!( Header::get_headers(&options), vec!( "Filesystem", - "Size", + "1K-blocks", "Used", - "Available", + "Avail", "Use%", "Mounted on" ) @@ -568,9 +628,7 @@ mod tests { ..Default::default() }; let row = Row { - file: Some("/path/to/file".to_string()), fs_device: "my_device".to_string(), - fs_type: "my_type".to_string(), fs_mount: "my_mount".to_string(), bytes: 100, @@ -578,13 +636,7 @@ mod tests { bytes_avail: 75, bytes_usage: Some(0.25), - #[cfg(target_os = "macos")] - bytes_capacity: Some(0.5), - - inodes: 10, - inodes_used: 2, - inodes_free: 8, - inodes_usage: Some(0.2), + ..Default::default() }; let fmt = RowFormatter::new(&row, &options); assert_eq!( @@ -601,7 +653,6 @@ mod tests { ..Default::default() }; let row = Row { - file: Some("/path/to/file".to_string()), fs_device: "my_device".to_string(), fs_type: "my_type".to_string(), fs_mount: "my_mount".to_string(), @@ -611,13 +662,7 @@ mod tests { bytes_avail: 75, bytes_usage: Some(0.25), - #[cfg(target_os = "macos")] - bytes_capacity: Some(0.5), - - inodes: 10, - inodes_used: 2, - inodes_free: 8, - inodes_usage: Some(0.2), + ..Default::default() }; let fmt = RowFormatter::new(&row, &options); assert_eq!( @@ -634,23 +679,15 @@ mod tests { ..Default::default() }; let row = Row { - file: Some("/path/to/file".to_string()), fs_device: "my_device".to_string(), - fs_type: "my_type".to_string(), fs_mount: "my_mount".to_string(), - bytes: 100, - bytes_used: 25, - bytes_avail: 75, - bytes_usage: Some(0.25), - - #[cfg(target_os = "macos")] - bytes_capacity: Some(0.5), - inodes: 10, inodes_used: 2, inodes_free: 8, inodes_usage: Some(0.2), + + ..Default::default() }; let fmt = RowFormatter::new(&row, &options); assert_eq!( @@ -667,23 +704,9 @@ mod tests { ..Default::default() }; let row = Row { - file: Some("/path/to/file".to_string()), - fs_device: "my_device".to_string(), - fs_type: "my_type".to_string(), - fs_mount: "my_mount".to_string(), - bytes: 100, - bytes_used: 25, - bytes_avail: 75, - bytes_usage: Some(0.25), - - #[cfg(target_os = "macos")] - bytes_capacity: Some(0.5), - inodes: 10, - inodes_used: 2, - inodes_free: 8, - inodes_usage: Some(0.2), + ..Default::default() }; let fmt = RowFormatter::new(&row, &options); assert_eq!(fmt.get_values(), vec!("1", "10")); @@ -697,7 +720,6 @@ mod tests { ..Default::default() }; let row = Row { - file: Some("/path/to/file".to_string()), fs_device: "my_device".to_string(), fs_type: "my_type".to_string(), fs_mount: "my_mount".to_string(), @@ -707,13 +729,7 @@ mod tests { bytes_avail: 3000, bytes_usage: Some(0.25), - #[cfg(target_os = "macos")] - bytes_capacity: Some(0.5), - - inodes: 10, - inodes_used: 2, - inodes_free: 8, - inodes_usage: Some(0.2), + ..Default::default() }; let fmt = RowFormatter::new(&row, &options); assert_eq!( @@ -738,7 +754,6 @@ mod tests { ..Default::default() }; let row = Row { - file: Some("/path/to/file".to_string()), fs_device: "my_device".to_string(), fs_type: "my_type".to_string(), fs_mount: "my_mount".to_string(), @@ -748,13 +763,7 @@ mod tests { bytes_avail: 3072, bytes_usage: Some(0.25), - #[cfg(target_os = "macos")] - bytes_capacity: Some(0.5), - - inodes: 10, - inodes_used: 2, - inodes_free: 8, - inodes_usage: Some(0.2), + ..Default::default() }; let fmt = RowFormatter::new(&row, &options); assert_eq!( @@ -774,32 +783,14 @@ mod tests { #[test] fn test_row_formatter_with_round_up_usage() { let options = Options { - block_size: BlockSize::Bytes(1), + columns: vec![Column::Pcent], ..Default::default() }; let row = Row { - file: Some("/path/to/file".to_string()), - fs_device: "my_device".to_string(), - fs_type: "my_type".to_string(), - fs_mount: "my_mount".to_string(), - - bytes: 100, - bytes_used: 25, - bytes_avail: 75, bytes_usage: Some(0.251), - - #[cfg(target_os = "macos")] - bytes_capacity: Some(0.5), - - inodes: 10, - inodes_used: 2, - inodes_free: 8, - inodes_usage: Some(0.2), + ..Default::default() }; let fmt = RowFormatter::new(&row, &options); - assert_eq!( - fmt.get_values(), - vec!("my_device", "100", "25", "75", "26%", "my_mount") - ); + assert_eq!(fmt.get_values(), vec!("26%")); } } diff --git a/src/uu/expr/Cargo.toml b/src/uu/expr/Cargo.toml index c4a1bae4a..9ea8008af 100644 --- a/src/uu/expr/Cargo.toml +++ b/src/uu/expr/Cargo.toml @@ -17,7 +17,7 @@ path = "src/expr.rs" [dependencies] clap = { version = "3.1", features = ["wrap_help", "cargo"] } num-bigint = "0.4.0" -num-traits = "0.2.14" +num-traits = "0.2.15" onig = { version = "~6.3", default-features = false } uucore = { version=">=0.0.11", package="uucore", path="../../uucore" } diff --git a/src/uu/factor/Cargo.toml b/src/uu/factor/Cargo.toml index e1620ed90..20a21ac00 100644 --- a/src/uu/factor/Cargo.toml +++ b/src/uu/factor/Cargo.toml @@ -12,12 +12,12 @@ categories = ["command-line-utilities"] edition = "2021" [build-dependencies] -num-traits = "0.2.13" # used in src/numerics.rs, which is included by build.rs +num-traits = "0.2.15" # used in src/numerics.rs, which is included by build.rs [dependencies] clap = { version = "3.1", features = ["wrap_help", "cargo"] } coz = { version = "0.1.3", optional = true } -num-traits = "0.2.13" # Needs at least version 0.2.13 for "OverflowingAdd" +num-traits = "0.2.15" # Needs at least version 0.2.15 for "OverflowingAdd" rand = { version = "0.8", features = ["small_rng"] } smallvec = "1.7" # TODO(nicoo): Use `union` feature, requires Rust 1.49 or later. uucore = { version = ">=0.0.8", package = "uucore", path = "../../uucore" } diff --git a/src/uu/install/Cargo.toml b/src/uu/install/Cargo.toml index 009005a4c..72be5b70f 100644 --- a/src/uu/install/Cargo.toml +++ b/src/uu/install/Cargo.toml @@ -24,6 +24,9 @@ file_diff = "1.0.0" libc = ">= 0.2" uucore = { version=">=0.0.11", package="uucore", path="../../uucore", features=["fs", "mode", "perms", "entries"] } +[dev-dependencies] +time = "0.3" + [[bin]] name = "install" path = "src/main.rs" diff --git a/src/uu/mktemp/src/mktemp.rs b/src/uu/mktemp/src/mktemp.rs index f999d6675..94def4874 100644 --- a/src/uu/mktemp/src/mktemp.rs +++ b/src/uu/mktemp/src/mktemp.rs @@ -41,7 +41,12 @@ enum MkTempError { PersistError(PathBuf), MustEndInX(String), TooFewXs(String), - ContainsDirSeparator(String), + + /// The template prefix contains a path separator (e.g. `"a/bXXX"`). + PrefixContainsDirSeparator(String), + + /// The template suffix contains a path separator (e.g. `"XXXa/b"`). + SuffixContainsDirSeparator(String), InvalidTemplate(String), } @@ -56,7 +61,14 @@ impl Display for MkTempError { PersistError(p) => write!(f, "could not persist file {}", p.quote()), MustEndInX(s) => write!(f, "with --suffix, template {} must end in X", s.quote()), TooFewXs(s) => write!(f, "too few X's in template {}", s.quote()), - ContainsDirSeparator(s) => { + PrefixContainsDirSeparator(s) => { + write!( + f, + "invalid template, {}, contains directory separator", + s.quote() + ) + } + SuffixContainsDirSeparator(s) => { write!( f, "invalid suffix {}, contains directory separator", @@ -205,20 +217,41 @@ pub fn uu_app<'a>() -> Command<'a> { ) } +/// Parse a template string into prefix, suffix, and random components. +/// +/// `temp` is the template string, with three or more consecutive `X`s +/// representing a placeholder for randomly generated characters (for +/// example, `"abc_XXX.txt"`). If `temp` ends in an `X`, then a suffix +/// can be specified by `suffix` instead. +/// +/// # Errors +/// +/// * If there are fewer than three consecutive `X`s in `temp`. +/// * If `suffix` is a [`Some`] object but `temp` does not end in `X`. +/// * If the suffix (specified either way) contains a path separator. +/// +/// # Examples +/// +/// ```rust,ignore +/// assert_eq!(parse_template("XXX", None).unwrap(), ("", 3, "")); +/// assert_eq!(parse_template("abcXXX", None).unwrap(), ("abc", 3, "")); +/// assert_eq!(parse_template("XXXdef", None).unwrap(), ("", 3, "def")); +/// assert_eq!(parse_template("abcXXXdef", None).unwrap(), ("abc", 3, "def")); +/// ``` fn parse_template<'a>( temp: &'a str, suffix: Option<&'a str>, -) -> UResult<(&'a str, usize, &'a str)> { +) -> Result<(&'a str, usize, &'a str), MkTempError> { let right = match temp.rfind('X') { Some(r) => r + 1, - None => return Err(MkTempError::TooFewXs(temp.into()).into()), + None => return Err(MkTempError::TooFewXs(temp.into())), }; let left = temp[..right].rfind(|c| c != 'X').map_or(0, |i| i + 1); let prefix = &temp[..left]; let rand = right - left; if rand < 3 { - return Err(MkTempError::TooFewXs(temp.into()).into()); + return Err(MkTempError::TooFewXs(temp.into())); } let mut suf = &temp[right..]; @@ -227,12 +260,16 @@ fn parse_template<'a>( if suf.is_empty() { suf = s; } else { - return Err(MkTempError::MustEndInX(temp.into()).into()); + return Err(MkTempError::MustEndInX(temp.into())); } }; + if prefix.chars().any(is_separator) { + return Err(MkTempError::PrefixContainsDirSeparator(temp.into())); + } + if suf.chars().any(is_separator) { - return Err(MkTempError::ContainsDirSeparator(suf.into()).into()); + return Err(MkTempError::SuffixContainsDirSeparator(suf.into())); } Ok((prefix, rand, suf)) @@ -304,3 +341,37 @@ fn exec(dir: &Path, prefix: &str, rand: usize, suffix: &str, make_dir: bool) -> println_verbatim(path).map_err_context(|| "failed to print directory name".to_owned()) } + +#[cfg(test)] +mod tests { + use crate::parse_template; + + #[test] + fn test_parse_template_no_suffix() { + assert_eq!(parse_template("XXX", None).unwrap(), ("", 3, "")); + assert_eq!(parse_template("abcXXX", None).unwrap(), ("abc", 3, "")); + assert_eq!(parse_template("XXXdef", None).unwrap(), ("", 3, "def")); + assert_eq!( + parse_template("abcXXXdef", None).unwrap(), + ("abc", 3, "def") + ); + } + + #[test] + fn test_parse_template_suffix() { + assert_eq!(parse_template("XXX", Some("def")).unwrap(), ("", 3, "def")); + assert_eq!( + parse_template("abcXXX", Some("def")).unwrap(), + ("abc", 3, "def") + ); + } + + #[test] + fn test_parse_template_errors() { + assert!(parse_template("a/bXXX", None).is_err()); + assert!(parse_template("XXXa/b", None).is_err()); + assert!(parse_template("XX", None).is_err()); + assert!(parse_template("XXXabc", Some("def")).is_err()); + assert!(parse_template("XXX", Some("a/b")).is_err()); + } +} diff --git a/src/uu/pinky/src/pinky.rs b/src/uu/pinky/src/pinky.rs index 5029caa24..4a793944f 100644 --- a/src/uu/pinky/src/pinky.rs +++ b/src/uu/pinky/src/pinky.rs @@ -221,10 +221,10 @@ impl Capitalize for str { fn idle_string(when: i64) -> String { thread_local! { - static NOW: time::Tm = time::now() + static NOW: time::OffsetDateTime = time::OffsetDateTime::now_local().unwrap(); } NOW.with(|n| { - let duration = n.to_timespec().sec - when; + let duration = n.unix_timestamp() - when; if duration < 60 { // less than 1min " ".to_owned() @@ -242,7 +242,11 @@ fn idle_string(when: i64) -> String { } fn time_string(ut: &Utmpx) -> String { - time::strftime("%b %e %H:%M", &ut.login_time()).unwrap() // LC_ALL=C + // "%b %e %H:%M" + let time_format: Vec = + time::format_description::parse("[month repr:short] [day padding:space] [hour]:[minute]") + .unwrap(); + ut.login_time().format(&time_format).unwrap() // LC_ALL=C } fn gecos_to_fullname(pw: &Passwd) -> Option { diff --git a/src/uu/seq/Cargo.toml b/src/uu/seq/Cargo.toml index ad8bba5b6..67226093d 100644 --- a/src/uu/seq/Cargo.toml +++ b/src/uu/seq/Cargo.toml @@ -19,7 +19,7 @@ path = "src/seq.rs" bigdecimal = "0.3" clap = { version = "3.1", features = ["wrap_help", "cargo"] } num-bigint = "0.4.0" -num-traits = "0.2.14" +num-traits = "0.2.15" uucore = { version=">=0.0.11", package="uucore", path="../../uucore", features=["memo"] } [[bin]] diff --git a/src/uu/touch/Cargo.toml b/src/uu/touch/Cargo.toml index 646b65f50..aa747ae78 100644 --- a/src/uu/touch/Cargo.toml +++ b/src/uu/touch/Cargo.toml @@ -17,7 +17,7 @@ path = "src/touch.rs" [dependencies] filetime = "0.2.1" clap = { version = "3.1", features = ["wrap_help", "cargo"] } -time = "0.1.40" +time = { version = "0.3", features = ["parsing", "formatting", "local-offset", "macros"] } uucore = { version=">=0.0.11", package="uucore", path="../../uucore", features=["libc"] } [target.'cfg(target_os = "windows")'.dependencies] diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index ff08a1b59..f71eb81d0 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -6,8 +6,7 @@ // For the full copyright and license information, please view the LICENSE file // that was distributed with this source code. -// spell-checker:ignore (ToDO) filetime strptime utcoff strs datetime MMDDhhmm clapv PWSTR lpszfilepath hresult - +// spell-checker:ignore (ToDO) filetime strptime utcoff strs datetime MMDDhhmm clapv PWSTR lpszfilepath hresult mktime YYYYMMDDHHMM YYMMDDHHMM DATETIME YYYYMMDDHHMMS subsecond pub extern crate filetime; #[macro_use] @@ -17,6 +16,8 @@ use clap::{crate_version, Arg, ArgGroup, Command}; use filetime::*; use std::fs::{self, File}; use std::path::{Path, PathBuf}; +use time::macros::{format_description, offset, time}; +use time::Duration; use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult, USimpleError}; use uucore::format_usage; @@ -41,14 +42,27 @@ pub mod options { static ARG_FILES: &str = "files"; -fn to_local(mut tm: time::Tm) -> time::Tm { - tm.tm_utcoff = time::now().tm_utcoff; - tm +// Convert a date/time to a date with a TZ offset +fn to_local(tm: time::PrimitiveDateTime) -> time::OffsetDateTime { + let offset = match time::OffsetDateTime::now_local() { + Ok(lo) => lo.offset(), + Err(e) => { + panic!("error: {}", e); + } + }; + tm.assume_offset(offset) } -fn local_tm_to_filetime(tm: time::Tm) -> FileTime { - let ts = tm.to_timespec(); - FileTime::from_unix_time(ts.sec as i64, ts.nsec as u32) +// Convert a date/time with a TZ offset into a FileTime +fn local_dt_to_filetime(dt: time::OffsetDateTime) -> FileTime { + FileTime::from_unix_time(dt.unix_timestamp(), dt.nanosecond()) +} + +// Convert a date/time, considering that the input is in UTC time +// Used for touch -d 1970-01-01 18:43:33.023456789 for example +fn dt_to_filename(tm: time::PrimitiveDateTime) -> FileTime { + let dt = tm.assume_offset(offset!(UTC)); + local_dt_to_filetime(dt) } #[uucore::main] @@ -62,7 +76,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { Try 'touch --help' for more information."##, ) })?; - let (mut atime, mut mtime) = if let Some(reference) = matches.value_of_os(options::sources::REFERENCE) { stat(Path::new(reference), !matches.is_present(options::NO_DEREF))? @@ -72,7 +85,7 @@ Try 'touch --help' for more information."##, } else if let Some(current) = matches.value_of(options::sources::CURRENT) { parse_timestamp(current)? } else { - local_tm_to_filetime(time::now()) + local_dt_to_filetime(time::OffsetDateTime::now_local().unwrap()) }; (timestamp, timestamp) }; @@ -248,38 +261,129 @@ fn stat(path: &Path, follow: bool) -> UResult<(FileTime, FileTime)> { )) } -fn parse_date(str: &str) -> UResult { +const POSIX_LOCALE_FORMAT: &[time::format_description::FormatItem] = format_description!( + "[weekday repr:short] [month repr:short] [day padding:space] \ + [hour]:[minute]:[second] [year]" +); + +const ISO_8601_FORMAT: &[time::format_description::FormatItem] = + format_description!("[year]-[month]-[day]"); + +// "%Y%m%d%H%M.%S" 15 chars +const YYYYMMDDHHMM_DOT_SS_FORMAT: &[time::format_description::FormatItem] = format_description!( + "[year repr:full][month repr:numerical padding:zero]\ + [day][hour][minute].[second]" +); + +// "%Y-%m-%d %H:%M:%S.%SS" 12 chars +const YYYYMMDDHHMMSS_FORMAT: &[time::format_description::FormatItem] = format_description!( + "[year repr:full]-[month repr:numerical padding:zero]-\ + [day] [hour]:[minute]:[second].[subsecond]" +); + +// "%Y-%m-%d %H:%M:%S" 12 chars +const YYYYMMDDHHMMS_FORMAT: &[time::format_description::FormatItem] = format_description!( + "[year repr:full]-[month repr:numerical padding:zero]-\ + [day] [hour]:[minute]:[second]" +); + +// "%Y-%m-%d %H:%M" 12 chars +// Used for example in tests/touch/no-rights.sh +const YYYY_MM_DD_HH_MM_FORMAT: &[time::format_description::FormatItem] = format_description!( + "[year repr:full]-[month repr:numerical padding:zero]-\ + [day] [hour]:[minute]" +); + +// "%Y%m%d%H%M" 12 chars +const YYYYMMDDHHMM_FORMAT: &[time::format_description::FormatItem] = format_description!( + "[year repr:full][month repr:numerical padding:zero]\ + [day][hour][minute]" +); + +// "%y%m%d%H%M.%S" 13 chars +const YYMMDDHHMM_DOT_SS_FORMAT: &[time::format_description::FormatItem] = format_description!( + "[year repr:last_two padding:none][month][day]\ + [hour][minute].[second]" +); + +// "%y%m%d%H%M" 10 chars +const YYMMDDHHMM_FORMAT: &[time::format_description::FormatItem] = format_description!( + "[year repr:last_two padding:none][month padding:zero][day padding:zero]\ + [hour repr:24 padding:zero][minute padding:zero]" +); + +// "%Y-%m-%d %H:%M +offset" +// Used for example in tests/touch/relative.sh +const YYYYMMDDHHMM_OFFSET_FORMAT: &[time::format_description::FormatItem] = format_description!( + "[year]-[month]-[day] [hour repr:24]:[minute] \ + [offset_hour sign:mandatory][offset_minute]" +); + +fn parse_date(s: &str) -> UResult { // This isn't actually compatible with GNU touch, but there doesn't seem to // be any simple specification for what format this parameter allows and I'm // not about to implement GNU parse_datetime. // http://git.savannah.gnu.org/gitweb/?p=gnulib.git;a=blob_plain;f=lib/parse-datetime.y - let formats = vec!["%c", "%F"]; - for f in formats { - if let Ok(tm) = time::strptime(str, f) { - return Ok(local_tm_to_filetime(to_local(tm))); + + // TODO: match on char count? + + // "The preferred date and time representation for the current locale." + // "(In the POSIX locale this is equivalent to %a %b %e %H:%M:%S %Y.)" + // time 0.1.43 parsed this as 'a b e T Y' + // which is equivalent to the POSIX locale: %a %b %e %H:%M:%S %Y + // Tue Dec 3 ... + // ("%c", POSIX_LOCALE_FORMAT), + // + if let Ok(parsed) = time::PrimitiveDateTime::parse(s, &POSIX_LOCALE_FORMAT) { + return Ok(local_dt_to_filetime(to_local(parsed))); + } + + // Also support other formats found in the GNU tests like + // in tests/misc/stat-nanoseconds.sh + // or tests/touch/no-rights.sh + for fmt in [ + YYYYMMDDHHMMS_FORMAT, + YYYYMMDDHHMMSS_FORMAT, + YYYY_MM_DD_HH_MM_FORMAT, + YYYYMMDDHHMM_OFFSET_FORMAT, + ] { + if let Ok(parsed) = time::PrimitiveDateTime::parse(s, &fmt) { + return Ok(dt_to_filename(parsed)); } } - if let Ok(tm) = time::strptime(str, "@%s") { - // Don't convert to local time in this case - seconds since epoch are not time-zone dependent - return Ok(local_tm_to_filetime(tm)); + // "Equivalent to %Y-%m-%d (the ISO 8601 date format). (C99)" + // ("%F", ISO_8601_FORMAT), + if let Ok(parsed) = time::Date::parse(s, &ISO_8601_FORMAT) { + return Ok(local_dt_to_filetime(to_local( + time::PrimitiveDateTime::new(parsed, time!(00:00)), + ))); } - Err(USimpleError::new( - 1, - format!("Unable to parse date: {}", str), - )) + // "@%s" is "The number of seconds since the Epoch, 1970-01-01 00:00:00 +0000 (UTC). (TZ) (Calculated from mktime(tm).)" + if s.bytes().next() == Some(b'@') { + if let Ok(ts) = &s[1..].parse::() { + // Don't convert to local time in this case - seconds since epoch are not time-zone dependent + return Ok(local_dt_to_filetime( + time::OffsetDateTime::from_unix_timestamp(*ts).unwrap(), + )); + } + } + + Err(USimpleError::new(1, format!("Unable to parse date: {}", s))) } fn parse_timestamp(s: &str) -> UResult { - let now = time::now(); - let (format, ts) = match s.chars().count() { - 15 => ("%Y%m%d%H%M.%S", s.to_owned()), - 12 => ("%Y%m%d%H%M", s.to_owned()), - 13 => ("%y%m%d%H%M.%S", s.to_owned()), - 10 => ("%y%m%d%H%M", s.to_owned()), - 11 => ("%Y%m%d%H%M.%S", format!("{}{}", now.tm_year + 1900, s)), - 8 => ("%Y%m%d%H%M", format!("{}{}", now.tm_year + 1900, s)), + // TODO: handle error + let now = time::OffsetDateTime::now_utc(); + + let (mut format, mut ts) = match s.chars().count() { + 15 => (YYYYMMDDHHMM_DOT_SS_FORMAT, s.to_owned()), + 12 => (YYYYMMDDHHMM_FORMAT, s.to_owned()), + 13 => (YYMMDDHHMM_DOT_SS_FORMAT, s.to_owned()), + 10 => (YYMMDDHHMM_FORMAT, s.to_owned()), + 11 => (YYYYMMDDHHMM_DOT_SS_FORMAT, format!("{}{}", now.year(), s)), + 8 => (YYYYMMDDHHMM_FORMAT, format!("{}{}", now.year(), s)), _ => { return Err(USimpleError::new( 1, @@ -287,30 +391,53 @@ fn parse_timestamp(s: &str) -> UResult { )) } }; - - let tm = time::strptime(&ts, format) - .map_err(|_| USimpleError::new(1, format!("invalid date format {}", s.quote())))?; - - let mut local = to_local(tm); - local.tm_isdst = -1; - let ft = local_tm_to_filetime(local); - - // We have to check that ft is valid time. Due to daylight saving - // time switch, local time can jump from 1:59 AM to 3:00 AM, - // in which case any time between 2:00 AM and 2:59 AM is not valid. - // Convert back to local time and see if we got the same value back. - let ts = time::Timespec { - sec: ft.unix_seconds(), - nsec: 0, - }; - let tm2 = time::at(ts); - if tm.tm_hour != tm2.tm_hour { - return Err(USimpleError::new( - 1, - format!("invalid date format {}", s.quote()), - )); + // workaround time returning Err(TryFromParsed(InsufficientInformation)) for year w/ + // repr:last_two + // https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=1ccfac7c07c5d1c7887a11decf0e1996 + if s.chars().count() == 10 { + format = YYYYMMDDHHMM_FORMAT; + ts = "20".to_owned() + &ts; + } else if s.chars().count() == 13 { + format = YYYYMMDDHHMM_DOT_SS_FORMAT; + ts = "20".to_owned() + &ts; } + let leap_sec = if (format == YYYYMMDDHHMM_DOT_SS_FORMAT || format == YYMMDDHHMM_DOT_SS_FORMAT) + && ts.ends_with(".60") + { + // Work around to disable leap seconds + // Used in gnu/tests/touch/60-seconds + ts = ts.replace(".60", ".59"); + true + } else { + false + }; + + let tm = time::PrimitiveDateTime::parse(&ts, &format) + .map_err(|_| USimpleError::new(1, format!("invalid date ts format {}", ts.quote())))?; + let mut local = to_local(tm); + if leap_sec { + // We are dealing with a leap second, add it + local = local.saturating_add(Duration::SECOND); + } + let ft = local_dt_to_filetime(local); + + // // We have to check that ft is valid time. Due to daylight saving + // // time switch, local time can jump from 1:59 AM to 3:00 AM, + // // in which case any time between 2:00 AM and 2:59 AM is not valid. + // // Convert back to local time and see if we got the same value back. + // let ts = time::Timespec { + // sec: ft.unix_seconds(), + // nsec: 0, + // }; + // let tm2 = time::at(ts); + // if tm.tm_hour != tm2.tm_hour { + // return Err(USimpleError::new( + // 1, + // format!("invalid date format {}", s.quote()), + // )); + // } + Ok(ft) } diff --git a/src/uu/uptime/src/uptime.rs b/src/uu/uptime/src/uptime.rs index a93344dbc..5c64cb5af 100644 --- a/src/uu/uptime/src/uptime.rs +++ b/src/uu/uptime/src/uptime.rs @@ -111,9 +111,9 @@ fn process_utmpx() -> (Option, usize) { match line.record_type() { USER_PROCESS => nusers += 1, BOOT_TIME => { - let t = line.login_time().to_timespec(); - if t.sec > 0 { - boot_time = Some(t.sec as time_t); + let dt = line.login_time(); + if dt.second() > 0 { + boot_time = Some(dt.second() as time_t); } } _ => continue, diff --git a/src/uu/who/src/who.rs b/src/uu/who/src/who.rs index 6e21ac912..47d96a381 100644 --- a/src/uu/who/src/who.rs +++ b/src/uu/who/src/who.rs @@ -275,10 +275,10 @@ struct Who { fn idle_string<'a>(when: i64, boottime: i64) -> Cow<'a, str> { thread_local! { - static NOW: time::Tm = time::now() + static NOW: time::OffsetDateTime = time::OffsetDateTime::now_local().unwrap(); } NOW.with(|n| { - let now = n.to_timespec().sec; + let now = n.unix_timestamp(); if boottime < when && now - 24 * 3600 < when && when <= now { let seconds_idle = now - when; if seconds_idle < 60 { @@ -298,7 +298,11 @@ fn idle_string<'a>(when: i64, boottime: i64) -> Cow<'a, str> { } fn time_string(ut: &Utmpx) -> String { - time::strftime("%b %e %H:%M", &ut.login_time()).unwrap() // LC_ALL=C + // "%b %e %H:%M" + let time_format: Vec = + time::format_description::parse("[month repr:short] [day padding:space] [hour]:[minute]") + .unwrap(); + ut.login_time().format(&time_format).unwrap() // LC_ALL=C } #[inline] diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 56e582bf8..7a350146e 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -26,7 +26,7 @@ wild = "2.0" # * optional itertools = { version="0.10.0", optional=true } thiserror = { version="1.0", optional=true } -time = { version="<= 0.1.43", optional=true } +time = { version="<= 0.3", optional=true, features = ["formatting", "local-offset", "macros"] } # * "problem" dependencies (pinned) data-encoding = { version="2.1", optional=true } data-encoding-macro = { version="0.1.12", optional=true } @@ -62,6 +62,6 @@ process = ["libc"] ringbuffer = [] signals = [] utf8 = [] -utmpx = ["time", "libc", "dns-lookup"] +utmpx = ["time", "time/macros", "libc", "dns-lookup"] wide = [] pipes = ["nix"] diff --git a/src/uucore/src/lib/features/fsext.rs b/src/uucore/src/lib/features/fsext.rs index 511c1dd2c..787684e93 100644 --- a/src/uucore/src/lib/features/fsext.rs +++ b/src/uucore/src/lib/features/fsext.rs @@ -9,9 +9,11 @@ //! Set of functions to manage file systems -// spell-checker:ignore (arch) bitrig ; (fs) cifs smbfs +// spell-checker:ignore DATETIME subsecond (arch) bitrig ; (fs) cifs smbfs extern crate time; +use time::macros::format_description; +use time::UtcOffset; pub use crate::*; // import macros from `../../macros.rs` @@ -63,7 +65,6 @@ fn LPWSTR2String(buf: &[u16]) -> String { String::from_utf16(&buf[..len]).unwrap() } -use self::time::Timespec; #[cfg(unix)] use libc::{ mode_t, strerror, S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG, S_IFSOCK, @@ -733,11 +734,42 @@ where } } +// match strftime "%Y-%m-%d %H:%M:%S.%f %z" +const PRETTY_DATETIME_FORMAT: &[time::format_description::FormatItem] = format_description!( + "\ +[year]-[month]-[day padding:zero] \ +[hour]:[minute]:[second].[subsecond digits:9] \ +[offset_hour sign:mandatory][offset_minute]" +); + pub fn pretty_time(sec: i64, nsec: i64) -> String { // sec == seconds since UNIX_EPOCH // nsec == nanoseconds since (UNIX_EPOCH + sec) - let tm = time::at(Timespec::new(sec, nsec as i32)); - let res = time::strftime("%Y-%m-%d %H:%M:%S.%f %z", &tm).unwrap(); + let ts_nanos: i128 = (sec * 1_000_000_000 + nsec).into(); + + // Return the date in UTC + let tm = match time::OffsetDateTime::from_unix_timestamp_nanos(ts_nanos) { + Ok(tm) => tm, + Err(e) => { + panic!("error: {}", e); + } + }; + + // Get the offset to convert to local time + // Because of DST (daylight saving), we get the local time back when + // the date was set + let local_offset = match UtcOffset::local_offset_at(tm) { + Ok(lo) => lo, + Err(e) => { + panic!("error: {}", e); + } + }; + + // Include the conversion to local time + let res = tm + .to_offset(local_offset) + .format(&PRETTY_DATETIME_FORMAT) + .unwrap(); if res.ends_with(" -0000") { res.replace(" -0000", " +0000") } else { diff --git a/src/uucore/src/lib/features/utmpx.rs b/src/uucore/src/lib/features/utmpx.rs index 302d03d71..8875e3489 100644 --- a/src/uucore/src/lib/features/utmpx.rs +++ b/src/uucore/src/lib/features/utmpx.rs @@ -32,7 +32,6 @@ //! ``` pub extern crate time; -use self::time::{Timespec, Tm}; use std::ffi::CString; use std::io::Result as IOResult; @@ -189,11 +188,14 @@ impl Utmpx { chars2string!(self.inner.ut_line) } /// A.K.A. ut.ut_tv - pub fn login_time(&self) -> Tm { - time::at(Timespec::new( - self.inner.ut_tv.tv_sec as i64, - self.inner.ut_tv.tv_usec as i32, - )) + pub fn login_time(&self) -> time::OffsetDateTime { + let ts_nanos: i128 = (self.inner.ut_tv.tv_sec as i64 * 1_000_000_000_i64 + + self.inner.ut_tv.tv_usec as i64 * 1_000_i64) + .into(); + let local_offset = time::OffsetDateTime::now_local().unwrap().offset(); + time::OffsetDateTime::from_unix_timestamp_nanos(ts_nanos) + .unwrap() + .to_offset(local_offset) } /// A.K.A. ut.ut_exit /// diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index cdcaf2760..06134d2dd 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -1032,8 +1032,8 @@ fn test_cp_no_deref_folder_to_folder() { #[cfg(target_os = "linux")] fn test_cp_archive() { let (at, mut ucmd) = at_and_ucmd!(); - let ts = time::now().to_timespec(); - let previous = FileTime::from_unix_time(ts.sec as i64 - 3600, ts.nsec as u32); + let ts = time::OffsetDateTime::now_local().unwrap(); + let previous = FileTime::from_unix_time(ts.unix_timestamp() - 3600, ts.nanosecond() as u32); // set the file creation/modification an hour ago filetime::set_file_times( at.plus_as_string(TEST_HELLO_WORLD_SOURCE), @@ -1135,8 +1135,8 @@ fn test_cp_archive_recursive() { #[cfg(any(target_os = "linux", target_os = "android"))] fn test_cp_preserve_timestamps() { let (at, mut ucmd) = at_and_ucmd!(); - let ts = time::now().to_timespec(); - let previous = FileTime::from_unix_time(ts.sec as i64 - 3600, ts.nsec as u32); + let ts = time::OffsetDateTime::now_local().unwrap(); + let previous = FileTime::from_unix_time(ts.unix_timestamp() - 3600, ts.nanosecond()); // set the file creation/modification an hour ago filetime::set_file_times( at.plus_as_string(TEST_HELLO_WORLD_SOURCE), @@ -1168,8 +1168,8 @@ fn test_cp_preserve_timestamps() { #[cfg(any(target_os = "linux", target_os = "android"))] fn test_cp_no_preserve_timestamps() { let (at, mut ucmd) = at_and_ucmd!(); - let ts = time::now().to_timespec(); - let previous = FileTime::from_unix_time(ts.sec as i64 - 3600, ts.nsec as u32); + let ts = time::OffsetDateTime::now_local().unwrap(); + let previous = FileTime::from_unix_time(ts.unix_timestamp() - 3600, ts.nanosecond()); // set the file creation/modification an hour ago filetime::set_file_times( at.plus_as_string(TEST_HELLO_WORLD_SOURCE), diff --git a/tests/by-util/test_df.rs b/tests/by-util/test_df.rs index f03a71a7b..157818d95 100644 --- a/tests/by-util/test_df.rs +++ b/tests/by-util/test_df.rs @@ -73,7 +73,7 @@ fn test_df_output() { "Filesystem", "Size", "Used", - "Available", + "Avail", "Capacity", "Use%", "Mounted", @@ -84,7 +84,7 @@ fn test_df_output() { "Filesystem", "Size", "Used", - "Available", + "Avail", "Use%", "Mounted", "on", @@ -107,7 +107,7 @@ fn test_df_output_overridden() { "Filesystem", "Size", "Used", - "Available", + "Avail", "Capacity", "Use%", "Mounted", @@ -118,7 +118,7 @@ fn test_df_output_overridden() { "Filesystem", "Size", "Used", - "Available", + "Avail", "Use%", "Mounted", "on", @@ -134,6 +134,46 @@ fn test_df_output_overridden() { assert_eq!(actual, expected); } +#[test] +fn test_default_headers() { + let expected = if cfg!(target_os = "macos") { + vec![ + "Filesystem", + "1K-blocks", + "Used", + "Available", + "Capacity", + "Use%", + "Mounted", + "on", + ] + } else { + vec![ + "Filesystem", + "1K-blocks", + "Used", + "Available", + "Use%", + "Mounted", + "on", + ] + }; + let output = new_ucmd!().succeeds().stdout_move_str(); + let actual = output.lines().take(1).collect::>()[0]; + let actual = actual.split_whitespace().collect::>(); + assert_eq!(actual, expected); +} + +#[test] +fn test_precedence_of_human_readable_header_over_output_header() { + let output = new_ucmd!() + .args(&["-H", "--output=size"]) + .succeeds() + .stdout_move_str(); + let header = output.lines().next().unwrap().to_string(); + assert_eq!(header.trim(), "Size"); +} + #[test] fn test_total_option_with_single_dash() { // These should fail because `-total` should have two dashes, @@ -375,6 +415,26 @@ fn test_iuse_percentage() { } } +#[test] +fn test_default_block_size() { + let output = new_ucmd!() + .arg("--output=size") + .succeeds() + .stdout_move_str(); + let header = output.lines().next().unwrap().to_string(); + + assert_eq!(header, "1K-blocks"); + + let output = new_ucmd!() + .arg("--output=size") + .env("POSIXLY_CORRECT", "1") + .succeeds() + .stdout_move_str(); + let header = output.lines().next().unwrap().to_string(); + + assert_eq!(header, "512B-blocks"); +} + #[test] fn test_block_size_1024() { fn get_header(block_size: u64) -> String { @@ -392,6 +452,11 @@ fn test_block_size_1024() { assert_eq!(get_header(2 * 1024 * 1024), "2M-blocks"); assert_eq!(get_header(1024 * 1024 * 1024), "1G-blocks"); assert_eq!(get_header(34 * 1024 * 1024 * 1024), "34G-blocks"); + + // multiples of both 1024 and 1000 + assert_eq!(get_header(128_000), "128kB-blocks"); + assert_eq!(get_header(1000 * 1024), "1.1MB-blocks"); + assert_eq!(get_header(1_000_000_000_000), "1TB-blocks"); } #[test] @@ -418,6 +483,31 @@ fn test_block_size_with_suffix() { assert_eq!(get_header("1GB"), "1GB-blocks"); } +#[test] +fn test_block_size_in_posix_portability_mode() { + fn get_header(block_size: &str) -> String { + let output = new_ucmd!() + .args(&["-P", "-B", block_size]) + .succeeds() + .stdout_move_str(); + output + .lines() + .next() + .unwrap() + .to_string() + .split_whitespace() + .nth(1) + .unwrap() + .to_string() + } + + assert_eq!(get_header("1024"), "1024-blocks"); + assert_eq!(get_header("1K"), "1024-blocks"); + assert_eq!(get_header("1KB"), "1000-blocks"); + assert_eq!(get_header("1M"), "1048576-blocks"); + assert_eq!(get_header("1MB"), "1000000-blocks"); +} + #[test] fn test_too_large_block_size() { fn run_command(size: &str) { @@ -440,6 +530,16 @@ fn test_invalid_block_size() { .arg("--block-size=x") .fails() .stderr_contains("invalid --block-size argument 'x'"); + + new_ucmd!() + .arg("--block-size=0") + .fails() + .stderr_contains("invalid --block-size argument '0'"); + + new_ucmd!() + .arg("--block-size=0K") + .fails() + .stderr_contains("invalid --block-size argument '0K'"); } #[test] diff --git a/tests/by-util/test_mktemp.rs b/tests/by-util/test_mktemp.rs index c28efc37b..9fce0e591 100644 --- a/tests/by-util/test_mktemp.rs +++ b/tests/by-util/test_mktemp.rs @@ -2,6 +2,8 @@ use crate::common::util::*; +use uucore::display::Quotable; + use std::path::PathBuf; use tempfile::tempdir; @@ -482,3 +484,15 @@ fn test_respect_template_directory() { assert_matches_template!(template, filename); assert!(at.file_exists(filename)); } + +/// Test that a template with a path separator is invalid. +#[test] +fn test_template_path_separator() { + new_ucmd!() + .args(&["-t", "a/bXXX"]) + .fails() + .stderr_only(format!( + "mktemp: invalid template, {}, contains directory separator\n", + "a/bXXX".quote() + )); +} diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index c4ec03d95..06f4d5259 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -595,9 +595,9 @@ fn test_mv_update_option() { at.touch(file_a); at.touch(file_b); - let ts = time::now().to_timespec(); - let now = FileTime::from_unix_time(ts.sec as i64, ts.nsec as u32); - let later = FileTime::from_unix_time(ts.sec as i64 + 3600, ts.nsec as u32); + let ts = time::OffsetDateTime::now_local().unwrap(); + let now = FileTime::from_unix_time(ts.unix_timestamp(), ts.nanosecond()); + let later = FileTime::from_unix_time(ts.unix_timestamp() as i64 + 3600, ts.nanosecond() as u32); filetime::set_file_times(at.plus_as_string(file_a), now, now).unwrap(); filetime::set_file_times(at.plus_as_string(file_b), now, later).unwrap(); diff --git a/tests/by-util/test_stat.rs b/tests/by-util/test_stat.rs index c3ffb3be5..14aba96fc 100644 --- a/tests/by-util/test_stat.rs +++ b/tests/by-util/test_stat.rs @@ -283,6 +283,40 @@ fn test_char() { ts.ucmd().args(&args).succeeds().stdout_is(expected_stdout); } +#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))] +#[test] +fn test_date() { + // Just test the date for the time 0.3 change + let args = [ + "-c", + #[cfg(any(target_os = "linux", target_os = "android"))] + "%z", + #[cfg(target_os = "linux")] + "/bin/sh", + #[cfg(any(target_vendor = "apple"))] + "%z", + #[cfg(any(target_os = "android", target_vendor = "apple"))] + "/bin/sh", + ]; + let ts = TestScenario::new(util_name!()); + let expected_stdout = unwrap_or_return!(expected_result(&ts, &args)).stdout_move_str(); + ts.ucmd().args(&args).succeeds().stdout_is(expected_stdout); + // Just test the date for the time 0.3 change + let args = [ + "-c", + #[cfg(any(target_os = "linux", target_os = "android"))] + "%z", + #[cfg(target_os = "linux")] + "/dev/ptmx", + #[cfg(any(target_vendor = "apple"))] + "%z", + #[cfg(any(target_os = "android", target_vendor = "apple"))] + "/dev/ptmx", + ]; + let ts = TestScenario::new(util_name!()); + let expected_stdout = unwrap_or_return!(expected_result(&ts, &args)).stdout_move_str(); + ts.ucmd().args(&args).succeeds().stdout_is(expected_stdout); +} #[cfg(unix)] #[test] fn test_multi_files() { diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index 6e5d656c4..346e27919 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -1,9 +1,16 @@ -// spell-checker:ignore (formats) cymdhm cymdhms mdhm mdhms ymdhm ymdhms +// spell-checker:ignore (formats) cymdhm cymdhms mdhm mdhms ymdhm ymdhms datetime mktime + +// This test relies on +// --cfg unsound_local_offset +// https://github.com/time-rs/time/blob/deb8161b84f355b31e39ce09e40c4d6ce3fea837/src/sys/local_offset_at/unix.rs#L112-L120= +// See https://github.com/time-rs/time/issues/293#issuecomment-946382614= +// Defined in .cargo/config extern crate touch; use self::touch::filetime::{self, FileTime}; extern crate time; +use time::macros::{datetime, format_description}; use crate::common::util::*; use std::fs::remove_file; @@ -32,11 +39,24 @@ fn set_file_times(at: &AtPath, path: &str, atime: FileTime, mtime: FileTime) { // Adjusts for local timezone fn str_to_filetime(format: &str, s: &str) -> FileTime { - let mut tm = time::strptime(s, format).unwrap(); - tm.tm_utcoff = time::now().tm_utcoff; - tm.tm_isdst = -1; // Unknown flag DST - let ts = tm.to_timespec(); - FileTime::from_unix_time(ts.sec as i64, ts.nsec as u32) + let format_description = match format { + "%y%m%d%H%M" => format_description!("[year repr:last_two][month][day][hour][minute]"), + "%y%m%d%H%M.%S" => { + format_description!("[year repr:last_two][month][day][hour][minute].[second]") + } + "%Y%m%d%H%M" => format_description!("[year][month][day][hour][minute]"), + "%Y%m%d%H%M.%S" => format_description!("[year][month][day][hour][minute].[second]"), + _ => panic!("unexpected dt format"), + }; + let tm = time::PrimitiveDateTime::parse(s, &format_description).unwrap(); + let d = match time::OffsetDateTime::now_local() { + Ok(now) => now, + Err(e) => { + panic!("Error {} retrieving the OffsetDateTime::now_local", e); + } + }; + let offset_dt = tm.assume_offset(d.offset()); + FileTime::from_unix_time(offset_dt.unix_timestamp(), tm.nanosecond()) } #[test] @@ -83,7 +103,10 @@ fn test_touch_set_mdhm_time() { let start_of_year = str_to_filetime( "%Y%m%d%H%M", - &format!("{}01010000", 1900 + time::now().tm_year), + &format!( + "{}01010000", + time::OffsetDateTime::now_local().unwrap().year() + ), ); let (atime, mtime) = get_file_times(&at, file); assert_eq!(atime, mtime); @@ -104,7 +127,7 @@ fn test_touch_set_mdhms_time() { let start_of_year = str_to_filetime( "%Y%m%d%H%M.%S", - &format!("{}01010000.00", 1900 + time::now().tm_year), + &format!("{}01010000.00", time::OffsetDateTime::now_utc().year()), ); let (atime, mtime) = get_file_times(&at, file); assert_eq!(atime, mtime); @@ -123,7 +146,7 @@ fn test_touch_set_ymdhm_time() { assert!(at.file_exists(file)); - let start_of_year = str_to_filetime("%y%m%d%H%M", "1501010000"); + let start_of_year = str_to_filetime("%Y%m%d%H%M", "201501010000"); let (atime, mtime) = get_file_times(&at, file); assert_eq!(atime, mtime); assert_eq!(atime.unix_seconds() - start_of_year.unix_seconds(), 45240); @@ -141,7 +164,7 @@ fn test_touch_set_ymdhms_time() { assert!(at.file_exists(file)); - let start_of_year = str_to_filetime("%y%m%d%H%M.%S", "1501010000.00"); + let start_of_year = str_to_filetime("%Y%m%d%H%M.%S", "201501010000.00"); let (atime, mtime) = get_file_times(&at, file); assert_eq!(atime, mtime); assert_eq!(atime.unix_seconds() - start_of_year.unix_seconds(), 45296); @@ -404,6 +427,86 @@ fn test_touch_set_date3() { assert_eq!(mtime, expected); } +#[test] +fn test_touch_set_date4() { + let (at, mut ucmd) = at_and_ucmd!(); + let file = "test_touch_set_date"; + + ucmd.args(&["-d", "1970-01-01 18:43:33", file]) + .succeeds() + .no_stderr(); + + assert!(at.file_exists(file)); + + let expected = FileTime::from_unix_time(67413, 0); + let (atime, mtime) = get_file_times(&at, file); + assert_eq!(atime, mtime); + assert_eq!(atime, expected); + assert_eq!(mtime, expected); +} + +#[test] +fn test_touch_set_date5() { + let (at, mut ucmd) = at_and_ucmd!(); + let file = "test_touch_set_date"; + + ucmd.args(&["-d", "1970-01-01 18:43:33.023456789", file]) + .succeeds() + .no_stderr(); + + assert!(at.file_exists(file)); + + // Slightly different result on Windows for nano seconds + // TODO: investigate + #[cfg(windows)] + let expected = FileTime::from_unix_time(67413, 23456700); + #[cfg(not(windows))] + let expected = FileTime::from_unix_time(67413, 23456789); + + let (atime, mtime) = get_file_times(&at, file); + assert_eq!(atime, mtime); + assert_eq!(atime, expected); + assert_eq!(mtime, expected); +} + +#[test] +fn test_touch_set_date6() { + let (at, mut ucmd) = at_and_ucmd!(); + let file = "test_touch_set_date"; + + ucmd.args(&["-d", "2000-01-01 00:00", file]) + .succeeds() + .no_stderr(); + + assert!(at.file_exists(file)); + + let expected = FileTime::from_unix_time(946684800, 0); + + let (atime, mtime) = get_file_times(&at, file); + assert_eq!(atime, mtime); + assert_eq!(atime, expected); + assert_eq!(mtime, expected); +} + +#[test] +fn test_touch_set_date7() { + let (at, mut ucmd) = at_and_ucmd!(); + let file = "test_touch_set_date"; + + ucmd.args(&["-d", "2004-01-16 12:00 +0000", file]) + .succeeds() + .no_stderr(); + + assert!(at.file_exists(file)); + + let expected = FileTime::from_unix_time(1074254400, 0); + + let (atime, mtime) = get_file_times(&at, file); + assert_eq!(atime, mtime); + assert_eq!(atime, expected); + assert_eq!(mtime, expected); +} + #[test] fn test_touch_set_date_wrong_format() { let (_at, mut ucmd) = at_and_ucmd!(); @@ -430,18 +533,18 @@ fn test_touch_mtime_dst_succeeds() { assert_eq!(target_time, mtime); } -// is_dst_switch_hour returns true if timespec ts is just before the switch -// to Daylight Saving Time. -// For example, in EST (UTC-5), Timespec { sec: 1583647200, nsec: 0 } -// for March 8 2020 01:00:00 AM -// is just before the switch because on that day clock jumps by 1 hour, -// so 1 minute after 01:59:00 is 03:00:00. -fn is_dst_switch_hour(ts: time::Timespec) -> bool { - let ts_after = ts + time::Duration::hours(1); - let tm = time::at(ts); - let tm_after = time::at(ts_after); - tm_after.tm_hour == tm.tm_hour + 2 -} +// // is_dst_switch_hour returns true if timespec ts is just before the switch +// // to Daylight Saving Time. +// // For example, in EST (UTC-5), Timespec { sec: 1583647200, nsec: 0 } +// // for March 8 2020 01:00:00 AM +// // is just before the switch because on that day clock jumps by 1 hour, +// // so 1 minute after 01:59:00 is 03:00:00. +// fn is_dst_switch_hour(ts: time::Timespec) -> bool { +// let ts_after = ts + time::Duration::hours(1); +// let tm = time::at(ts); +// let tm_after = time::at(ts_after); +// tm_after.tm_hour == tm.tm_hour + 2 +// } // get_dst_switch_hour returns date string for which touch -m -t fails. // For example, in EST (UTC-5), that will be "202003080200" so @@ -450,23 +553,30 @@ fn is_dst_switch_hour(ts: time::Timespec) -> bool { // In other locales it will be a different date/time, and in some locales // it doesn't exist at all, in which case this function will return None. fn get_dst_switch_hour() -> Option { - let now = time::now(); - // Start from January 1, 2020, 00:00. - let mut tm = time::strptime("20200101-0000", "%Y%m%d-%H%M").unwrap(); - tm.tm_isdst = -1; - tm.tm_utcoff = now.tm_utcoff; - let mut ts = tm.to_timespec(); - // Loop through all hours in year 2020 until we find the hour just - // before the switch to DST. - for _i in 0..(366 * 24) { - if is_dst_switch_hour(ts) { - let mut tm = time::at(ts); - tm.tm_hour += 1; - let s = time::strftime("%Y%m%d%H%M", &tm).unwrap(); - return Some(s); + //let now = time::OffsetDateTime::now_local().unwrap(); + let now = match time::OffsetDateTime::now_local() { + Ok(now) => now, + Err(e) => { + panic!("Error {} retrieving the OffsetDateTime::now_local", e); } - ts = ts + time::Duration::hours(1); - } + }; + + // Start from January 1, 2020, 00:00. + let tm = datetime!(2020-01-01 00:00 UTC); + tm.to_offset(now.offset()); + + // let mut ts = tm.to_timespec(); + // // Loop through all hours in year 2020 until we find the hour just + // // before the switch to DST. + // for _i in 0..(366 * 24) { + // // if is_dst_switch_hour(ts) { + // // let mut tm = time::at(ts); + // // tm.tm_hour += 1; + // // let s = time::strftime("%Y%m%d%H%M", &tm).unwrap(); + // // return Some(s); + // // } + // ts = ts + time::Duration::hours(1); + // } None } @@ -573,3 +683,21 @@ fn test_no_dereference_no_file() { .stderr_contains("setting times of 'not-a-file-1': No such file or directory") .stderr_contains("setting times of 'not-a-file-2': No such file or directory"); } + +#[test] +fn test_touch_leap_second() { + let (at, mut ucmd) = at_and_ucmd!(); + let file = "test_touch_leap_sec"; + + ucmd.args(&["-t", "197001010000.60", file]) + .succeeds() + .no_stderr(); + + assert!(at.file_exists(file)); + + let epoch = str_to_filetime("%Y%m%d%H%M", "197001010000"); + let (atime, mtime) = get_file_times(&at, file); + assert_eq!(atime, mtime); + assert_eq!(atime.unix_seconds() - epoch.unix_seconds(), 60); + assert_eq!(mtime.unix_seconds() - epoch.unix_seconds(), 60); +} diff --git a/tests/by-util/test_uniq.rs b/tests/by-util/test_uniq.rs index b3f5fcd68..77ad10b32 100644 --- a/tests/by-util/test_uniq.rs +++ b/tests/by-util/test_uniq.rs @@ -680,6 +680,9 @@ fn gnu_tests() { stderr: None, exit: None, }, + /* + Disable as it fails too often. See: + https://github.com/uutils/coreutils/issues/3509 TestCase { name: "112", args: &["-D", "-c"], @@ -687,7 +690,7 @@ fn gnu_tests() { stdout: Some(""), stderr: Some("uniq: printing all duplicated lines and repeat counts is meaningless"), exit: Some(1), - }, + },*/ TestCase { name: "113", args: &["--all-repeated=separate"], diff --git a/tests/common/util.rs b/tests/common/util.rs index 5a669fcd4..d601b90d8 100644 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -1362,6 +1362,70 @@ pub fn expected_result(ts: &TestScenario, args: &[&str]) -> std::result::Result< )) } +/// This is a convenience wrapper to run a ucmd with root permissions. +/// It can be used to test programs when being root is needed +/// This runs 'sudo -E --non-interactive target/debug/coreutils util_name args` +/// This is primarily designed to run in an environment where whoami is in $path +/// and where non-interactive sudo is possible. +/// To check if i) non-interactive sudo is possible and ii) if sudo works, this runs: +/// 'sudo -E --non-interactive whoami' first. +/// +/// This return an `Err()` if run inside CICD because there's no 'sudo'. +/// +/// Example: +/// +/// ```no_run +/// use crate::common::util::*; +/// #[test] +/// fn test_xyz() { +/// let ts = TestScenario::new("whoami"); +/// let expected = "root\n".to_string(); +/// if let Ok(result) = run_ucmd_as_root(&ts, &[]) { +/// result.stdout_is(expected); +/// } else { +/// println!("TEST SKIPPED"); +/// } +/// } +///``` +#[cfg(unix)] +pub fn run_ucmd_as_root( + ts: &TestScenario, + args: &[&str], +) -> std::result::Result { + if !is_ci() { + // check if we can run 'sudo' + log_info("run", "sudo -E --non-interactive whoami"); + match Command::new("sudo") + .env("LC_ALL", "C") + .args(&["-E", "--non-interactive", "whoami"]) + .output() + { + Ok(output) if String::from_utf8_lossy(&output.stdout).eq("root\n") => { + // we can run sudo and we're root + // run ucmd as root: + Ok(ts + .cmd_keepenv("sudo") + .env("LC_ALL", "C") + .arg("-E") + .arg("--non-interactive") + .arg(&ts.bin_path) + .arg(&ts.util_name) + .args(args) + .run()) + } + Ok(output) + if String::from_utf8_lossy(&output.stderr).eq("sudo: a password is required\n") => + { + Err("Cannot run non-interactive sudo".to_string()) + } + Ok(_output) => Err("\"sudo whoami\" didn't return \"root\"".to_string()), + Err(e) => Err(format!("{}: {}", UUTILS_WARNING, e)), + } + } else { + Err(format!("{}: {}", UUTILS_INFO, "cannot run inside CI")) + } +} + /// Sanity checks for test utils #[cfg(test)] mod tests { @@ -1712,4 +1776,32 @@ mod tests { std::assert_eq!(host_name_for("gwho"), "gwho"); } } + + #[test] + #[cfg(unix)] + #[cfg(feature = "whoami")] + fn test_run_ucmd_as_root() { + if !is_ci() { + // Skip test if we can't guarantee non-interactive `sudo`, or if we're not "root" + if let Ok(output) = Command::new("sudo") + .env("LC_ALL", "C") + .args(&["-E", "--non-interactive", "whoami"]) + .output() + { + if output.status.success() && String::from_utf8_lossy(&output.stdout).eq("root\n") { + let ts = TestScenario::new("whoami"); + std::assert_eq!( + run_ucmd_as_root(&ts, &[]).unwrap().stdout_str().trim(), + "root" + ); + } else { + println!("TEST SKIPPED (we're not root)"); + } + } else { + println!("TEST SKIPPED (cannot run sudo)"); + } + } else { + println!("TEST SKIPPED (cannot run inside CI)"); + } + } } diff --git a/util/android-commands.sh b/util/android-commands.sh index 2a4fca416..0a245a004 100755 --- a/util/android-commands.sh +++ b/util/android-commands.sh @@ -1,3 +1,4 @@ +#!/bin/bash # spell-checker:ignore termux keyevent sdcard binutils unmatch adb's dumpsys logcat pkill # There are three shells: the host's, adb, and termux. Only adb lets us run diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 83993fde9..942aa9d87 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -137,7 +137,8 @@ sed -i 's|chmod |/usr/bin/chmod |' tests/du/inacc-dir.sh tests/tail-2/tail-n0f.s sed -i 's|sort |/usr/bin/sort |' tests/ls/hyperlink.sh tests/misc/test-N.sh sed -i 's|split |/usr/bin/split |' tests/misc/factor-parallel.sh sed -i 's|id -|/usr/bin/id -|' tests/misc/runcon-no-reorder.sh -sed -i 's|touch |/usr/bin/touch |' tests/cp/preserve-link.sh tests/cp/reflink-perm.sh tests/ls/block-size.sh tests/mv/update.sh tests/misc/ls-time.sh tests/misc/stat-nanoseconds.sh tests/misc/time-style.sh tests/misc/test-N.sh +# tests/ls/abmon-align.sh - https://github.com/uutils/coreutils/issues/3505 +sed -i 's|touch |/usr/bin/touch |' tests/cp/preserve-link.sh tests/cp/reflink-perm.sh tests/ls/block-size.sh tests/mv/update.sh tests/misc/ls-time.sh tests/misc/stat-nanoseconds.sh tests/misc/time-style.sh tests/misc/test-N.sh tests/ls/abmon-align.sh sed -i 's|ln -|/usr/bin/ln -|' tests/cp/link-deref.sh sed -i 's|cp |/usr/bin/cp |' tests/mv/hard-2.sh sed -i 's|paste |/usr/bin/paste |' tests/misc/od-endian.sh