From f37ecaa25a12d995be48aab065b7140e894da7f5 Mon Sep 17 00:00:00 2001 From: Zoa Hickenlooper Date: Tue, 12 May 2026 14:06:09 -0700 Subject: [PATCH 1/4] fuck im leaking... --- .github/workflows/ci.yml | 37 +++ .github/workflows/release.yml | 80 ++++++ Cargo.lock | 454 +++++++++++++++++++++++++++++++--- Cargo.toml | 24 +- LICENSE | 13 + README.md | 167 +++++++++++++ readme.md | 113 --------- src/cleanup.rs | 44 ++++ src/cli.rs | 149 +++++++++++ src/dicks.rs | 155 ++++++++++++ src/drive.rs | 376 ++++++++++++++++++++++++++++ src/main.rs | 318 ++++++++++++------------ src/progress.rs | 421 +++++++++++++++++++++++++++++++ src/safety.rs | 171 +++++++++++++ src/wipe.rs | 306 +++++++++++++++++++++++ 15 files changed, 2522 insertions(+), 306 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 LICENSE create mode 100644 README.md delete mode 100644 readme.md create mode 100644 src/cleanup.rs create mode 100644 src/cli.rs create mode 100644 src/dicks.rs create mode 100644 src/drive.rs create mode 100644 src/progress.rs create mode 100644 src/safety.rs create mode 100644 src/wipe.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..599a128 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI + +on: + push: + branches: [master, main] + pull_request: + +jobs: + test: + name: ${{ matrix.os }} / stable + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust (stable) + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - uses: Swatinem/rust-cache@v2 + + - name: cargo fmt + run: cargo fmt --all -- --check + + - name: cargo clippy + run: cargo clippy --release --all-targets -- -D warnings + + - name: cargo build + run: cargo build --release + + - name: cargo test + run: cargo test --release diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8a9332a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,80 @@ +name: Release + +on: + push: + tags: ['v*'] + +permissions: + contents: write + +jobs: + build: + name: ${{ matrix.target }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + ext: '' + - os: ubuntu-latest + target: aarch64-unknown-linux-gnu + ext: '' + cross: true + - os: macos-latest + target: x86_64-apple-darwin + ext: '' + - os: macos-latest + target: aarch64-apple-darwin + ext: '' + - os: windows-latest + target: x86_64-pc-windows-msvc + ext: '.exe' + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - uses: Swatinem/rust-cache@v2 + + - name: Install cross-toolchain (Linux aarch64) + if: matrix.cross == true + run: | + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> "$GITHUB_ENV" + + - name: Build release + run: cargo build --release --target ${{ matrix.target }} + + - name: Stage artifact + shell: bash + env: + TARGET: ${{ matrix.target }} + EXT: ${{ matrix.ext }} + REF_NAME: ${{ github.ref_name }} + run: | + mkdir -p dist + BIN="target/${TARGET}/release/wipedicks${EXT}" + OUT="wipedicks-${REF_NAME}-${TARGET}" + mkdir -p "dist/${OUT}" + cp "${BIN}" "dist/${OUT}/" + cp readme.md "dist/${OUT}/" + cd dist + if [[ "${EXT}" == ".exe" ]]; then + 7z a "${OUT}.zip" "${OUT}" + else + tar czf "${OUT}.tar.gz" "${OUT}" + fi + + - name: Upload to release + uses: softprops/action-gh-release@v2 + with: + files: | + dist/*.tar.gz + dist/*.zip diff --git a/Cargo.lock b/Cargo.lock index 79c0741..24c298d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,12 +1,12 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "anstream" -version = "0.6.14" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -19,58 +19,99 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.7" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.4" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.0" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.3" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "windows-sys", + "once_cell_polyfill", + "windows-sys 0.61.2", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "clap" -version = "4.5.9" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] name = "clap_builder" -version = "4.5.9" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -78,52 +119,239 @@ dependencies = [ "strsim", ] +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "clap_lex" -version = "0.7.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.1" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.59.0", +] + +[[package]] +name = "ctrlc" +version = "3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" +dependencies = [ + "dispatch2", + "nix", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", "wasi", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indicatif" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width", + "web-time", +] + [[package]] name = "is_terminal_polyfill" -version = "1.70.0" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "js-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] [[package]] name = "libc" -version = "0.2.155" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] -name = "ppv-lite86" +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha", @@ -149,12 +377,56 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "utf8parse" version = "0.2.2" @@ -163,19 +435,99 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wide" +version = "0.7.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" +dependencies = [ + "bytemuck", + "safe_arch", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" -version = "0.52.0" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -242,8 +594,34 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "wipedicks" -version = "0.1.0" +version = "0.2.0" dependencies = [ + "anyhow", "clap", + "console", + "ctrlc", + "indicatif", + "libc", "rand", + "wide", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] diff --git a/Cargo.toml b/Cargo.toml index 4b7a245..670a000 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,26 @@ [package] name = "wipedicks" -version = "0.1.0" +version = "0.2.0" edition = "2021" +description = "Wipe files and drives securely with random ASCII dicks" +keywords = ["anti-forensics", "antiforensics", "forensics", "wipe", "ascii"] +readme = "README.md" +repository = "https://github.com/vxfemboy/wipedicks" +license = "WTFPL" + [dependencies] -rand = "0.8.5" -clap = "4.5.9" +anyhow = "1" +clap = { version = "4.5", features = ["derive"] } +console = "0.15" +ctrlc = "3.4" +indicatif = "0.17" +rand = { version = "0.8", features = ["small_rng"] } +wide = "0.7" + +[target.'cfg(unix)'.dependencies] +libc = "0.2" + +[profile.release] +lto = "thin" +codegen-units = 1 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3c56b47 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2026 Zoa Hickenlooper / aka vxfemboy + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. diff --git a/README.md b/README.md new file mode 100644 index 0000000..09f6a1b --- /dev/null +++ b/README.md @@ -0,0 +1,167 @@ +# 8====D~ WipeDicks + +[![CI](https://github.com/vxfemboy/wipedicks/actions/workflows/ci.yml/badge.svg)](https://github.com/vxfemboy/wipedicks/actions/workflows/ci.yml) + +``` +⠀⠀⠀⠀⠀⣠⠶⠚⠛⠛⠛⠲⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⣴⠟⠁⠀⠀⠀⠀⠀⠀⠀⠻⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⣠⣾⣷⣄⠀⠀⠀⢀⣠⣤⣤⡀⠀⢿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⢸⣿⡿⢃⣸⡶⠂⢠⣿⣿⡿⠁⣱⠀⢸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⢸⡏⠉⠩⣏⣐⣦⠀⠛⠦⠴⠚⠁⠀⣸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⣼⠧⠶⠶⠶⠿⠶⠶⠖⠚⠛⠉⠁⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⠶⠶⡄⠀⠀ +⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⢠⡟⠀⠀⢹⠀⠀ +⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⢤⢠⡆⠀⢸⡄⠀⠀⠀⠀⠀⠀⢀⡿⠁⠀⠀⡾⠀⠀ +⢹⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠈⡇⠀⠸⣧⣠⠴⠶⠖⠲⢶⡞⠁⠀⢈⡼⢃⠀⠀ +⠸⡆⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⡇⠀⠀⢿⠁⠄⣲⡶⠶⠿⢤⣄⡀⠛⢛⠉⢻⠀ +⠀⢿⡀⠀⠀⠀⠀⠀⠀⠀⢸⠠⣇⠀⠀⠀⠀⠊⠁⠀⠀⠀⠀⠀⠙⢦⠈⠙⠓⣆ +⠀⠈⢷⡀⠀⠀⠀⠀⠀⢠⠏⡀⣬⣹⣦⠀⠀⠀⠀⠀⠁⠀⠀⠀⠀⠈⡿⠶⠶⠋ +⠀⠀⠈⢷⡀⠀⠀⠀⠀⠘⠛⠛⠋⠀⠀⠀⠀⠀⠀⠄⠀⠀⠀⠀⠀⣼⠃⠀⠀⠀ +⠀⠀⠀⠀⠙⢦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠄⠀⠀⣠⡞⠁⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠈⠛⣷⢶⣦⣤⣄⣀⣠⣤⣤⠀⣶⠶⠶⠶⠛⠁⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⣀⡀⠀⣰⠇⣾⠀⠀⠈⣩⣥⣄⣿⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⢿⡉⠳⡟⣸⠃⠀⠀⠀⠘⢷⣌⠉⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠙⢦⣴⠏⠀⠀⠀⠀⠀⠀⠉⠳⠶⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ + +❤ xxd /dev/sda1 | head +00000000: 383d 447e 7e20 383d 3d3d 3d3d 3d3d 447e 8=D~~ 8=======D~ +00000010: 2038 233d 3d3d 3d3d 3d3d 3d44 7e7e 2038 8#========D~~ 8 +00000020: 233d 3d44 7e7e 2038 3d3d 3d3d 3d3d 3d3d #==D~~ 8======== +00000030: 3d44 2038 3d3d 3d3d 447e 7e7e 2038 3d3d =D 8====D~~~ 8== +00000040: 3d3d 3d3d 3d3d 3d3d 3d44 7e20 3823 3d3d =========D~ 8#== +00000050: 3d3d 3d3d 3d3d 3d3d 3d3d 447e 7e20 383d ==========D~~ 8= +00000060: 3d3d 3d3d 3d44 7e7e 2038 233d 3d3d 3d3d =====D~~ 8#===== +00000070: 3d3d 3d3d 447e 2038 233d 3d3d 3d3d 3d3d ====D~ 8#======= +00000080: 3d3d 3d3d 447e 2038 233d 3d3d 3d44 7e20 ====D~ 8#====D~ +00000090: 3823 3d3d 3d3d 3d3d 3d3d 3d3d 3d3d 447e 8#============D~ +``` +Welcome to WipeDicks, the tool that gives your data the shaft! + +## What's the Big Deal? 8===D + +WipeDicks is a high-performance, multi-threaded file and device wiping tool that overwrites your data with a veritable bukkake of ASCII penises. It's designed for secure data erasure with more dick jokes than you can shake a stick at! + +## Features (or as we like to call them, "Dick Pics") 8=D + +- Securely penetrates your files and devices +- Multi-threaded for maximum pleasure (and performance) — bounded thread pool, no fork bombs +- Recursively wipes directories (because size matters) +- Adjustable number of rounds (for those who need extra stamina) +- Option to wipe free space (leave no trace behind!) +- **Live ASCII penis progress bar** — watch the `D` slide right as your data dies (resolves #1) +- **SIMD-accelerated** penis generation via the `wide` crate (stable Rust, no nightly) +- **Configurable shaft length, jizz volume, and ball ring chance** — for the discerning data destroyer +- **SSD/NVMe-aware safety**: refuses to multi-pass overwrite flash storage by default, and tells you the right native tool to use instead (Linux + macOS) +- **Path-safety guards**: refuses to wipe `/`, system dirs, active swap files, and mounted block devices — pass `--rape` if you really mean it +- **`--verify`** read-back pass to catch bad sectors and FS corruption +- **`--gay`** mode for when monochrome dicks just aren't enough (rainbow bar + bright-white jizz) +- **Ctrl-C cleanup**: an interrupted wipe still removes its temp files +- Buffered I/O with per-round buffer reuse — orders of magnitude fewer syscalls and ~100x less RNG work on multi-pass runs + +> [!Warning] +> ## Cock Block Alert! 8==X +> This tool is designed to permanently destroy data. Use with extreme caution, or you might end up with blue balls (i.e., regret). +> +> Always double-check your targets before unleashing our ASCII members upon them. + +## SSDs, NVMes, and the Sad Truth About Wear Leveling 8==X + +**Overwriting flash storage with multi-pass writes is both useless and harmful.** Yes, even with our beautiful penises. + +- **Useless**: SSDs and NVMes use a Flash Translation Layer that remaps logical writes to fresh NAND pages. Overwriting LBA 0 doesn't necessarily clobber the physical NAND cell that holds your old data — that data lingers in over-provisioned NAND until the controller decides to recycle it. +- **Harmful**: Every pass burns finite program/erase cycles. A 7-pass DoD-style wipe on a 1 TB QLC SSD can consume meaningful percentage points of its lifetime. + +So wipedicks **refuses by default** when you point it at a flash block device, and prints the right native command instead: + +| Drive | Right tool | +|---|---| +| NVMe | `sudo nvme format /dev/nvmeXnY -s 1` (user-data erase) or `-s 2` (crypto erase, SED only) | +| SATA SSD | `sudo hdparm --user-master u --security-set-pass p /dev/sdX && sudo hdparm --user-master u --security-erase p /dev/sdX` | +| Either (fast, non-cryptographic) | `sudo blkdiscard /dev/X` | +| Self-encrypting (TCG Opal) | `sudo sedutil-cli --revertNoErase` | + +Use `--secure-erase` to have wipedicks run the right one for you. Use `--rape` to override the refusal and proceed with the (ineffective, damaging) overwrite anyway. Wiping individual *files* on a flash filesystem still works — wipedicks prints a one-time warning and continues, because at least `unlink(2)` actually deletes the inode. + +## Installation (Getting It Up and Running) 8===D~ + +1. Make sure you've got Rust and Cargo (the only package we care about) +2. Clone this bad boy: + ```bash + git clone https://github.com/vxfemboy/wipedicks.git && cd wipedicks + ``` +3. Get it hard... err, built: + ```bash + cargo build --release + ``` +4. Find your new toy in `target/release/wipedicks` + +## Usage (How to Handle Your Tool) 8====D + +#### Basic usage: +Options (Piercings for your command): +- `-r, --recursive`: Go deep into directories +- `-n, --numrounds `: How many times you want to go (default: 1) +- `-w, --wipefree`: Clean up afterwards (fills the FS with dicks until ENOSPC, then deletes) +- `-s, --slow`: Take it slow, big boy (one-penis-per-write, no buffering, no SIMD) +- `--rape`: Override the SSD/NVMe refusal. You probably don't want this. +- `--secure-erase`: For block-device targets, delegate to the native tool (`nvme format` / `hdparm --security-erase`) +- `--shaft-min ` / `--shaft-max `: Range of `=` per penis (default 1..12) +- `--jizz-min ` / `--jizz-max `: Range of `~` per penis (default 0..3) +- `--balls-chance

`: Probability of a `#` base ring, 0.0..1.0 (default 0.5) +- `--buffer-size `: Write batch size (default 1 MiB). Bigger = fewer syscalls. +- `--bar-width `: Width of the penis progress bar (default 16) +- `--no-progress`: Disable progress bars (auto-disabled when stderr isn't a TTY) +- `--gay`: Cycle the bar across the rainbow (red→orange→yellow→green→cyan→blue→magenta) with bright-white jizz +- `--verify`: After each round, read the file back and confirm every byte is a valid penis character. Catches bad sectors and FS-level corruption. +- `--dry-run`: Print one buffer of generated dicks to stdout and exit. For inspecting your config. + +#### Examples: +1. Give a single file the business: + ```bash + ./wipedicks /path/to/file + ``` +2. Go to town on a directory, three times: + ```bash + ./wipedicks -r -n 3 /path/to/directory + ``` +3. A more endowed wipe (huge shaft, drenched in jizz, every dick has balls): + ```bash + ./wipedicks --shaft-min 20 --shaft-max 40 --jizz-min 5 --jizz-max 10 --balls-chance 1.0 ./file + ``` +4. Preview what the generator would write, without touching anything: + ```bash + ./wipedicks --dry-run --shaft-max 30 /tmp/anything + ``` +5. Securely erase an SSD/NVMe (delegates to the native tool — needs root): + ```bash + sudo ./wipedicks --secure-erase /dev/nvme0n1 + ``` +6. Absolutely destroy a spinning-rust device (Good luck pulling out of this one!): + ```bash + ./wipedicks /dev/sdX # only allowed on rotational drives by default + ``` + +> [!CAUTION] +> ## Wrap It Before You Tap It +> - Always check your target twice, or you might screw the wrong hole +> - Wiping entire devices is like unprotected data sex - there's no going back +> - Don't run this on system files unless you want your computer to go limp +> - This tool is for giggles and learning. For serious business, use protection (i.e., certified tools) + +## Contributing (Sword Fighters Wanted) + +![Sword Fighting](https://media.giphy.com/media/l1ugaivowDSqyE3KM/giphy.gif) + +Want to cross streams? Contributions are welcome! Feel free to submit a Pull Request. + +## Credits + +This project was brought up to me by [Drewsif/wipedicks](https://github.com/Drewsif/wipedicks/) +all credits for the idea goes to the original creator I just rewrote it in Rust + +## Disclaimer (Cover Your Ass-ets) + +The authors aren't responsible for any data loss or damage. Use at your own risk, you kinky data destroyer! + +Remember: +> With great power comes great responsibili-D. +> Use wipedicks wisely, and may your data always rest in pieces! 8====D~~~ diff --git a/readme.md b/readme.md deleted file mode 100644 index d22b49e..0000000 --- a/readme.md +++ /dev/null @@ -1,113 +0,0 @@ -# 8====D~ WipeDicks -``` -⠀⠀⠀⠀⠀⣠⠶⠚⠛⠛⠛⠲⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ -⠀⠀⠀⣴⠟⠁⠀⠀⠀⠀⠀⠀⠀⠻⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ -⠀⣠⣾⣷⣄⠀⠀⠀⢀⣠⣤⣤⡀⠀⢿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ -⢸⣿⡿⢃⣸⡶⠂⢠⣿⣿⡿⠁⣱⠀⢸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ -⢸⡏⠉⠩⣏⣐⣦⠀⠛⠦⠴⠚⠁⠀⣸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ -⣼⠧⠶⠶⠶⠿⠶⠶⠖⠚⠛⠉⠁⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⠶⠶⡄⠀⠀ -⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⢠⡟⠀⠀⢹⠀⠀ -⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⢤⢠⡆⠀⢸⡄⠀⠀⠀⠀⠀⠀⢀⡿⠁⠀⠀⡾⠀⠀ -⢹⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠈⡇⠀⠸⣧⣠⠴⠶⠖⠲⢶⡞⠁⠀⢈⡼⢃⠀⠀ -⠸⡆⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⡇⠀⠀⢿⠁⠄⣲⡶⠶⠿⢤⣄⡀⠛⢛⠉⢻⠀ -⠀⢿⡀⠀⠀⠀⠀⠀⠀⠀⢸⠠⣇⠀⠀⠀⠀⠊⠁⠀⠀⠀⠀⠀⠙⢦⠈⠙⠓⣆ -⠀⠈⢷⡀⠀⠀⠀⠀⠀⢠⠏⡀⣬⣹⣦⠀⠀⠀⠀⠀⠁⠀⠀⠀⠀⠈⡿⠶⠶⠋ -⠀⠀⠈⢷⡀⠀⠀⠀⠀⠘⠛⠛⠋⠀⠀⠀⠀⠀⠀⠄⠀⠀⠀⠀⠀⣼⠃⠀⠀⠀ -⠀⠀⠀⠀⠙⢦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠄⠀⠀⣠⡞⠁⠀⠀⠀⠀ -⠀⠀⠀⠀⠀⠀⠈⠛⣷⢶⣦⣤⣄⣀⣠⣤⣤⠀⣶⠶⠶⠶⠛⠁⠀⠀⠀⠀⠀⠀ -⠀⠀⠀⠀⣀⡀⠀⣰⠇⣾⠀⠀⠈⣩⣥⣄⣿⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ -⠀⠀⠀⠀⢿⡉⠳⡟⣸⠃⠀⠀⠀⠘⢷⣌⠉⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ -⠀⠀⠀⠀⠀⠙⢦⣴⠏⠀⠀⠀⠀⠀⠀⠉⠳⠶⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - -❤ xxd /dev/sda1 | head -00000000: 383d 447e 7e20 383d 3d3d 3d3d 3d3d 447e 8=D~~ 8=======D~ -00000010: 2038 233d 3d3d 3d3d 3d3d 3d44 7e7e 2038 8#========D~~ 8 -00000020: 233d 3d44 7e7e 2038 3d3d 3d3d 3d3d 3d3d #==D~~ 8======== -00000030: 3d44 2038 3d3d 3d3d 447e 7e7e 2038 3d3d =D 8====D~~~ 8== -00000040: 3d3d 3d3d 3d3d 3d3d 3d44 7e20 3823 3d3d =========D~ 8#== -00000050: 3d3d 3d3d 3d3d 3d3d 3d3d 447e 7e20 383d ==========D~~ 8= -00000060: 3d3d 3d3d 3d44 7e7e 2038 233d 3d3d 3d3d =====D~~ 8#===== -00000070: 3d3d 3d3d 447e 2038 233d 3d3d 3d3d 3d3d ====D~ 8#======= -00000080: 3d3d 3d3d 447e 2038 233d 3d3d 3d44 7e20 ====D~ 8#====D~ -00000090: 3823 3d3d 3d3d 3d3d 3d3d 3d3d 3d3d 447e 8#============D~ -``` -Welcome to WipeDicks, the tool that gives your data the shaft! - -## What's the Big Deal? 8===D - -WipeDicks is a high-performance, multi-threaded file and device wiping tool that overwrites your data with a veritable bukkake of ASCII penises. It's designed for secure data erasure with more dick jokes than you can shake a stick at! - -## Features (or as we like to call them, "Dick Pics") 8=D - -- Securely penetrates your files and devices -- Multi-threaded for maximum pleasure (and performance) -- Recursively wipes directories (because size matters) -- Adjustable number of rounds (for those who need extra stamina) -- Option to wipe free space (leave no trace behind!) -- Humorous ASCII art overwrite patterns (it's not the size of the boat, it's the motion of the ocean) - -## Warning (Cock Block Alert!) 8==X - -This tool is designed to permanently destroy data. Use with extreme caution, or you might end up with blue balls (i.e., regret). - -Always double-check your targets before unleashing our ASCII members upon them. - -## Installation (Getting It Up and Running) 8===D~ - -1. Make sure you've got Rust and Cargo (the only package we care about) -2. Clone this bad boy: - ```bash - git clone https://github.com/vxfemboy/wipedicks.git && cd wipedicks - ``` -3. Get it hard... err, built: - ```bash - cargo build --release - ``` -4. Find your new toy in `target/release/wipedicks` - -## Usage (How to Handle Your Tool) 8====D - -#### Basic usage: -Options (Piercings for your command): -- `-r, --recursive`: Go deep into directories -- `-n, --numrounds `: How many times you want to go (default: 1) -- `-w, --wipefree`: Clean up afterwards -- `-s, --slow`: Take it slow, big boy - -#### Examples: -1. Give a single file the business: - ```bash - ./wipedicks /path/to/file - ``` -2. Go to town on a directory, three times: - ```bash - ./wipedicks -r -n 3 /path/to/directory - ``` -3. Absolutely destroy a device (Good luck pulling out of this one!): - ```bash - ./wipedicks /dev/sdX - ``` - -## Caution (Wrap It Before You Tap It) - -- Always check your target twice, or you might screw the wrong hole -- Wiping entire devices is like unprotected data sex - there's no going back -- Don't run this on system files unless you want your computer to go limp -- This tool is for giggles and learning. For serious business, use protection (i.e., certified tools) - -## Contributing (Sword Fighters Wanted) - -Want to cross streams? Contributions are welcome! Feel free to submit a Pull Request. - -## Credits - -This project was brought up to me by https://github.com/Drewsif/wipedicks/ -all credits for the idea goes to the original creator I just rewrote it in Rust - -## Disclaimer (Cover Your Ass-ets) - -The authors aren't responsible for any data loss or damage. Use at your own risk, you kinky data destroyer! - -Remember: -> With great power comes great responsibili-D. -> Use wipedicks wisely, and may your data always rest in pieces! 8====D~~~ diff --git a/src/cleanup.rs b/src/cleanup.rs new file mode 100644 index 0000000..97ed6b9 --- /dev/null +++ b/src/cleanup.rs @@ -0,0 +1,44 @@ +//! Pull-out registry for graceful interrupts. +//! +//! Every wipe target and every freespace temp file checks in here before it +//! gets opened. If the user hits Ctrl-C / something kills us with SIGTERM, +//! the handler atomically drains the registry and `unlink(2)`s every entry +//! — no used files left dangling like a guilty walk of shame. +//! +//! The normal happy-ending path also unlinks files directly. Both paths are +//! idempotent (already-gone == fine), so they cooperate without coordination. + +use std::path::{Path, PathBuf}; +use std::sync::{Mutex, OnceLock}; + +use anyhow::Result; + +static REGISTRY: OnceLock>> = OnceLock::new(); + +fn registry() -> &'static Mutex> { + REGISTRY.get_or_init(|| Mutex::new(Vec::new())) +} + +pub fn install_handler() -> Result<()> { + let _ = registry(); + ctrlc::set_handler(|| { + eprintln!("\n8==X interrupted; cleaning up..."); + let paths = std::mem::take(&mut *registry().lock().unwrap()); + for path in paths { + let _ = std::fs::remove_file(&path); + } + std::process::exit(130); // 128 + SIGINT + })?; + Ok(()) +} + +pub fn register(path: &Path) { + registry().lock().unwrap().push(path.to_path_buf()); +} + +pub fn unregister(path: &Path) { + let mut q = registry().lock().unwrap(); + if let Some(idx) = q.iter().position(|p| p == path) { + q.swap_remove(idx); + } +} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..494b80d --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,149 @@ +//! CLI surface. Every doc comment in here becomes part of `--help`, so we treat +//! help text as marketing copy: technically correct, narratively unhinged. + +use std::path::PathBuf; + +use clap::Parser; + +/// Wipe files and devices by face-fucking them with ASCII penises. +/// +/// By default, refuses to plow SSDs and NVMes — flash storage doesn't enjoy +/// repeat performances. The Flash Translation Layer remaps your writes to fresh +/// NAND, so your "secure overwrite" leaves the original data sitting smug in +/// over-provisioned cells, and every pass burns program/erase cycles off the +/// drive's lifespan. Use --secure-erase to delegate to the OS's native tool +/// (the drive's own factory reset), or --rape to override and proceed anyway, +/// you absolute degenerate. +#[derive(Parser, Debug)] +#[command( + name = "wipedicks", + version, + about = "Wipe files/devices with dicks.", + long_about = None, +)] +pub struct Args { + /// Files, directories, or block devices to wipe. Pass as many as you like — + /// we'll service them all in parallel like the bukkake bottom we are. + #[arg(required = true, num_args = 1..)] + pub files: Vec, + + /// Go deep — recurse into directories and shred every file inside. + #[arg(short = 'r', long)] + pub recursive: bool, + + /// How many rounds of overwriting per target. 1 by default (basic + /// missionary). Bump it for ceremonial multi-pass plowing — note: extra + /// rounds on SSDs are pure self-harm; see --rape. + #[arg(short = 'n', long = "numrounds", default_value_t = 1, value_name = "N")] + pub rounds: usize, + + /// After the targets are gone, fill the filesystem's free space with dicks + /// until ENOSPC, then delete the temp file. Erases the ghosts of files past. + #[arg(short = 'w', long)] + pub wipefree: bool, + + /// Take it slow, big boy. One penis per write(2). Useful for sanity + /// comparison or terminals with a kink for tiny syscalls. ~5000x more + /// syscalls than the buffered path. + #[arg(short = 's', long)] + pub slow: bool, + + /// Override every safety check (system paths, swap, mounted devices, the + /// SSD refusal). The nuclear safe word — if you pull this, we trust you + /// know exactly which hole you're aiming at. There is no afterward. + #[arg(long)] + pub rape: bool, + + /// On a block-device target, delegate to the drive's native secure-erase + /// tool: `nvme format -s 1` for NVMe, `hdparm --security-erase` for SATA. + /// Actually secure, doesn't burn PE cycles, gives the drive a happy ending. + #[arg(long)] + pub secure_erase: bool, + + /// Minimum '=' (shaft) count per penis. Default 1 — micro-dick energy. + #[arg(long = "shaft-min", default_value_t = 1, value_name = "N")] + pub shaft_min: usize, + + /// Maximum '=' (shaft) count per penis. Default 12 — well-endowed but + /// believable. Crank it for absolute schlong content. + #[arg(long = "shaft-max", default_value_t = 12, value_name = "N")] + pub shaft_max: usize, + + /// Minimum '~' (jizz) drip count per penis. 0 is "pulled out in time". + #[arg(long = "jizz-min", default_value_t = 0, value_name = "N")] + pub jizz_min: usize, + + /// Maximum '~' (jizz) drip count per penis. 3 is the default — modest pearl + /// necklace. Raise it for serious cumshot energy. + #[arg(long = "jizz-max", default_value_t = 3, value_name = "N")] + pub jizz_max: usize, + + /// Probability that a given penis gets a '#' (balls) at the base, in + /// [0.0, 1.0]. 0.5 is the default — half your dicks are circumcised. + #[arg(long = "balls-chance", default_value_t = 0.5, value_name = "P")] + pub balls_chance: f32, + + /// Write batch size in bytes. Larger buffer = fewer syscalls = faster + /// stroke rate. Default 1 MiB. Don't go below 64. + #[arg(long = "buffer-size", default_value_t = 1 << 20, value_name = "BYTES")] + pub buffer_size: usize, + + /// Width of each progress bar in characters. Default 16. Make it bigger + /// if you like long bars. + #[arg(long = "bar-width", default_value_t = 16, value_name = "N")] + pub bar_width: usize, + + /// Disable progress bars entirely. Auto-disabled when stderr isn't a TTY + /// (piped output stays clean — no escaped dicks bleeding into your logs). + #[arg(long = "no-progress")] + pub no_progress: bool, + + /// Print one buffer of generated dicks to stdout and exit, without + /// destroying anything. Use to preview your --shaft-* / --jizz-* / + /// --balls-chance configuration. Visual masturbation only. + #[arg(long = "dry-run")] + pub dry_run: bool, + + /// After every round, read the file back and confirm every byte is a + /// valid penis character (8, =, #, D, ~, space). Catches bad sectors and + /// filesystem corruption. For the paranoid bottom. + #[arg(long)] + pub verify: bool, + + /// Cycle the progress bar through the rainbow. Pride parade for the + /// shaft; jizz stays bright white (cum is cum regardless of orientation). + #[arg(long)] + pub gay: bool, +} + +impl Args { + /// Sanity-check cross-flag combinations. Returns the user-facing reason + /// the args don't make sense, if any. Good safe-words save lives. + pub fn validate(&self) -> Result<(), String> { + if self.shaft_min > self.shaft_max { + return Err("--shaft-min must be <= --shaft-max (your max can't be smaller than your min, big guy)".into()); + } + if self.jizz_min > self.jizz_max { + return Err("--jizz-min must be <= --jizz-max (drip range got crossed)".into()); + } + if !(0.0..=1.0).contains(&self.balls_chance) { + return Err( + "--balls-chance must be in [0.0, 1.0] (it's a probability, not a body count)" + .into(), + ); + } + if self.buffer_size < 64 { + return Err( + "--buffer-size must be >= 64 bytes (too small to fit a single dick)".into(), + ); + } + if self.bar_width < 4 { + return Err("--bar-width must be >= 4 (need room for [8D] at minimum)".into()); + } + Ok(()) + } +} + +pub fn parse() -> Args { + Args::parse() +} diff --git a/src/dicks.rs b/src/dicks.rs new file mode 100644 index 0000000..1afd172 --- /dev/null +++ b/src/dicks.rs @@ -0,0 +1,155 @@ +//! Random-penis factory. SIMD-juiced where it counts. Per-thread, +//! no sword-crossing. + +use rand::rngs::SmallRng; +use rand::{Rng, SeedableRng}; +use wide::u8x32; + +/// The anatomical knobs for our generated dicks. +#[derive(Clone, Debug)] +pub struct DickConfig { + pub shaft_min: usize, + pub shaft_max: usize, + pub jizz_min: usize, + pub jizz_max: usize, + pub balls_chance: f32, +} + +impl DickConfig { + /// Worst-case length of a single dick in bytes. Used to pre-size the buffer + /// so we never realloc mid-stroke. + pub fn max_dick_len(&self) -> usize { + // 8 + optional # + shaft + D + jizz + space + 1 + 1 + self.shaft_max + 1 + self.jizz_max + 1 + } +} + +/// Per-thread reusable cock-buffer. One thread owns one — sharing dicks +/// across threads is how you end up with weird race conditions and bent +/// shafts. Call `fill()` to load it up, then `as_bytes()` for the goods. +pub struct DickBuf { + cfg: DickConfig, + buf: Vec, + rng: SmallRng, + capacity: usize, +} + +impl DickBuf { + pub fn new(cfg: DickConfig, capacity: usize) -> Self { + let slack = cfg.max_dick_len(); + Self { + cfg, + buf: Vec::with_capacity(capacity + slack), + rng: SmallRng::from_entropy(), + capacity, + } + } + + /// Pump the buffer full of fresh dicks. Exactly `capacity` bytes when done. + pub fn fill(&mut self) { + self.buf.clear(); + while self.buf.len() < self.capacity { + push_one(&mut self.rng, &self.cfg, &mut self.buf); + } + self.buf.truncate(self.capacity); + } + + pub fn as_bytes(&self) -> &[u8] { + &self.buf + } +} + +/// Jerk out one random penis and append it to `out`. Shared between the +/// buffered fast path and the `--slow` one-dick-per-write masochism path. +pub fn push_one(rng: &mut SmallRng, cfg: &DickConfig, out: &mut Vec) { + let balls = rng.gen::() < cfg.balls_chance; + let shaft = if cfg.shaft_min == cfg.shaft_max { + cfg.shaft_min + } else { + rng.gen_range(cfg.shaft_min..=cfg.shaft_max) + }; + let jizz = if cfg.jizz_min == cfg.jizz_max { + cfg.jizz_min + } else { + rng.gen_range(cfg.jizz_min..=cfg.jizz_max) + }; + + out.push(b'8'); + if balls { + out.push(b'#'); + } + fill_run(out, b'=', shaft); + out.push(b'D'); + fill_run(out, b'~', jizz); + out.push(b' '); +} + +pub fn new_rng() -> SmallRng { + SmallRng::from_entropy() +} + +/// Is this byte part of the canonical penis alphabet? The doorman for +/// `--verify`: any byte that isn't on the list means a non-dick snuck through. +#[inline] +pub fn is_dick_byte(b: u8) -> bool { + matches!(b, b'8' | b'=' | b'#' | b'D' | b'~' | b' ') +} + +/// Splat `n` copies of `ch` into `out` 32 bytes at a time. The SIMD shaft +/// extender — turns a 1-byte char into long, hard rows of repeated symbols +/// with one vector instruction per chunk. +fn fill_run(out: &mut Vec, ch: u8, n: usize) { + if n == 0 { + return; + } + let start = out.len(); + out.resize(start + n, 0); + let slice = &mut out[start..start + n]; + let chunk = u8x32::splat(ch).to_array(); + let mut i = 0; + while i + 32 <= slice.len() { + slice[i..i + 32].copy_from_slice(&chunk); + i += 32; + } + while i < slice.len() { + slice[i] = ch; + i += 1; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_cfg() -> DickConfig { + DickConfig { + shaft_min: 3, + shaft_max: 8, + jizz_min: 0, + jizz_max: 3, + balls_chance: 0.5, + } + } + + #[test] + fn fill_yields_exact_capacity() { + let mut buf = DickBuf::new(test_cfg(), 4096); + buf.fill(); + assert_eq!(buf.as_bytes().len(), 4096); + } + + #[test] + fn output_starts_with_eight() { + let mut buf = DickBuf::new(test_cfg(), 64); + buf.fill(); + assert_eq!(buf.as_bytes()[0], b'8'); + } + + #[test] + fn fill_run_vectorized_matches_scalar() { + let mut a = Vec::new(); + fill_run(&mut a, b'=', 100); + assert_eq!(a.len(), 100); + assert!(a.iter().all(|&b| b == b'=')); + } +} diff --git a/src/drive.rs b/src/drive.rs new file mode 100644 index 0000000..703dff4 --- /dev/null +++ b/src/drive.rs @@ -0,0 +1,376 @@ +//! Tells you whether the thing you're about to plow is a spinning slut +//! (HDD — go nuts) or a delicate flash queen (SSD/NVMe — easy, tiger). +//! +//! Multi-pass overwrites on flash are a double L: ineffective because the +//! FTL remaps your writes to fresh NAND (the original data sits smug in +//! over-provisioned cells), and harmful because every pass eats program/erase +//! cycles off the drive's lifespan. So we refuse flash by default and point +//! you at the drive's native secure-erase, which actually gives the drive a +//! happy ending instead of premature death. + +use std::path::{Path, PathBuf}; +use std::process::Command; + +use anyhow::{anyhow, Context, Result}; + +/// What flavor of storage we're dealing with. Determines whether it can take +/// a multi-pass railing or whether we should treat it like fine china. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum DriveKind { + Nvme, + Sata, // SSD on SATA bus, rotational==0 + Rotational, // Spinning rust, fair game for overwriting + Unknown, // Probably non-Linux or unmounted; treat as safe-to-overwrite +} + +impl DriveKind { + pub fn is_flash(self) -> bool { + matches!(self, DriveKind::Nvme | DriveKind::Sata) + } +} + +/// Whether the path itself is a block device file (e.g. /dev/sda). +pub fn is_block_device(path: &Path) -> bool { + #[cfg(unix)] + { + use std::os::unix::fs::FileTypeExt; + std::fs::metadata(path) + .map(|m| m.file_type().is_block_device()) + .unwrap_or(false) + } + #[cfg(not(unix))] + { + let _ = path; + false + } +} + +/// For a path on a mounted filesystem, find the underlying block device. +/// For a path that already IS a block device, return it as-is. +/// +/// Resolves via `/proc/self/mountinfo` rather than `st_dev`, because some +/// filesystems (notably btrfs subvolumes, bind mounts, overlay) use anonymous +/// block devices with major=0 that don't exist under `/sys/dev/block/`. +#[cfg(target_os = "linux")] +pub fn resolve_block_device(path: &Path) -> Option { + if is_block_device(path) { + return Some(path.to_path_buf()); + } + let canonical = std::fs::canonicalize(path).ok()?; + let mountinfo = std::fs::read_to_string("/proc/self/mountinfo").ok()?; + + let mut best: Option<(usize, PathBuf)> = None; + for line in mountinfo.lines() { + // Format: id parent maj:min root mountpoint opts ... - fstype source super_opts + let mut parts = line.split_whitespace(); + let _id = parts.next(); + let _parent = parts.next(); + let _devno = parts.next(); + let _root = parts.next(); + let mountpoint = match parts.next() { + Some(m) => m, + None => continue, + }; + // Skip optional fields up to the " - " separator + let after_dash = match line.split(" - ").nth(1) { + Some(s) => s, + None => continue, + }; + let mut fields = after_dash.split_whitespace(); + let _fstype = fields.next(); + let source = match fields.next() { + Some(s) => s, + None => continue, + }; + + let mp = PathBuf::from(decode_mountinfo(mountpoint)); + if !canonical.starts_with(&mp) { + continue; + } + let len = mp.as_os_str().len(); + if best.as_ref().is_none_or(|(l, _)| len > *l) { + best = Some((len, PathBuf::from(decode_mountinfo(source)))); + } + } + let (_, src) = best?; + // Pseudo filesystems give sources like "tmpfs", "proc" with no leading slash. + if !src.is_absolute() { + return None; + } + std::fs::canonicalize(&src).ok().or(Some(src)) +} + +/// mountinfo encodes spaces/tabs as octal escapes (\040, \011). Decode them. +#[cfg(target_os = "linux")] +fn decode_mountinfo(s: &str) -> String { + let bytes = s.as_bytes(); + let mut out = String::with_capacity(bytes.len()); + let mut i = 0; + while i < bytes.len() { + if bytes[i] == b'\\' && i + 3 < bytes.len() { + if let Ok(n) = u8::from_str_radix( + std::str::from_utf8(&bytes[i + 1..i + 4]).unwrap_or("000"), + 8, + ) { + out.push(n as char); + i += 4; + continue; + } + } + out.push(bytes[i] as char); + i += 1; + } + out +} + +/// macOS resolution: walk `df -P` output to find the device for a path. +#[cfg(target_os = "macos")] +pub fn resolve_block_device(path: &Path) -> Option { + if is_block_device(path) { + return Some(path.to_path_buf()); + } + let out = Command::new("/bin/df").arg("-P").arg(path).output().ok()?; + if !out.status.success() { + return None; + } + let stdout = std::str::from_utf8(&out.stdout).ok()?; + // Skip header; take first column of second line. + let line = stdout.lines().nth(1)?; + let dev = line.split_whitespace().next()?; + Some(PathBuf::from(dev)) +} + +#[cfg(not(any(target_os = "linux", target_os = "macos")))] +pub fn resolve_block_device(path: &Path) -> Option { + if is_block_device(path) { + Some(path.to_path_buf()) + } else { + // TODO: Windows storage classification. + None + } +} + +/// Classify a block device (e.g. /dev/nvme0n1, /dev/sda, /dev/sda1). +#[cfg(target_os = "linux")] +pub fn classify(dev_path: &Path) -> DriveKind { + let name = match dev_path.file_name().and_then(|s| s.to_str()) { + Some(n) => n, + None => return DriveKind::Unknown, + }; + if name.starts_with("nvme") { + return DriveKind::Nvme; + } + // For partitions like sda1, sysfs queue/ lives on the parent (sda). + let direct = PathBuf::from("/sys/class/block").join(name); + let queue_dir = if direct.join("queue/rotational").exists() { + direct + } else if let Ok(real) = std::fs::canonicalize(&direct) { + match real.parent() { + Some(p) if p.join("queue/rotational").exists() => p.to_path_buf(), + _ => return DriveKind::Unknown, + } + } else { + return DriveKind::Unknown; + }; + match std::fs::read_to_string(queue_dir.join("queue/rotational")) + .ok() + .map(|s| s.trim().to_string()) + .as_deref() + { + Some("0") => DriveKind::Sata, + Some("1") => DriveKind::Rotational, + _ => DriveKind::Unknown, + } +} + +#[cfg(target_os = "macos")] +pub fn classify(dev_path: &Path) -> DriveKind { + // diskutil info -plist emits XML containing SolidState + // followed by or . Heuristic — doesn't distinguish NVMe + // from SATA — but the user-facing advice (use the OS-native secure-erase) + // is universally correct for both, so we classify all flash as `Sata`. + let out = match Command::new("/usr/sbin/diskutil") + .arg("info") + .arg("-plist") + .arg(dev_path) + .output() + { + Ok(o) if o.status.success() => o.stdout, + _ => return DriveKind::Unknown, + }; + let s = match std::str::from_utf8(&out) { + Ok(s) => s, + Err(_) => return DriveKind::Unknown, + }; + if let Some(pos) = s.find("SolidState") { + let tail = &s[pos..]; + if tail.contains("") + && tail.find("") < tail.find("").or(Some(usize::MAX)) + { + return DriveKind::Sata; + } + if tail.contains("") { + return DriveKind::Rotational; + } + } + DriveKind::Unknown +} + +#[cfg(not(any(target_os = "linux", target_os = "macos")))] +pub fn classify(_dev_path: &Path) -> DriveKind { + // TODO: Windows storage classification via Get-PhysicalDisk / IOCTL_STORAGE_QUERY_PROPERTY. + DriveKind::Unknown +} + +/// The bouncer for block-device targets. Returns `Ok(true)` to let the +/// caller plow ahead, `Ok(false)` if we already handled it via secure-erase +/// (drive's own factory reset — clean and consensual), `Err` to slam the +/// brakes because something's flash and the user didn't say --rape. +pub fn handle_block_device(dev: &Path, rape: bool, secure_erase: bool) -> Result { + let kind = classify(dev); + + if secure_erase { + run_secure_erase(dev, kind)?; + return Ok(false); + } + + if kind.is_flash() && !rape { + print_refusal(dev, kind); + return Err(anyhow!( + "refusing to overwrite flash device {:?} (use --rape to override, or --secure-erase)", + dev + )); + } + + Ok(true) +} + +/// Heads-up for the user when their wipe target lives on flash. File-level +/// overwrite on an SSD is theatrical edging — feels good, accomplishes +/// nothing at the NAND level. We still delete the inode (that part works). +pub fn warn_file_on_flash(path: &Path, dev: &Path) { + eprintln!("WARNING: {:?} sits on flash device {:?}.", path, dev); + eprintln!(" File-level overwrite is theatrical on SSD/NVMe: the FTL won't"); + eprintln!(" necessarily clobber the physical NAND page that held this file."); + eprintln!(" The file will still be deleted, but treat the wipe as advisory."); +} + +fn print_refusal(dev: &Path, kind: DriveKind) { + eprintln!(); + eprintln!("8==X Refusing to overwrite {:?}", dev); + eprintln!(); + eprintln!( + "This is flash storage ({:?}). Multi-pass overwrite is:", + kind + ); + eprintln!( + " - INEFFECTIVE: the FTL remaps writes, leaving stale copies in over-provisioned NAND" + ); + eprintln!(" - HARMFUL: each pass consumes finite program/erase cycles"); + eprintln!(); + eprintln!("Use the drive's native secure-erase instead:"); + match kind { + DriveKind::Nvme => { + eprintln!( + " sudo nvme format {} -s 1 # user-data erase", + dev.display() + ); + eprintln!( + " sudo nvme format {} -s 2 # cryptographic erase (SED only)", + dev.display() + ); + eprintln!( + " sudo nvme sanitize {} --sanact=2 # block erase (if supported)", + dev.display() + ); + } + DriveKind::Sata => { + eprintln!( + " sudo hdparm --user-master u --security-set-pass p {}", + dev.display() + ); + eprintln!( + " sudo hdparm --user-master u --security-erase p {}", + dev.display() + ); + eprintln!(); + eprintln!("Or, for a fast non-cryptographic discard:"); + eprintln!(" sudo blkdiscard {}", dev.display()); + } + _ => {} + } + eprintln!(); + eprintln!("For self-encrypting drives (TCG Opal), use `sedutil-cli --revertNoErase`."); + eprintln!(); + eprintln!("To run those for you: pass --secure-erase."); + eprintln!("To bypass this check anyway: pass --rape."); + eprintln!(); +} + +fn run_secure_erase(dev: &Path, kind: DriveKind) -> Result<()> { + match kind { + DriveKind::Nvme => nvme_format(dev), + DriveKind::Sata => hdparm_secure_erase(dev), + DriveKind::Rotational => { + eprintln!( + "NOTE: {:?} is rotational; native secure-erase still applies via hdparm.", + dev + ); + hdparm_secure_erase(dev) + } + DriveKind::Unknown => Err(anyhow!( + "cannot classify {:?} as NVMe or SATA; refuse to guess", + dev + )), + } +} + +fn nvme_format(dev: &Path) -> Result<()> { + eprintln!("Running: nvme format {} -s 1", dev.display()); + let status = Command::new("nvme") + .arg("format") + .arg(dev) + .arg("-s") + .arg("1") + .status() + .context("failed to spawn `nvme` (install nvme-cli)")?; + if !status.success() { + return Err(anyhow!("nvme format failed: {}", status)); + } + Ok(()) +} + +fn hdparm_secure_erase(dev: &Path) -> Result<()> { + // Use a dummy password; the drive doesn't care, it just needs to be set + // to unlock the SECURITY ERASE UNIT command. + let pass = "p"; + eprintln!( + "Running: hdparm --user-master u --security-set-pass {} {}", + pass, + dev.display() + ); + let s1 = Command::new("hdparm") + .args(["--user-master", "u", "--security-set-pass"]) + .arg(pass) + .arg(dev) + .status() + .context("failed to spawn `hdparm`")?; + if !s1.success() { + return Err(anyhow!("hdparm --security-set-pass failed: {}", s1)); + } + eprintln!( + "Running: hdparm --user-master u --security-erase {} {}", + pass, + dev.display() + ); + let s2 = Command::new("hdparm") + .args(["--user-master", "u", "--security-erase"]) + .arg(pass) + .arg(dev) + .status() + .context("failed to spawn `hdparm`")?; + if !s2.success() { + return Err(anyhow!("hdparm --security-erase failed: {}", s2)); + } + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 1a7da05..bb80532 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,175 +1,189 @@ -use std::fs::{self, OpenOptions}; -use std::io::{self, Write}; -use std::path::{Path, PathBuf}; +//! WipeDicks: the only file shredder that finishes with a happy ending. +//! +//! Pointy end of the codebase. Parses args, runs the safety + drive policy +//! gauntlet, then unleashes a bounded thread pool of horny workers on the +//! file list. Each worker takes a target, opens it, fills it with dicks, +//! removes its corpse, and reports back. Ctrl-C drains the in-flight cleanup +//! registry so we don't leave temp files lying around like used condoms. + +mod cleanup; +mod cli; +mod dicks; +mod drive; +mod progress; +mod safety; +mod wipe; + +use std::collections::HashSet; +use std::io::{IsTerminal, Write}; +use std::path::PathBuf; +use std::sync::Mutex; use std::thread; -use rand::prelude::*; -use clap::{Command, Arg}; -use rand::thread_rng; - -const DICKS: &[&str] = &[ - "8=D ", "8=D~ ", "8=D~~ ", "8=D~~~ ", "8==D ", "8==D~ ", "8==D~~ ", "8==D~~~ ", "8===D ", "8===D~ ", "8===D~~ ", "8===D~~~ ", "8====D ", "8====D~ ", "8====D~~ ", "8====D~~~ ", "8=====D ", "8=====D~ ", "8=====D~~ ", "8=====D~~~ ", "8======D ", "8======D~ ", "8======D~~ ", "8======D~~~ ", "8=======D ", "8=======D~ ", "8=======D~~ ", "8=======D~~~ ", "8========D ", "8========D~ ", "8========D~~ ", "8========D~~~ ", "8=========D ", "8=========D~ ", "8=========D~~ ", "8=========D~~~ ", "8==========D ", "8==========D~ ", "8==========D~~ ", "8==========D~~~ ", "8===========D ", "8===========D~ ", "8===========D~~ ", "8===========D~~~ ", "8============D ", "8============D~ ", "8============D~~ ", "8============D~~~ ", "8#=D ", "8#=D~ ", "8#=D~~ ", "8#=D~~~ ", "8#==D ", "8#==D~ ", "8#==D~~ ", "8#==D~~~ ", "8#===D ", "8#===D~ ", "8#===D~~ ", "8#===D~~~ ", "8#====D ", "8#====D~ ", "8#====D~~ ", "8#====D~~~ ", "8#=====D ", "8#=====D~ ", "8#=====D~~ ", "8#=====D~~~ ", "8#======D ", "8#======D~ ", "8#======D~~ ", "8#======D~~~ ", "8#=======D ", "8#=======D~ ", "8#=======D~~ ", "8#=======D~~~ ", "8#========D ", "8#========D~ ", "8#========D~~ ", "8#========D~~~ ", "8#=========D ", "8#=========D~ ", "8#=========D~~ ", "8#=========D~~~ ", "8#==========D ", "8#==========D~ ", "8#==========D~~ ", "8#==========D~~~ ", "8#===========D ", "8#===========D~ ", "8#===========D~~ ", "8#===========D~~~ ", "8#============D ", "8#============D~ ", "8#============D~~ ", "8#============D~~~ ", -]; - -fn generate_dicks() -> Vec { - let mut dicks = Vec::new(); - for a in 0..2 { - for b in 1..13 { - for c in 0..4 { - let dick = format!("8{}{}D{} ", "#".repeat(a), "=".repeat(b), "~".repeat(c)); - dicks.push(dick); - } - } - } - dicks -} -fn rand_dick(rng: &mut ThreadRng) -> &'static str { - let index = rng.gen_range(0..DICKS.len()); - DICKS[index] -} +use anyhow::Result; -fn fast_rand_dick<'a>(cache: &'a mut String, count: &mut usize, rng: &mut ThreadRng) -> &'a str { - if cache.is_empty() || *count == 0 { - *cache = String::new(); - *count = rng.gen_range(1000..10000); - for _ in 0..rng.gen_range(150..300) { - cache.push_str(rand_dick(rng)); - } +use crate::cli::Args; +use crate::dicks::{push_one, DickConfig}; +use crate::progress::Progress; + +fn main() { + let args = cli::parse(); + if let Err(msg) = args.validate() { + eprintln!("error: {msg}"); + std::process::exit(2); } - *count -= 1; - cache -} + let cfg = DickConfig { + shaft_min: args.shaft_min, + shaft_max: args.shaft_max, + jizz_min: args.jizz_min, + jizz_max: args.jizz_max, + balls_chance: args.balls_chance, + }; -fn wipe(dev: &Path, rounds: usize, rng: &mut ThreadRng) -> io::Result<()> { - let size = fs::metadata(dev).map(|m| m.len()).unwrap_or(0); + if args.dry_run { + run_dry(&cfg, args.buffer_size); + return; + } - for _ in 0..rounds { - let mut file = OpenOptions::new().write(true).open(dev)?; + if let Err(e) = cleanup::install_handler() { + eprintln!("warning: failed to install signal handler: {e:#}"); + } - if size == 0 { - loop { - let dick = rand_dick(rng); - if file.write_all(dick.as_bytes()).is_err() { - break; - } - } - } else { - let mut dlen = 0; - while dlen < size { - let dick = rand_dick(rng); - dlen += dick.len() as u64; - if file.write_all(dick.as_bytes()).is_err() { - break; - } - } + match run(args, cfg) { + Ok(0) => {} + Ok(n) => { + eprintln!("{n} target(s) failed"); + std::process::exit(1); + } + Err(e) => { + eprintln!("error: {e:#}"); + std::process::exit(1); } } - - fs::remove_file(dev)?; - - Ok(()) } -fn parse_dir(dir: &Path, recursive: bool) -> io::Result> { - let mut filelist = Vec::new(); - - for entry in fs::read_dir(dir)? { - let entry = entry?; - let path = entry.path(); - if path.is_dir() { - if recursive { - filelist.extend(parse_dir(&path, recursive)?); - } - } else { - filelist.push(path); - } +/// Squirt one buffer of generated dicks to stdout, then bail. For previewing +/// your --shaft-* / --jizz-* / --balls-chance settings without actually +/// ruining anyone's day. Visual edging only — no penetration. +fn run_dry(cfg: &DickConfig, buf_size: usize) { + let mut rng = dicks::new_rng(); + let mut out = Vec::with_capacity(buf_size); + while out.len() < buf_size { + push_one(&mut rng, cfg, &mut out); } - Ok(filelist) + out.truncate(buf_size); + let _ = std::io::stdout().write_all(&out); } -fn parse_filelist(filelist: &[PathBuf], recursive: bool) -> io::Result> { - let mut files = Vec::new(); - for item in filelist { - if item.is_dir() { - if recursive { - files.extend(parse_dir(item, recursive)?); - } else { - eprintln!("WARNING: {:?} is a directory and recursive is off.", item) +/// The main fuckening. Returns the number of files that failed to take a +/// proper dicking (caller sets the exit code based on that). +fn run(args: Args, cfg: DickConfig) -> Result { + // Cock-block check on the raw inputs. Catches `wipedicks /` or + // `wipedicks -r /etc` BEFORE we walk a huge tree and refuse one file at a + // time. Nobody wants to find out their hand slipped on /usr/. + if !args.rape { + for f in &args.files { + let is_block = drive::is_block_device(f); + if let Err(hazard) = safety::check_path(f, is_block) { + safety::explain(f, hazard); + return Ok(1); } - } else if item.exists() { - files.push(item.to_path_buf()); } } - Ok(files) -} -fn main() { - let matches = Command::new("Wipe files/devices with dicks") - .version("0.0.1") - .author("vxfemboy") - .arg(Arg::new("recursive") - .short('r') - .long("recursive") - .help("Recursively wipe directories") - .action(clap::ArgAction::SetTrue) - ) - .arg(Arg::new("numrounds") - .short('n') - .long("numrounds") - .help("The number of rounds to wipe the file/device") - .value_parser(clap::value_parser!(usize)) - .default_value("1") - ) - .arg(Arg::new("wipefree") - .short('w') - .long("wipefree") - .help("Wipe free space on device") - .action(clap::ArgAction::SetTrue) - ) - .arg(Arg::new("slow") - .short('s') - .long("slow") - .help("Use more randomness, tends to be slower") - .action(clap::ArgAction::SetTrue) - ) - .arg(Arg::new("files") - .help("Files or directories to wipe") - .num_args(1..) - .required(true) - ) - .get_matches(); - - let recursive = matches.get_flag("recursive"); - let numrounds: usize = *matches.get_one("numrounds").unwrap(); - let slow = matches.get_flag("slow"); - let wipefree = matches.get_flag("wipefree"); - - let initial_file_list: Vec = matches.get_many::("files").unwrap().map(|s| PathBuf::from(s)).collect(); - - let file_list = parse_filelist(&initial_file_list, recursive).unwrap(); - - let file_list = if wipefree { - let mut fl = file_list; - fl.push(PathBuf::from("dick.tmp")); - fl - } else { - file_list - }; - - let mut handles = Vec::new(); + let mut files = wipe::parse_filelist(&args.files, args.recursive)?; + if files.is_empty() && !args.wipefree { + eprintln!("nothing to wipe"); + return Ok(0); + } - for f in file_list { - let handle = thread::spawn(move || { - let mut rng = thread_rng(); // create new threads uwu - if let Err(e) = wipe(&f, numrounds, &mut rng) { - eprintln!("ERROR: {:?}: {:?}", f, e); + // Path safety: refuse system paths, swap, mounted block devices. + // Drive safety: refuse / delegate for block-device targets up front, + // warn once per unique flash device for regular files. + let mut warned_devices: HashSet = HashSet::new(); + let mut keep: Vec = Vec::with_capacity(files.len()); + for f in files.drain(..) { + let is_block = drive::is_block_device(&f); + if !args.rape { + if let Err(hazard) = safety::check_path(&f, is_block) { + safety::explain(&f, hazard); + return Ok(1); + } + } + if is_block { + match drive::handle_block_device(&f, args.rape, args.secure_erase) { + Ok(true) => keep.push(f), + Ok(false) => { /* secure_erase already ran; skip overwrite */ } + Err(e) => { + eprintln!("{e:#}"); + return Ok(1); + } } - }); - handles.push(handle); + } else { + if let Some(dev) = drive::resolve_block_device(&f) { + if drive::classify(&dev).is_flash() + && warned_devices.insert(dev.clone()) + && !args.rape + { + drive::warn_file_on_flash(&f, &dev); + } + } + keep.push(f); + } } - - for handle in handles { - handle.join().unwrap(); + let files = keep; + + let progress_enabled = !args.no_progress && std::io::stderr().is_terminal(); + let progress = Progress::new(progress_enabled, args.bar_width, args.gay); + + let workers = thread::available_parallelism() + .map(|n| n.get()) + .unwrap_or(4) + .min(files.len().max(1)); + + let queue: Mutex> = Mutex::new(files); + let failures = std::sync::atomic::AtomicU32::new(0); + + thread::scope(|s| { + for _ in 0..workers { + let queue = &queue; + let cfg = &cfg; + let progress = &progress; + let failures = &failures; + let args = &args; + s.spawn(move || loop { + let path = match queue.lock().unwrap().pop() { + Some(p) => p, + None => break, + }; + let total = std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0); + let pb = progress.add_file(&path, total); + let res = wipe::wipe_file( + &path, + args.rounds, + cfg, + args.buffer_size, + args.slow, + args.verify, + pb.as_ref(), + ); + if let Err(e) = res { + eprintln!("ERROR wiping {:?}: {:#}", path, e); + failures.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } + }); + } + }); + + if args.wipefree { + // Use the first input as a mount hint; fall back to CWD. + let hint = args.files.first().cloned().unwrap_or(PathBuf::from(".")); + let pb = progress.add_file(&PathBuf::from("free space"), 0); + if let Err(e) = wipe::wipe_freespace(&hint, &cfg, args.buffer_size, pb.as_ref()) { + eprintln!("ERROR wiping free space: {e:#}"); + failures.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } } -} + Ok(failures.load(std::sync::atomic::Ordering::Relaxed)) +} diff --git a/src/progress.rs b/src/progress.rs new file mode 100644 index 0000000..a2e8056 --- /dev/null +++ b/src/progress.rs @@ -0,0 +1,421 @@ +//! Live ASCII-cock progress theatre. +//! +//! Each wipe target gets its own bar — penis grows from `8D` to `8==============D` +//! as the file dies. Jizz drips in bright white. Bytes go yellow → green like +//! a traffic light flipping to "ohyeah". The trailing filename is painted red +//! so you can see exactly whose data is currently getting plowed. +//! +//! On completion, the file gets a custom send-off: one of ~20 verbs (fucked, +//! creamed, milked, nuked...) chosen at random, each with its own color and +//! formatting. Because "wiped" was getting boring. +//! +//! Format (resolves GitHub issue #1): +//! [8D~~~~~~~~~~~~~~~] 0.00 B/64.00 MiB 0% /path/to/victim +//! [8=======D~~~~~~~~] 32.00 MiB/64.00 MiB 50% /path/to/victim +//! [8===============D] 64.00 MiB/64.00 MiB 100% /path/to/victim 💦 creamed +//! +//! `indicatif`'s built-in `progress_chars` treats the middle character as a +//! "tip" that disappears at 100%, so we render the bar ourselves via a custom +//! `with_key` callback to guarantee the `D` stays in frame all the way to the +//! right edge — wouldn't want to lose the head at the finish line. + +use std::path::Path; + +use console::Style; +use indicatif::{HumanBytes, MultiProgress, ProgressBar, ProgressState, ProgressStyle}; +use rand::Rng; + +// xterm-256 indices for the gay rainbow (ROYGBIV) — applied to the shaft. +const RAINBOW: &[u8] = &[196, 208, 226, 46, 51, 21, 201]; + +// Yellow → green gradient for the wiped-bytes counter and percent. +const GRADIENT_FULL: u8 = 46; // bright green — finishes hard +const TOTAL_COLOR: u8 = 51; // cyan — the goalposts +const JIZZ_COLOR: u8 = 231; // bright white — pearl drip +const PATH_COLOR: u8 = 196; // red — the victim's name in lights + +fn gradient_for(frac: f32) -> u8 { + if frac >= 1.0 { + return GRADIENT_FULL; + } + match (frac * 10.0) as u32 { + 0..=1 => 226, // bright yellow — just getting hard + 2..=3 => 227, // yellow + 4..=5 => 191, // yellow-green + 6..=7 => 154, // lime-yellow + _ => 82, // lime green — about to bust + } +} + +fn paint(s: &str, color: u8, enabled: bool) -> String { + if enabled { + Style::new() + .force_styling(true) + .color256(color) + .apply_to(s) + .to_string() + } else { + s.to_string() + } +} + +/// Wrap a path in red ANSI for the progress message tail. +pub fn red_path(path: &Path) -> String { + if console::colors_enabled_stderr() { + paint(&path.display().to_string(), PATH_COLOR, true) + } else { + path.display().to_string() + } +} + +/// One of the many ways a dick can finish off a file. +struct Verb { + word: &'static str, + color: u8, + bold: bool, + underline: bool, + italic: bool, + /// Optional emoji prefix. Goes BEFORE the styled word, with a space. + emoji: Option<&'static str>, +} + +/// The hall of fame. Every wipe ends with one of these, randomly picked. +const VERBS: &[Verb] = &[ + Verb { + word: "fucked", + color: 196, + bold: true, + underline: true, + italic: false, + emoji: None, + }, + Verb { + word: "wiped", + color: 46, + bold: true, + underline: false, + italic: false, + emoji: None, + }, + Verb { + word: "deleted", + color: 196, + bold: true, + underline: false, + italic: false, + emoji: None, + }, + Verb { + word: "destroyed", + color: 196, + bold: true, + underline: false, + italic: false, + emoji: Some("💀"), + }, + Verb { + word: "obliterated", + color: 208, + bold: true, + underline: true, + italic: false, + emoji: Some("💥"), + }, + Verb { + word: "purged", + color: 226, + bold: true, + underline: false, + italic: false, + emoji: None, + }, + Verb { + word: "creamed", + color: 231, + bold: true, + underline: false, + italic: false, + emoji: Some("💦"), + }, + Verb { + word: "milked", + color: 250, + bold: false, + underline: false, + italic: false, + emoji: Some("🥛"), + }, + Verb { + word: "blasted", + color: 196, + bold: true, + underline: false, + italic: false, + emoji: Some("💥"), + }, + Verb { + word: "pissed on", + color: 226, + bold: false, + underline: false, + italic: true, + emoji: Some("💦"), + }, + Verb { + word: "annihilated", + color: 196, + bold: true, + underline: true, + italic: false, + emoji: Some("☢"), + }, + Verb { + word: "demolished", + color: 208, + bold: true, + underline: false, + italic: false, + emoji: None, + }, + Verb { + word: "ravaged", + color: 197, + bold: true, + underline: false, + italic: false, + emoji: None, + }, + Verb { + word: "pounded", + color: 201, + bold: true, + underline: false, + italic: false, + emoji: Some("🍆"), + }, + Verb { + word: "nuked", + color: 226, + bold: true, + underline: false, + italic: false, + emoji: Some("☢"), + }, + Verb { + word: "gangbanged", + color: 201, + bold: true, + underline: true, + italic: false, + emoji: None, + }, + Verb { + word: "railed", + color: 165, + bold: true, + underline: false, + italic: false, + emoji: None, + }, + Verb { + word: "splooged on", + color: 231, + bold: false, + underline: false, + italic: true, + emoji: Some("💦"), + }, + Verb { + word: "drilled", + color: 208, + bold: true, + underline: false, + italic: false, + emoji: None, + }, + Verb { + word: "facialized", + color: 231, + bold: true, + underline: true, + italic: false, + emoji: Some("💦"), + }, + Verb { + word: "jizzed on", + color: 231, + bold: true, + underline: false, + italic: false, + emoji: Some("💦"), + }, + Verb { + word: "creampied", + color: 199, + bold: true, + underline: false, + italic: false, + emoji: Some("🍑"), + }, + Verb { + word: "rawdogged", + color: 197, + bold: true, + underline: true, + italic: false, + emoji: None, + }, +]; + +fn pick_verb() -> &'static Verb { + let idx = rand::thread_rng().gen_range(0..VERBS.len()); + &VERBS[idx] +} + +fn style_verb(v: &Verb) -> String { + let mut style = Style::new().force_styling(true).color256(v.color); + if v.bold { + style = style.bold(); + } + if v.underline { + style = style.underlined(); + } + if v.italic { + style = style.italic(); + } + style.apply_to(v.word).to_string() +} + +/// Bury a file with a randomly chosen send-off. Used by `wipe::wipe_file` +/// when the deed is done. +pub fn celebrate(pb: &ProgressBar, path: &Path) { + let colors = console::colors_enabled_stderr(); + let verb = pick_verb(); + let body = if colors { + let path_str = paint(&path.display().to_string(), PATH_COLOR, true); + let styled = style_verb(verb); + match verb.emoji { + Some(e) => format!("{} {} {}", path_str, e, styled), + None => format!("{} {}", path_str, styled), + } + } else { + match verb.emoji { + Some(e) => format!("{} {} {}", path.display(), e, verb.word), + None => format!("{} {}", path.display(), verb.word), + } + }; + pb.finish_with_message(body); +} + +/// Mark a wipe that shit the bed. Bright-red, bold "FUCKED UP" so the user +/// notices something failed in a sea of green successes. +pub fn lament(pb: &ProgressBar, path: &Path, err: &str) { + let colors = console::colors_enabled_stderr(); + let body = if colors { + let path_str = paint(&path.display().to_string(), PATH_COLOR, true); + let tag = Style::new() + .force_styling(true) + .color256(196) + .bold() + .apply_to("FUCKED UP") + .to_string(); + format!("{} {}: {}", path_str, tag, err) + } else { + format!("{} FUCKED UP: {}", path.display(), err) + }; + pb.abandon_with_message(body); +} + +#[derive(Clone)] +pub struct Progress { + multi: Option, + bar_width: usize, + gay: bool, +} + +impl Progress { + pub fn new(enabled: bool, bar_width: usize, gay: bool) -> Self { + Self { + multi: enabled.then(MultiProgress::new), + bar_width, + gay, + } + } + + pub fn add_file(&self, path: &Path, total_bytes: u64) -> Option { + let multi = self.multi.as_ref()?; + let pb = multi.add(ProgressBar::new(total_bytes)); + pb.set_style(self.style()); + pb.set_message(red_path(path)); + Some(pb) + } + + fn style(&self) -> ProgressStyle { + let inner = self.bar_width.saturating_sub(1).max(2); + let gay = self.gay; + let colors = console::colors_enabled_stderr(); + + let template = "[{dickbar}] {bytes}/{total_bytes} {percent}% {msg}"; + ProgressStyle::with_template(template) + .expect("static template is valid") + .with_key( + "dickbar", + move |state: &ProgressState, w: &mut dyn std::fmt::Write| { + let frac = state.fraction().clamp(0.0, 1.0); + let shaft = ((inner - 1) as f32 * frac).round() as usize; + let shaft = shaft.min(inner - 1); + let jizz = inner - 1 - shaft; + let bar: String = std::iter::once('8') + .chain((0..shaft).map(|_| '=')) + .chain(std::iter::once('D')) + .chain((0..jizz).map(|_| '~')) + .collect(); + + if colors { + // Jizz is white in every mode — that's the whole point. + // The shaft only goes rainbow when --gay is on. + let mut color_idx = 0; + for ch in bar.chars() { + if ch == '~' { + let style = Style::new().force_styling(true).color256(JIZZ_COLOR); + let _ = write!(w, "{}", style.apply_to(ch)); + } else if gay { + let style = Style::new() + .force_styling(true) + .color256(RAINBOW[color_idx % RAINBOW.len()]); + let _ = write!(w, "{}", style.apply_to(ch)); + color_idx += 1; + } else { + let _ = w.write_char(ch); + } + } + } else { + let _ = w.write_str(&bar); + } + }, + ) + .with_key( + "bytes", + move |state: &ProgressState, w: &mut dyn std::fmt::Write| { + let text = format!("{:>10}", HumanBytes(state.pos()).to_string()); + let _ = w.write_str(&paint(&text, gradient_for(state.fraction()), colors)); + }, + ) + .with_key( + "total_bytes", + move |state: &ProgressState, w: &mut dyn std::fmt::Write| { + let total = state.len().unwrap_or(0); + let text = format!("{:<10}", HumanBytes(total).to_string()); + let _ = w.write_str(&paint(&text, TOTAL_COLOR, colors)); + }, + ) + .with_key( + "percent", + move |state: &ProgressState, w: &mut dyn std::fmt::Write| { + let p = (state.fraction() * 100.0).round() as u32; + let text = format!("{:>3}", p); + let _ = w.write_str(&paint(&text, gradient_for(state.fraction()), colors)); + }, + ) + } +} diff --git a/src/safety.rs b/src/safety.rs new file mode 100644 index 0000000..5bdfd80 --- /dev/null +++ b/src/safety.rs @@ -0,0 +1,171 @@ +//! Cock-block department. Stops you from face-fucking your /etc/ when your +//! hand slipped, your /boot/ when you meant /tmp/, or the swap file your +//! kernel is currently using to remember things. +//! +//! Refuses by default; the --rape safe-word bypasses every check. We don't +//! ass-blast your filesystem just because you fat-fingered a path. + +use std::fmt; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Hazard { + /// Path is, or is inside, a system directory we shouldn't touch. + SystemPath, + /// Path is currently registered as a swap file/device. + SwapFile, + /// Path is a block device that's currently mounted somewhere. + MountedDevice, +} + +impl fmt::Display for Hazard { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Hazard::SystemPath => write!(f, "system path (refusing to wipe OS files)"), + Hazard::SwapFile => write!(f, "active swap (listed in /proc/swaps)"), + Hazard::MountedDevice => write!(f, "block device currently mounted"), + } + } +} + +/// Hard-coded prefixes wipedicks refuses by default. The current working +/// directory and `/home` are deliberately NOT on this list — users routinely +/// wipe their own data there. +const SYSTEM_PREFIXES: &[&str] = &[ + "/boot", "/etc", "/usr", "/var", "/lib", "/lib64", "/lib32", "/bin", "/sbin", "/sys", "/proc", + "/dev", "/root", "/srv", "/opt", +]; + +pub fn check_path(path: &Path, is_block: bool) -> Result<(), Hazard> { + if is_dangerous_prefix(path) { + return Err(Hazard::SystemPath); + } + if is_swap(path) { + return Err(Hazard::SwapFile); + } + if is_block && is_mounted_block_device(path) { + return Err(Hazard::MountedDevice); + } + Ok(()) +} + +fn is_dangerous_prefix(path: &Path) -> bool { + let canonical = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf()); + + // The root itself. + if canonical == Path::new("/") { + return true; + } + for prefix in SYSTEM_PREFIXES { + let p = Path::new(prefix); + if canonical == p || canonical.starts_with(p) { + return true; + } + } + false +} + +#[cfg(target_os = "linux")] +fn is_swap(path: &Path) -> bool { + let canonical = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf()); + let swaps = match std::fs::read_to_string("/proc/swaps") { + Ok(s) => s, + Err(_) => return false, + }; + swaps + .lines() + .skip(1) // header + .filter_map(|line| line.split_whitespace().next()) + .map(PathBuf::from) + .any(|p| p == canonical) +} + +#[cfg(not(target_os = "linux"))] +fn is_swap(_path: &Path) -> bool { + false +} + +#[cfg(target_os = "linux")] +fn is_mounted_block_device(dev: &Path) -> bool { + let canonical = std::fs::canonicalize(dev).unwrap_or_else(|_| dev.to_path_buf()); + let mountinfo = match std::fs::read_to_string("/proc/self/mountinfo") { + Ok(s) => s, + Err(_) => return false, + }; + for line in mountinfo.lines() { + let after_dash = match line.split(" - ").nth(1) { + Some(s) => s, + None => continue, + }; + let mut fields = after_dash.split_whitespace(); + let _fstype = fields.next(); + if let Some(src) = fields.next() { + let src_path = PathBuf::from(src); + if src_path == canonical { + return true; + } + if let Ok(resolved) = std::fs::canonicalize(&src_path) { + if resolved == canonical { + return true; + } + } + } + } + false +} + +#[cfg(not(target_os = "linux"))] +fn is_mounted_block_device(_dev: &Path) -> bool { + // Without /proc/self/mountinfo we don't have a portable way to check. + // Err on the side of letting the user proceed; --secure-erase delegation + // and the OS-level "device busy" error provide a second line of defense. + false +} + +pub fn explain(path: &Path, hazard: Hazard) { + eprintln!(); + eprintln!("8==X Refusing to wipe {:?}", path); + eprintln!(" Reason: {}", hazard); + eprintln!(); + match hazard { + Hazard::SystemPath => { + eprintln!(" Wiping OS files breaks your machine. If you really mean to,"); + eprintln!(" pass --rape."); + } + Hazard::SwapFile => { + eprintln!(" Disable swap first: sudo swapoff {}", path.display()); + eprintln!(" Then re-run, or pass --rape to wipe anyway."); + } + Hazard::MountedDevice => { + eprintln!(" Unmount first: sudo umount {}", path.display()); + eprintln!(" Then re-run, or pass --rape to wipe anyway."); + } + } + eprintln!(); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn root_is_refused() { + assert!(is_dangerous_prefix(Path::new("/"))); + } + + #[test] + fn etc_passwd_is_refused() { + assert!(is_dangerous_prefix(Path::new("/etc/passwd"))); + } + + #[test] + fn etc_lookalike_is_allowed() { + // /home/etc-backup is not /etc. + assert!(!is_dangerous_prefix(Path::new("/home/etc-backup"))); + } + + #[test] + fn tmp_is_allowed() { + assert!(!is_dangerous_prefix(Path::new("/tmp/anything"))); + } +} diff --git a/src/wipe.rs b/src/wipe.rs new file mode 100644 index 0000000..9800039 --- /dev/null +++ b/src/wipe.rs @@ -0,0 +1,306 @@ +//! The plowing floor. Where files come to get railed and not come back. +//! +//! `wipe_file` takes one target, fills it with N rounds of dicks (fresh +//! variety per round), syncs to disk, and unlinks the corpse. Block devices +//! get the same treatment but skip the unlink — you don't `rm /dev/sda1`. +//! `wipe_freespace` fills the FS with dicks until ENOSPC, then deletes the +//! temp file. Both paths register with the cleanup module so a Ctrl-C +//! doesn't leave used files lying around. + +use std::fs::{self, File, OpenOptions}; +use std::io::{Read, Seek, SeekFrom, Write}; +use std::path::{Path, PathBuf}; + +use anyhow::{anyhow, Context, Result}; +use indicatif::ProgressBar; + +use crate::cleanup; +use crate::dicks::{is_dick_byte, push_one, DickBuf, DickConfig}; +use crate::drive; + +/// Expand the user's input into a flat list of victims. Directories get +/// walked iff `--recursive`; block devices come through untouched. +pub fn parse_filelist(inputs: &[PathBuf], recursive: bool) -> Result> { + let mut out = Vec::new(); + for item in inputs { + if drive::is_block_device(item) { + out.push(item.clone()); + } else if item.is_dir() { + if recursive { + walk_dir(item, &mut out)?; + } else { + eprintln!( + "WARNING: {:?} is a directory and --recursive is off; skipping", + item + ); + } + } else if item.exists() { + out.push(item.clone()); + } else { + eprintln!("WARNING: {:?} does not exist; skipping", item); + } + } + Ok(out) +} + +fn walk_dir(dir: &Path, out: &mut Vec) -> Result<()> { + for entry in fs::read_dir(dir).with_context(|| format!("read_dir {:?}", dir))? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + walk_dir(&path, out)?; + } else { + out.push(path); + } + } + Ok(()) +} + +/// How much do we need to fill? For a regular file, the file size. For a +/// block device, `metadata().len()` lies (returns 0), so we measure via +/// `seek(End)` instead — the only way to find out how big a dick the +/// hardware can take. +fn target_size(path: &Path) -> Result { + let mut f = OpenOptions::new() + .read(true) + .open(path) + .with_context(|| format!("open(read) {:?}", path))?; + let size = f.seek(SeekFrom::End(0))?; + Ok(size) +} + +/// Give one target the full treatment: overwrite `rounds` times, sync, +/// then unlink its corpse. `remove_file` always runs on regular files — a +/// half-pumped file in your directory is more embarrassing than no file at +/// all. Block devices keep their inode; we just bukkake the storage. +pub fn wipe_file( + path: &Path, + rounds: usize, + cfg: &DickConfig, + buf_size: usize, + slow: bool, + verify: bool, + progress: Option<&ProgressBar>, +) -> Result<()> { + let is_block = drive::is_block_device(path); + let size = target_size(path)?; + if let Some(pb) = progress { + // Each round writes `size`; verify (if enabled) reads `size` per round. + let per_round = if verify { size.saturating_mul(2) } else { size }; + pb.set_length(per_round.saturating_mul(rounds as u64)); + } + + if !is_block { + cleanup::register(path); + } + + let result: Result<()> = if slow { + wipe_slow(path, rounds, cfg, size, verify, progress) + } else { + wipe_buffered(path, rounds, cfg, buf_size, size, verify, progress) + }; + + if !is_block { + // Normal-exit cleanup. The Ctrl-C handler will hit this same path + // if it fired first; remove_file is idempotent against ENOENT. + if let Err(e) = fs::remove_file(path) { + if e.kind() != std::io::ErrorKind::NotFound { + eprintln!("WARNING: failed to remove {:?}: {}", path, e); + } + } + cleanup::unregister(path); + } + + if let Some(pb) = progress { + match &result { + Ok(()) => crate::progress::celebrate(pb, path), + Err(e) => crate::progress::lament(pb, path, &format!("{:#}", e)), + } + } + + result +} + +fn wipe_buffered( + path: &Path, + rounds: usize, + cfg: &DickConfig, + buf_size: usize, + size: u64, + verify: bool, + progress: Option<&ProgressBar>, +) -> Result<()> { + let mut buf = DickBuf::new(cfg.clone(), buf_size); + let mut file = OpenOptions::new() + .write(true) + .open(path) + .with_context(|| format!("open(write) {:?}", path))?; + + for _round in 0..rounds { + // Refill ONCE per round. Within a round we rewrite the same dicks for + // every chunk: the content is non-cryptographic ASCII art, so reusing + // the buffer is invisible. Across rounds we get fresh variety. + buf.fill(); + file.seek(SeekFrom::Start(0))?; + let mut written: u64 = 0; + while written < size { + let remaining = size - written; + let bytes = buf.as_bytes(); + let chunk = &bytes[..bytes.len().min(remaining as usize)]; + file.write_all(chunk) + .with_context(|| format!("write_all {:?}", path))?; + written += chunk.len() as u64; + if let Some(pb) = progress { + pb.inc(chunk.len() as u64); + } + } + file.sync_data().ok(); + if verify { + verify_file(path, size, progress)?; + } + } + Ok(()) +} + +fn wipe_slow( + path: &Path, + rounds: usize, + cfg: &DickConfig, + size: u64, + verify: bool, + progress: Option<&ProgressBar>, +) -> Result<()> { + let mut rng = crate::dicks::new_rng(); + let mut scratch: Vec = Vec::with_capacity(cfg.max_dick_len()); + let mut file = OpenOptions::new() + .write(true) + .open(path) + .with_context(|| format!("open(write) {:?}", path))?; + + for _round in 0..rounds { + file.seek(SeekFrom::Start(0))?; + let mut written: u64 = 0; + while written < size { + scratch.clear(); + push_one(&mut rng, cfg, &mut scratch); + let remaining = size - written; + let chunk = &scratch[..scratch.len().min(remaining as usize)]; + file.write_all(chunk) + .with_context(|| format!("write_all {:?}", path))?; + written += chunk.len() as u64; + if let Some(pb) = progress { + pb.inc(chunk.len() as u64); + } + } + file.sync_data().ok(); + if verify { + verify_file(path, size, progress)?; + } + } + Ok(()) +} + +/// Read the freshly-wiped file back and make sure every single byte is in +/// the canonical penis alphabet. If something other than a dick shows up, +/// the drive cheated on us (bad sector, FS shenanigans, mid-pump interrupt). +fn verify_file(path: &Path, size: u64, progress: Option<&ProgressBar>) -> Result<()> { + let mut file = File::open(path).with_context(|| format!("open(read) {:?}", path))?; + let mut buf = vec![0u8; 64 * 1024]; + let mut offset: u64 = 0; + while offset < size { + let want = ((size - offset) as usize).min(buf.len()); + let n = file + .read(&mut buf[..want]) + .with_context(|| format!("read {:?}", path))?; + if n == 0 { + return Err(anyhow!( + "verify: file shorter than expected at offset {}", + offset + )); + } + if let Some(bad) = buf[..n].iter().position(|b| !is_dick_byte(*b)) { + return Err(anyhow!( + "verify: non-dick byte 0x{:02x} at offset {}", + buf[bad], + offset + bad as u64 + )); + } + offset += n as u64; + if let Some(pb) = progress { + pb.inc(n as u64); + } + } + Ok(()) +} + +/// Cum-soak the filesystem's free space. Creates one big temp file under +/// the given mount, writes dicks until the FS taps out (ENOSPC), then +/// deletes the temp file so the disk's "free" again — but every previously +/// dead-but-not-overwritten block now holds a dick instead of your old data. +pub fn wipe_freespace( + mount_hint: &Path, + cfg: &DickConfig, + buf_size: usize, + progress: Option<&ProgressBar>, +) -> Result<()> { + let dir = if mount_hint.is_dir() { + mount_hint.to_path_buf() + } else { + mount_hint + .parent() + .map(|p| p.to_path_buf()) + .unwrap_or_else(|| PathBuf::from(".")) + }; + let temp = dir.join("wipedicks-freespace.tmp"); + cleanup::register(&temp); + + let mut buf = DickBuf::new(cfg.clone(), buf_size); + let mut file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&temp) + .with_context(|| format!("create {:?}", temp))?; + + // Fill once; the freespace fill is a single pass — reuse the same buffer + // for every chunk. + buf.fill(); + let result = freespace_loop(&mut file, &buf, progress); + + drop(file); + if let Err(e) = fs::remove_file(&temp) { + if e.kind() != std::io::ErrorKind::NotFound { + eprintln!("WARNING: failed to remove {:?}: {}", temp, e); + } + } + cleanup::unregister(&temp); + if let Some(pb) = progress { + match &result { + Ok(()) => crate::progress::celebrate(pb, std::path::Path::new("free space")), + Err(e) => { + crate::progress::lament(pb, std::path::Path::new("free space"), &format!("{:#}", e)) + } + } + } + result +} + +fn freespace_loop( + file: &mut std::fs::File, + buf: &DickBuf, + progress: Option<&ProgressBar>, +) -> Result<()> { + loop { + match file.write_all(buf.as_bytes()) { + Ok(()) => { + if let Some(pb) = progress { + pb.inc(buf.as_bytes().len() as u64); + } + } + Err(e) if e.raw_os_error() == Some(libc::ENOSPC) => break, + Err(e) => return Err(e).context("freespace write"), + } + } + file.sync_data().ok(); + Ok(()) +} From 3fe0269f9e20d9c5b6ab78524fadd2c554f2c1b9 Mon Sep 17 00:00:00 2001 From: Zoa Hickenlooper Date: Thu, 14 May 2026 12:21:33 -0700 Subject: [PATCH 2/4] =?UTF-8?q?8=3D=E2=9C=8A=F0=9F=8F=BB=3DD=F0=9F=92=A6?= =?UTF-8?q?=F0=9F=91=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 1 - Cargo.toml | 3 --- src/drive.rs | 4 ++++ src/safety.rs | 9 ++++++++- src/wipe.rs | 4 +++- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 24c298d..e84ba10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -601,7 +601,6 @@ dependencies = [ "console", "ctrlc", "indicatif", - "libc", "rand", "wide", ] diff --git a/Cargo.toml b/Cargo.toml index 670a000..14a7015 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,9 +18,6 @@ indicatif = "0.17" rand = { version = "0.8", features = ["small_rng"] } wide = "0.7" -[target.'cfg(unix)'.dependencies] -libc = "0.2" - [profile.release] lto = "thin" codegen-units = 1 diff --git a/src/drive.rs b/src/drive.rs index 703dff4..39de03d 100644 --- a/src/drive.rs +++ b/src/drive.rs @@ -16,6 +16,10 @@ use anyhow::{anyhow, Context, Result}; /// What flavor of storage we're dealing with. Determines whether it can take /// a multi-pass railing or whether we should treat it like fine china. #[derive(Clone, Copy, Debug, PartialEq, Eq)] +// Windows `classify` always returns Unknown (TODO: native classification), +// so Nvme/Sata/Rotational are never constructed there. Lint stays useful on +// Linux + macOS where the real classification logic lives. +#[cfg_attr(not(any(target_os = "linux", target_os = "macos")), allow(dead_code))] pub enum DriveKind { Nvme, Sata, // SSD on SATA bus, rotational==0 diff --git a/src/safety.rs b/src/safety.rs index 5bdfd80..ac7f5c6 100644 --- a/src/safety.rs +++ b/src/safety.rs @@ -6,9 +6,16 @@ //! ass-blast your filesystem just because you fat-fingered a path. use std::fmt; -use std::path::{Path, PathBuf}; +use std::path::Path; +// PathBuf is only referenced by name in the Linux mountinfo/swaps parsers. +#[cfg(target_os = "linux")] +use std::path::PathBuf; #[derive(Debug, Clone, Copy, PartialEq, Eq)] +// On non-Linux platforms `is_swap` and `is_mounted_block_device` are no-op +// fallbacks (no /proc/swaps, no /proc/self/mountinfo), so SwapFile and +// MountedDevice are never constructed there. Keep the lint useful on Linux. +#[cfg_attr(not(target_os = "linux"), allow(dead_code))] pub enum Hazard { /// Path is, or is inside, a system directory we shouldn't touch. SystemPath, diff --git a/src/wipe.rs b/src/wipe.rs index 9800039..757c188 100644 --- a/src/wipe.rs +++ b/src/wipe.rs @@ -297,7 +297,9 @@ fn freespace_loop( pb.inc(buf.as_bytes().len() as u64); } } - Err(e) if e.raw_os_error() == Some(libc::ENOSPC) => break, + // ENOSPC on Unix / ERROR_DISK_FULL on Windows — std maps both to + // StorageFull, so we sidestep platform-specific errno crates. + Err(e) if e.kind() == std::io::ErrorKind::StorageFull => break, Err(e) => return Err(e).context("freespace write"), } } From 618183cd0744eb8fced1390a01ff80b40536debe Mon Sep 17 00:00:00 2001 From: Zoa Hickenlooper Date: Thu, 14 May 2026 12:30:46 -0700 Subject: [PATCH 3/4] *gags on rusty cocks* --- LICENSE | 31 +++++++++++++++++++++++-------- src/drive.rs | 9 +++++---- src/safety.rs | 6 +++++- 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/LICENSE b/LICENSE index 3c56b47..1294480 100644 --- a/LICENSE +++ b/LICENSE @@ -1,13 +1,28 @@ - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - Version 2, December 2004 + WTFPL+ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE PLUS + Version 1, August 2025 - Copyright (C) 2026 Zoa Hickenlooper / aka vxfemboy +Copyright (C) 2026 Zoa Hickenlooper / aka vxfemboy - Everyone is permitted to copy and distribute verbatim or modified - copies of this license document, and changing it is allowed as long - as the name is changed. +Everyone is permitted to copy and distribute verbatim or modified +copies of this license document, and changing it is allowed as long +as the name is changed. - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE PLUS TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - 0. You just DO WHAT THE FUCK YOU WANT TO. + 0. You just DO WHAT THE FUCK YOU WANT TO! + + DISCLAIMER +This software is provided "AS IS", without warranty of any kind, +express or implied, including but not limited to the warranties of +merchantability, fitness for a particular purpose and noninfringement. +In no event shall the authors be liable for any claim, damages or +other liability, whether in an action of contract, tort or otherwise, +arising from, out of or in connection with the software or the use or +other dealings in the software. + +So in short: + DO WHATEVER YOU WANT, + BUT IF IT BREAKS SOMETHING, + DON’T COME CRYING — OR SUING — TO ME. diff --git a/src/drive.rs b/src/drive.rs index 39de03d..d9c67ed 100644 --- a/src/drive.rs +++ b/src/drive.rs @@ -16,10 +16,11 @@ use anyhow::{anyhow, Context, Result}; /// What flavor of storage we're dealing with. Determines whether it can take /// a multi-pass railing or whether we should treat it like fine china. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -// Windows `classify` always returns Unknown (TODO: native classification), -// so Nvme/Sata/Rotational are never constructed there. Lint stays useful on -// Linux + macOS where the real classification logic lives. -#[cfg_attr(not(any(target_os = "linux", target_os = "macos")), allow(dead_code))] +// Only Linux's sysfs path distinguishes all four variants. macOS lumps every +// flash device into `Sata` (so `Nvme` is dead there) and Windows always +// returns `Unknown` (so Nvme/Sata/Rotational are dead). Keep the lint useful +// where it actually catches things — i.e. Linux — and silence it elsewhere. +#[cfg_attr(not(target_os = "linux"), allow(dead_code))] pub enum DriveKind { Nvme, Sata, // SSD on SATA bus, rotational==0 diff --git a/src/safety.rs b/src/safety.rs index ac7f5c6..f3fbebf 100644 --- a/src/safety.rs +++ b/src/safety.rs @@ -151,7 +151,11 @@ pub fn explain(path: &Path, hazard: Hazard) { eprintln!(); } -#[cfg(test)] +// The hardcoded `SYSTEM_PREFIXES` are all Unix paths, and `fs::canonicalize` +// behaves wildly differently on Windows (it resolves `/` to the current +// drive's root, e.g. `\\?\C:\`, which breaks the equality check). The +// behavior these tests assert is fundamentally Linux-shaped — gate them. +#[cfg(all(test, target_os = "linux"))] mod tests { use super::*; From 2a1da41114cef119834b8b241fcd22fd43767663 Mon Sep 17 00:00:00 2001 From: Zoa Hickenlooper Date: Sun, 17 May 2026 15:52:05 -0700 Subject: [PATCH 4/4] 8===D data shafted. I had an old computer that just got fixed I turned it on and it had asked for a password. I asked my bf 'What the password was?' He replied 'MY PENIS' Two seconds later and went back to him and said its says ITS 2 SHORT. --- CONTRIBUTING.md | 139 +++++++++++++++++++++++++++++++++++++++ Cargo.lock | 2 +- Cargo.toml | 5 +- README.md | 169 +++++++++++++++++++++--------------------------- 4 files changed, 216 insertions(+), 99 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c5a7395 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,139 @@ +# Contributing (Sword Fighters Wanted) + +![Sword Fighting](https://media.giphy.com/media/l1ugaivowDSqyE3KM/giphy.gif) + +Want to cross streams? Pull requests are welcome. Before you start whittling, read this end to end — the project has a load-bearing tone and a strict tooling bar, and most rejected PRs would have been merges if the contributor had skimmed the rules first. + +## The vibe (three rules) + +1. **Truth wraps in jokes, not the other way around.** Every comment, doc-string, error message, and `--help` line is a dick joke as the wrapper around the technical payload. If the joke obscures the truth, the joke loses. If the truth has no joke, that's also fine — comments earn their place by being load-bearing. + +2. **Default to no comments.** Rust's identifiers and types do most of the documenting. Only comment when the *why* is non-obvious (a hidden invariant, a workaround, a perf detail, a platform quirk). Then dress that *why* in the project voice. + +3. **The joke is about dicks and sex acts. Not about people.** Anything that punches down — slurs, bigotry, harassment of named individuals — gets rejected on sight regardless of how clever the wrapper is. `--gay` (rainbow mode) and `--rape` (override safe-word) are project-internal *action-shape* names, not pejoratives aimed at anyone. If you can't tell the difference, this isn't the project for you. + +## Voice cheat-sheet + +Examples pulled verbatim from the codebase. Read these before you write your own. + +### Good — `--help` doc string (lives in `src/cli.rs`) + +```rust +/// Take it slow, big boy. One penis per write(2). Useful for sanity +/// comparison or terminals with a kink for tiny syscalls. ~5000x more +/// syscalls than the buffered path. +#[arg(short = 's', long)] +pub slow: bool, +``` + +The joke (basic missionary / one-penis-per-write) sits *on top of* the technical claim (one `write(2)` per generated dick, useful for syscall-count comparison, ~5000× ratio against the buffered path). The reader gets both. + +### Good — module header (lives in `src/safety.rs`) + +```rust +//! Cock-block department. Stops you from face-fucking your /etc/ when your +//! hand slipped, your /boot/ when you meant /tmp/, or the swap file your +//! kernel is currently using to remember things. +``` + +Names the module's job, then lists the actual hazards it catches. + +### Good — invariant comment (lives in `src/wipe.rs`) + +```rust +// Refill ONCE per round. Within a round we rewrite the same dicks for +// every chunk: the content is non-cryptographic ASCII art, so reusing +// the buffer is invisible. Across rounds we get fresh variety. +buf.fill(); +``` + +A load-bearing perf note. Tells future-you (or future-contributor-you) why this isn't an accidental optimization to undo. + +### Bad — sterile rewrite + +```rust +/// Disables buffered writing for compatibility purposes. +pub slow: bool, +``` + +Accurate, but kills the voice. PRs that "professionalize" existing copy get closed. + +### Bad — joke with no payload + +```rust +// nut nut nut +buf.fill(); +``` + +This is the joke without the why. Either explain the perf reason or delete the comment. + +### Bad — punching at people + +```rust +// only would name a variable this badly +``` + +Hard no, regardless of how true it feels in the moment. + +## Code style + tooling + +Reproduce locally before pushing. CI on `ubuntu-latest`, `macos-latest`, and `windows-latest` runs all four: + +```bash +cargo fmt --all -- --check # exit 0 +cargo clippy --release --all-targets -- -D warnings # exit 0, no warnings +cargo build --release # exit 0 +cargo test --release # all tests pass +``` + +- **Rust 2021, snake_case, no `unsafe`.** If you genuinely need `unsafe`, justify it in the PR description. +- **Errors via `anyhow`.** Bubble to `main` and print with `{:#}` so the context chain shows up. +- **Per-platform code uses `#[cfg(target_os = "...")]`.** The fallback branch always compiles cleanly — silence dead-code warnings on non-supported platforms via `#[cfg_attr(not(target_os = "linux"), allow(dead_code))]` on the enum, not blanket `#[allow(dead_code)]`. +- **New deps need a one-line justification in the PR description.** We prefer leaning on existing crates (`indicatif`, `wide`, `console`, `anyhow`, `clap`, `ctrlc`, `rand`) over pulling in new ones for marginal wins. +- **Public items** (`pub fn` / `pub struct` / `pub enum`) get a doc-string in the project voice. **Private items** don't, unless the *why* is non-obvious. + +## Module layout (so your PR lands in the right file) + +| Module | What it does | +|---|---| +| `main.rs` | Arg parse → safety check → drive policy → bounded thread pool → exit code. | +| `cli.rs` | clap derive `Args` + per-flag `--help` copy. **Touch this and you change user-visible help text.** Match the existing voice. | +| `dicks.rs` | The actual penis generator. SIMD batched fill via `wide::u8x32`, per-thread `SmallRng`. | +| `wipe.rs` | `wipe_file`, `wipe_freespace`, `verify_file`. Cleanup pass always runs. | +| `drive.rs` | SSD/NVMe detection + refusal + `--secure-erase` delegation. Linux + macOS supported; Windows is a stub. | +| `safety.rs` | Path-safety guard. System dirs, swap, mounted devices. `--rape` bypasses. | +| `cleanup.rs` | Process-wide ctrl-c cleanup registry. | +| `progress.rs` | indicatif bars + the `VERBS` table for completion messages. | + +If your PR touches more than two of these, it should probably be two PRs. + +## PR checklist + +- [ ] One concern per PR. Refactors stay separate from feature work. +- [ ] PR description matches the project voice. Bullet points are fine; corpo-speak is not. +- [ ] CI green on Linux, macOS, Windows. +- [ ] Tests added if you introduced a public function with non-trivial behavior. +- [ ] If you added or removed a CLI flag, the README's `

All options
` block stays in sync. +- [ ] For big changes (>200 LOC or a new module): open an issue first to align on the approach. We don't want anyone burning a weekend on a PR we'd close. + +## What we want + +- **Pick up `help wanted` issues.** +- **Bug reports with reproduction steps.** What you ran, what happened, what you expected, what OS + Rust version. +- **New completion verbs** for the `VERBS` table in `src/progress.rs`. Submit a PR adding one or three, each with color / bold / italic / emoji choices. Keep them on-brand. +- **More `--shaft-*` / `--jizz-*` knobs** that make the generator more configurable without exploding the flag list. + +## What we don't want + +- **"Modernization" PRs that swap our tone for boilerplate corporate copy.** The voice is the project. +- **Heavy new dependencies.** No `tokio`, no async runtimes, no web servers, no GUI toolkits. This is a single-binary CLI. +- **New override flags layered on top of `--rape`.** One nuclear safe-word is enough. +- **Anything that breaks the SSD-refusal default.** SSD-safety is the load-bearing feature; if you have a real reason to bypass it on a specific run, `--rape` already exists. + +## The actual social contract + +- Be a person to other contributors. Disagreement is fine; ad hominem is not. +- Maintainer reviews when maintainer reviews. Pinging "any update?" after 7+ days is fine; pinging hourly is not. +- If your PR sits unmerged for 2+ weeks with no maintainer response, ping in the PR thread. If it sits for 4+ weeks, escalate by tagging in the issue tracker. + +8====D~~~ diff --git a/Cargo.lock b/Cargo.lock index e84ba10..98312b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -594,7 +594,7 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "wipedicks" -version = "0.2.0" +version = "1.0.0" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 14a7015..570b4e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,13 @@ [package] name = "wipedicks" -version = "0.2.0" +version = "1.0.0" edition = "2021" description = "Wipe files and drives securely with random ASCII dicks" keywords = ["anti-forensics", "antiforensics", "forensics", "wipe", "ascii"] +categories = ["command-line-utilities", "filesystem"] readme = "README.md" repository = "https://github.com/vxfemboy/wipedicks" -license = "WTFPL" +license = "WTFPL+" [dependencies] diff --git a/README.md b/README.md index 09f6a1b..800926a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # 8====D~ WipeDicks [![CI](https://github.com/vxfemboy/wipedicks/actions/workflows/ci.yml/badge.svg)](https://github.com/vxfemboy/wipedicks/actions/workflows/ci.yml) +[![crates.io](https://img.shields.io/crates/v/wipedicks.svg)](https://crates.io/crates/wipedicks) + +*The only file shredder that finishes with a happy ending.* ``` ⠀⠀⠀⠀⠀⣠⠶⠚⠛⠛⠛⠲⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ @@ -21,147 +24,121 @@ ⠀⠀⠀⠀⣀⡀⠀⣰⠇⣾⠀⠀⠈⣩⣥⣄⣿⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⢿⡉⠳⡟⣸⠃⠀⠀⠀⠘⢷⣌⠉⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠙⢦⣴⠏⠀⠀⠀⠀⠀⠀⠉⠳⠶⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +``` + +wipedicks plowing 6 HDDs in parallel on an R710 +Six 12 TB drives, parallel wipe via SSH, with live colorized hex dumps watching the targets in real time. Before-and-after speaks for itself: + +``` ❤ xxd /dev/sda1 | head 00000000: 383d 447e 7e20 383d 3d3d 3d3d 3d3d 447e 8=D~~ 8=======D~ 00000010: 2038 233d 3d3d 3d3d 3d3d 3d44 7e7e 2038 8#========D~~ 8 00000020: 233d 3d44 7e7e 2038 3d3d 3d3d 3d3d 3d3d #==D~~ 8======== 00000030: 3d44 2038 3d3d 3d3d 447e 7e7e 2038 3d3d =D 8====D~~~ 8== 00000040: 3d3d 3d3d 3d3d 3d3d 3d44 7e20 3823 3d3d =========D~ 8#== -00000050: 3d3d 3d3d 3d3d 3d3d 3d3d 447e 7e20 383d ==========D~~ 8= -00000060: 3d3d 3d3d 3d44 7e7e 2038 233d 3d3d 3d3d =====D~~ 8#===== -00000070: 3d3d 3d3d 447e 2038 233d 3d3d 3d3d 3d3d ====D~ 8#======= -00000080: 3d3d 3d3d 447e 2038 233d 3d3d 3d44 7e20 ====D~ 8#====D~ -00000090: 3823 3d3d 3d3d 3d3d 3d3d 3d3d 3d3d 447e 8#============D~ ``` -Welcome to WipeDicks, the tool that gives your data the shaft! ## What's the Big Deal? 8===D -WipeDicks is a high-performance, multi-threaded file and device wiping tool that overwrites your data with a veritable bukkake of ASCII penises. It's designed for secure data erasure with more dick jokes than you can shake a stick at! +WipeDicks is a high-performance, multi-threaded file and device wiping tool that overwrites your data with a veritable bukkake of ASCII penises. Now with progress bars, SSD-awareness, and 23 randomized completion verbs (creamed, milked, nuked, gangbanged, splooged on...). It's designed for secure data erasure with more dick jokes than you can shake a stick at. ## Features (or as we like to call them, "Dick Pics") 8=D -- Securely penetrates your files and devices -- Multi-threaded for maximum pleasure (and performance) — bounded thread pool, no fork bombs -- Recursively wipes directories (because size matters) -- Adjustable number of rounds (for those who need extra stamina) -- Option to wipe free space (leave no trace behind!) -- **Live ASCII penis progress bar** — watch the `D` slide right as your data dies (resolves #1) -- **SIMD-accelerated** penis generation via the `wide` crate (stable Rust, no nightly) -- **Configurable shaft length, jizz volume, and ball ring chance** — for the discerning data destroyer -- **SSD/NVMe-aware safety**: refuses to multi-pass overwrite flash storage by default, and tells you the right native tool to use instead (Linux + macOS) -- **Path-safety guards**: refuses to wipe `/`, system dirs, active swap files, and mounted block devices — pass `--rape` if you really mean it -- **`--verify`** read-back pass to catch bad sectors and FS corruption -- **`--gay`** mode for when monochrome dicks just aren't enough (rainbow bar + bright-white jizz) -- **Ctrl-C cleanup**: an interrupted wipe still removes its temp files -- Buffered I/O with per-round buffer reuse — orders of magnitude fewer syscalls and ~100x less RNG work on multi-pass runs +- **Live `[8=========D~~~]` progress bar** with yellow→green gradient and a randomized completion verb +- **SSD/NVMe-aware safety** — refuses to wear out your flash, suggests the right native tool, or delegates to it via `--secure-erase` +- **Path-safety guards** — won't wipe `/`, `/etc`, swap files, or mounted block devices without explicit consent +- **Configurable everything** — shaft length, jizz amount, balls chance, buffer size, bar width, verify-on-write +- **SIMD-batched I/O** via the `wide` crate, bounded thread pool, Ctrl-C cleanup, `--gay` mode for those who want their bukkake in technicolor > [!Warning] > ## Cock Block Alert! 8==X > This tool is designed to permanently destroy data. Use with extreme caution, or you might end up with blue balls (i.e., regret). -> +> > Always double-check your targets before unleashing our ASCII members upon them. +> +>
+>

SSDs, NVMes, and the Sad Truth About Wear Leveling 8==X

+> +> **Overwriting flash storage with multi-pass writes is both useless and harmful.** Yes, even with our beautiful penises. +> +> - **Useless**: SSDs and NVMes use a Flash Translation Layer that remaps logical writes to fresh NAND pages. Overwriting LBA 0 doesn't necessarily clobber the physical NAND cell that holds your old data — that data lingers in over-provisioned NAND until the controller decides to recycle it. +> - **Harmful**: Every pass burns finite program/erase cycles. A 7-pass DoD-style wipe on a 1 TB QLC SSD can consume meaningful percentage points of its lifetime. +> +> So wipedicks **refuses by default** when you point it at a flash block device, and prints the right native command instead: +> +> | Drive | Right tool | +> |---|---| +> | NVMe | `sudo nvme format /dev/nvmeXnY -s 1` (user-data erase) or `-s 2` (crypto erase, SED only) | +> | SATA SSD | `sudo hdparm --user-master u --security-set-pass p /dev/sdX && sudo hdparm --user-master u --security-erase p /dev/sdX` | +> | Either (fast, non-cryptographic) | `sudo blkdiscard /dev/X` | +> | Self-encrypting (TCG Opal) | `sudo sedutil-cli --revertNoErase` | +> +> Use `--secure-erase` to have wipedicks run the right one for you. Use `--rape` to override the refusal and proceed with the (ineffective, damaging) overwrite anyway. Wiping individual *files* on a flash filesystem still works — wipedicks prints a one-time warning and continues, because at least `unlink(2)` actually deletes the inode. +>
-## SSDs, NVMes, and the Sad Truth About Wear Leveling 8==X +## Installation (Getting It Up and Running) 8===D~ -**Overwriting flash storage with multi-pass writes is both useless and harmful.** Yes, even with our beautiful penises. +```bash +# from crates.io (recommended) +cargo install wipedicks -- **Useless**: SSDs and NVMes use a Flash Translation Layer that remaps logical writes to fresh NAND pages. Overwriting LBA 0 doesn't necessarily clobber the physical NAND cell that holds your old data — that data lingers in over-provisioned NAND until the controller decides to recycle it. -- **Harmful**: Every pass burns finite program/erase cycles. A 7-pass DoD-style wipe on a 1 TB QLC SSD can consume meaningful percentage points of its lifetime. +# from source +git clone https://github.com/vxfemboy/wipedicks && cd wipedicks +cargo build --release # binary lands at target/release/wipedicks +``` -So wipedicks **refuses by default** when you point it at a flash block device, and prints the right native command instead: +Prebuilt binaries for Linux (x86_64 + aarch64), macOS (x86_64 + aarch64), and Windows are also attached to each [GitHub release](https://github.com/vxfemboy/wipedicks/releases). -| Drive | Right tool | -|---|---| -| NVMe | `sudo nvme format /dev/nvmeXnY -s 1` (user-data erase) or `-s 2` (crypto erase, SED only) | -| SATA SSD | `sudo hdparm --user-master u --security-set-pass p /dev/sdX && sudo hdparm --user-master u --security-erase p /dev/sdX` | -| Either (fast, non-cryptographic) | `sudo blkdiscard /dev/X` | -| Self-encrypting (TCG Opal) | `sudo sedutil-cli --revertNoErase` | +## Usage (How to Handle Your Tool) 8====D -Use `--secure-erase` to have wipedicks run the right one for you. Use `--rape` to override the refusal and proceed with the (ineffective, damaging) overwrite anyway. Wiping individual *files* on a flash filesystem still works — wipedicks prints a one-time warning and continues, because at least `unlink(2)` actually deletes the inode. +```bash +# Give a single file the business +wipedicks /path/to/file -## Installation (Getting It Up and Running) 8===D~ +# Recursively shred a directory +wipedicks -r /path/to/directory -1. Make sure you've got Rust and Cargo (the only package we care about) -2. Clone this bad boy: - ```bash - git clone https://github.com/vxfemboy/wipedicks.git && cd wipedicks - ``` -3. Get it hard... err, built: - ```bash - cargo build --release - ``` -4. Find your new toy in `target/release/wipedicks` +# Forcefully shove dicks into the disk +sudo wipedicks --rape /dev/sdX +``` -## Usage (How to Handle Your Tool) 8====D +
+All options (or run wipedicks --help) -#### Basic usage: -Options (Piercings for your command): - `-r, --recursive`: Go deep into directories -- `-n, --numrounds `: How many times you want to go (default: 1) -- `-w, --wipefree`: Clean up afterwards (fills the FS with dicks until ENOSPC, then deletes) -- `-s, --slow`: Take it slow, big boy (one-penis-per-write, no buffering, no SIMD) -- `--rape`: Override the SSD/NVMe refusal. You probably don't want this. -- `--secure-erase`: For block-device targets, delegate to the native tool (`nvme format` / `hdparm --security-erase`) +- `-n, --numrounds `: How many overwrite passes per target (default 1) +- `-w, --wipefree`: After the wipe, fill free space with dicks until ENOSPC +- `-s, --slow`: One-penis-per-write path (no buffering, no SIMD) +- `--rape`: Override every safety check. Nuclear safe-word. +- `--secure-erase`: Delegate to the drive's native secure-erase - `--shaft-min ` / `--shaft-max `: Range of `=` per penis (default 1..12) - `--jizz-min ` / `--jizz-max `: Range of `~` per penis (default 0..3) - `--balls-chance

`: Probability of a `#` base ring, 0.0..1.0 (default 0.5) -- `--buffer-size `: Write batch size (default 1 MiB). Bigger = fewer syscalls. -- `--bar-width `: Width of the penis progress bar (default 16) +- `--buffer-size `: Write batch size (default 1 MiB) +- `--bar-width `: Width of the progress bar (default 16) - `--no-progress`: Disable progress bars (auto-disabled when stderr isn't a TTY) -- `--gay`: Cycle the bar across the rainbow (red→orange→yellow→green→cyan→blue→magenta) with bright-white jizz -- `--verify`: After each round, read the file back and confirm every byte is a valid penis character. Catches bad sectors and FS-level corruption. -- `--dry-run`: Print one buffer of generated dicks to stdout and exit. For inspecting your config. - -#### Examples: -1. Give a single file the business: - ```bash - ./wipedicks /path/to/file - ``` -2. Go to town on a directory, three times: - ```bash - ./wipedicks -r -n 3 /path/to/directory - ``` -3. A more endowed wipe (huge shaft, drenched in jizz, every dick has balls): - ```bash - ./wipedicks --shaft-min 20 --shaft-max 40 --jizz-min 5 --jizz-max 10 --balls-chance 1.0 ./file - ``` -4. Preview what the generator would write, without touching anything: - ```bash - ./wipedicks --dry-run --shaft-max 30 /tmp/anything - ``` -5. Securely erase an SSD/NVMe (delegates to the native tool — needs root): - ```bash - sudo ./wipedicks --secure-erase /dev/nvme0n1 - ``` -6. Absolutely destroy a spinning-rust device (Good luck pulling out of this one!): - ```bash - ./wipedicks /dev/sdX # only allowed on rotational drives by default - ``` - -> [!CAUTION] +- `--gay`: Cycle the bar across the rainbow with bright-white jizz +- `--verify`: After each round, read the file back and confirm every byte is a valid penis character +- `--dry-run`: Print one buffer of generated dicks to stdout and exit + +

+ +> [!CAUTION] > ## Wrap It Before You Tap It > - Always check your target twice, or you might screw the wrong hole -> - Wiping entire devices is like unprotected data sex - there's no going back +> - Wiping entire devices is like unprotected data sex — there's no going back > - Don't run this on system files unless you want your computer to go limp > - This tool is for giggles and learning. For serious business, use protection (i.e., certified tools) -## Contributing (Sword Fighters Wanted) - -![Sword Fighting](https://media.giphy.com/media/l1ugaivowDSqyE3KM/giphy.gif) - -Want to cross streams? Contributions are welcome! Feel free to submit a Pull Request. - ## Credits -This project was brought up to me by [Drewsif/wipedicks](https://github.com/Drewsif/wipedicks/) -all credits for the idea goes to the original creator I just rewrote it in Rust +This project was brought up to me by [Drewsif/wipedicks](https://github.com/Drewsif/wipedicks/) — all credits for the idea goes to the original creator. I just rewrote it in Rust. ## Disclaimer (Cover Your Ass-ets) The authors aren't responsible for any data loss or damage. Use at your own risk, you kinky data destroyer! Remember: -> With great power comes great responsibili-D. +> With great power comes great responsibili-D. > Use wipedicks wisely, and may your data always rest in pieces! 8====D~~~