diff --git a/AGENTS.md b/AGENTS.md index eae3135..51fcb9c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,13 +2,33 @@ Hashdir is a dotnet (f# language) CLI tool to compute the hash or checksum of a directory. This is done recursively using the directories, files and their names within. This is useful for quickly comparing directories and also determining if anything in the filesystem has changed. -It is written in f# language (part of dotnet 8). +It is written in F# on .NET 8. + +The tool supports many popular hashing algorithms such as `blake3`, `md5`, `ripemd160`, `sha1`, `sha256`, `sha384`, `sha512`, and `xxhash3`. # How to Run -- **Dev Build:** `dotnet build` can be used to quickly make sure the app builds (safe for LLMs) -- **Run Tests:** `dotnet test` can be used to make sure the tests are passing (safe for LLMs) +- **Dev Build:** `make build` can be used to quickly make sure the app builds. +- **Run Tests:** `make test` can be used to make sure the tests are passing. +- **Check changes/PR:** `git diff main` can be used to understand the feature branch. # Project Structure -- `src/` contains all the source code +The solution is organized into several projects within the `src/` directory. + +- **`src/App`**: The main CLI application project. + - `Program.fs`: The entry point of the application, responsible for parsing command-line arguments and orchestrating the hashing process. + +- **`src/HashUtil`**: A library project containing the core hashing logic. + - `Checksum.fs`: This is a key file. It defines the supported hash algorithms (`HashType` discriminated union), a function to parse the algorithm from user input (`parseHashType`), and a factory function to create the appropriate `HashAlgorithm` instance (`getHashAlgorithm`). When adding a new algorithm, this file is the primary one to modify. + - `NonCryptoWrapper.fs`: A wrapper class that adapts non-cryptographic hash algorithms (like `XxHash3`) to the standard `HashAlgorithm` interface, allowing them to be used seamlessly with the existing hashing infrastructure. + - `Hashing.fs`: Contains the logic for hashing files and directories. + - `Library.fs`: Provides a programmatic API for the hashing functionality. + - `Util.fs`: Contains utility functions, including `computeHashOfString`. + +- **`src/Checksums`**: A C# project that provides an implementation of the `RIPEMD160` algorithm. + +- **`src/App.Tests`**: Contains tests for the `App` project. + +- **`src/HashUtil.Tests`**: Contains tests for the `HashUtil` library. + - `HashTests.fs`: Contains tests for the hashing functionality, including tests for different algorithms and input strings. When adding a new algorithm, this file should be updated with new test cases. diff --git a/README.md b/README.md index dfee07b..df3a1bd 100644 --- a/README.md +++ b/README.md @@ -37,16 +37,16 @@ Arguments: Directory or file to hash/check Options: - -t, --tree Print directory tree - -s, --save Save the checksum to a file - -a, --algorithm The hash function to use [default: sha1] - -i, --include-hidden-files Include hidden files - -e, --skip-empty-dir Skip empty directories - -n, --ignore Directories/files to not include - -h, --hash-only Print only the hash - -c, --color Colorize the output [default: True] - --version Show version information - -?, -h, --help Show help and usage information + -t, --tree Print directory tree + -s, --save Save the checksum to a file + -a, --algorithm The hash function to use [default: xxhash3] + -i, --include-hidden-files Include hidden files + -e, --skip-empty-dir Skip empty directories + -n, --ignore Directories/files to not include + -h, --hash-only Print only the hash + -c, --color Colorize the output [default: True] + --version Show version information + -?, -h, --help Show help and usage information Commands: diff --git a/src/App.Tests/E2ETests.fs b/src/App.Tests/E2ETests.fs index b38541f..f864bcc 100644 --- a/src/App.Tests/E2ETests.fs +++ b/src/App.Tests/E2ETests.fs @@ -92,7 +92,7 @@ type FsTests debugOutput: ITestOutputHelper ) = let hashFile = - Path.Combine(fsTempDirSetupFixture.TempDir, "project_hash.sha1.txt") + Path.Combine(fsTempDirSetupFixture.TempDir, "project_hash.xxhash3.txt") let oldStdOut = Console.Out let customStdOut = new IO.StringWriter() @@ -365,7 +365,7 @@ type FsTests let getHashFilePath id = Path.Join( fsTempDirSetupFixture.TempDir, - sprintf "topA.txt.%s.sha1.txt" (id.ToString()) + sprintf "topA.txt.%s.xxhash3.txt" (id.ToString()) ) // Run hashdir multiple times. diff --git a/src/App/Program.fs b/src/App/Program.fs index 579bb4f..c1b6594 100644 --- a/src/App/Program.fs +++ b/src/App/Program.fs @@ -9,7 +9,7 @@ open System.CommandLine.Invocation open System.Threading -let defaultHashAlg = HashType.SHA1 +let defaultHashAlg = HashType.XXHASH3 type RootOpt ( diff --git a/src/HashUtil.Tests/HashTests.fs b/src/HashUtil.Tests/HashTests.fs index 1628796..b1111e6 100644 --- a/src/HashUtil.Tests/HashTests.fs +++ b/src/HashUtil.Tests/HashTests.fs @@ -19,7 +19,9 @@ type ChecksumTests(output: ITestOutputHelper) = [| SHA512 "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e" |] [| BLAKE3 - "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262" |] ] + "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262" |] + [| XXHASH3; "2d06800538d394c2" |] + [| CRC32; "00000000" |] ] [] member _.``hash empty string``(hashType, expectedHash) = @@ -45,7 +47,9 @@ type ChecksumTests(output: ITestOutputHelper) = "9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043" |] [| BLAKE3 "hello" - "ea8f163db38682925e4491c5e58d4bb3506ef8c14eb78a86e908c5624a67200f" |] ] + "ea8f163db38682925e4491c5e58d4bb3506ef8c14eb78a86e908c5624a67200f" |] + [| XXHASH3; "hello"; "9555e8555c62dcfd" |] + [| CRC32; "hello"; "86a61036" |] ] [] @@ -64,7 +68,9 @@ type ChecksumTests(output: ITestOutputHelper) = [| [ "sha384"; " Sha384 "; "SHA384 " ]; Some SHA384 |] [| [ "sha512"; " Sha512 "; "SHA512 " ]; Some SHA512 |] [| [ "blake3"; " Blake3 "; "BLAKE3 " ]; Some BLAKE3 |] - [| [ "asha1"; " md6 "; "SHA513 " ]; None |] ] + [| [ "xxhash3"; " xxHash3 "; "XXHASH3 " ]; Some XXHASH3 |] + [| [ "crc32"; " crc32 "; "CRC32 " ]; Some CRC32 |] + [| [ "asna1"; " md6 "; "SHA513 " ]; None |] ] [] member _.``parse HashType``(inputStrs, expectedType) = diff --git a/src/HashUtil/Checksum.fs b/src/HashUtil/Checksum.fs index 8a7b88e..c67fc11 100644 --- a/src/HashUtil/Checksum.fs +++ b/src/HashUtil/Checksum.fs @@ -2,6 +2,7 @@ open Microsoft.FSharp.Reflection open System.IO +open System.IO.Hashing open System.Security.Cryptography open System.Text open Blake3 @@ -15,6 +16,8 @@ module Checksum = | SHA384 | SHA512 | BLAKE3 + | XXHASH3 + | CRC32 let allHashTypes: HashType[] = typeof @@ -33,14 +36,18 @@ module Checksum = | "SHA384" -> Some SHA384 | "SHA512" -> Some SHA512 | "BLAKE3" -> Some BLAKE3 + | "XXHASH3" -> Some XXHASH3 + | "CRC32" -> Some CRC32 | _ -> None let getHashAlgorithm hashType : HashAlgorithm = match hashType with - | MD5 -> upcast MD5.Create() - | RIPEMD160 -> upcast Checksums.RIPEMD160.Create() - | SHA1 -> upcast SHA1.Create() - | SHA256 -> upcast SHA256.Create() - | SHA384 -> upcast SHA384.Create() - | SHA512 -> upcast SHA512.Create() - | BLAKE3 -> upcast (new Blake3.Blake3HashAlgorithm()) + | MD5 -> MD5.Create() + | RIPEMD160 -> Checksums.RIPEMD160.Create() + | SHA1 -> SHA1.Create() + | SHA256 -> SHA256.Create() + | SHA384 -> SHA384.Create() + | SHA512 -> SHA512.Create() + | BLAKE3 -> new Blake3.Blake3HashAlgorithm() + | XXHASH3 -> new NonCryptoWrapper(new System.IO.Hashing.XxHash3()) + | CRC32 -> new NonCryptoWrapper(new System.IO.Hashing.Crc32()) diff --git a/src/HashUtil/HashUtil.fsproj b/src/HashUtil/HashUtil.fsproj index 565a7d7..415605e 100644 --- a/src/HashUtil/HashUtil.fsproj +++ b/src/HashUtil/HashUtil.fsproj @@ -30,6 +30,7 @@ + @@ -38,9 +39,10 @@ - + + diff --git a/src/HashUtil/NonCryptoWrapper.fs b/src/HashUtil/NonCryptoWrapper.fs new file mode 100644 index 0000000..b829880 --- /dev/null +++ b/src/HashUtil/NonCryptoWrapper.fs @@ -0,0 +1,15 @@ +namespace HashUtil + +open System +open System.IO.Hashing +open System.Security.Cryptography + +type NonCryptoWrapper(hashAlgo: NonCryptographicHashAlgorithm) = + inherit HashAlgorithm() + + override this.HashCore(array: byte[], ibStart: int, cbSize: int) = + hashAlgo.Append(System.ArraySegment(array, ibStart, cbSize)) + + override this.HashFinal() = hashAlgo.GetCurrentHash() + + override this.Initialize() = hashAlgo.Reset()