Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,16 @@ Arguments:
<item> Directory or file to hash/check

Options:
-t, --tree Print directory tree
-s, --save Save the checksum to a file
-a, --algorithm <blake3|md5|ripemd160|sha1|sha256|sha384|sha512> The hash function to use [default: sha1]
-i, --include-hidden-files Include hidden files
-e, --skip-empty-dir Skip empty directories
-n, --ignore <pattern> 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 <blake3|crc32|md5|ripemd160|sha1|sha256|sha384|sha512|xxhash3> The hash function to use [default: xxhash3]
-i, --include-hidden-files Include hidden files
-e, --skip-empty-dir Skip empty directories
-n, --ignore <pattern> 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:
Expand Down
4 changes: 2 additions & 2 deletions src/App.Tests/E2ETests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/App/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ open System.CommandLine.Invocation
open System.Threading


let defaultHashAlg = HashType.SHA1
let defaultHashAlg = HashType.XXHASH3

type RootOpt
(
Expand Down
12 changes: 9 additions & 3 deletions src/HashUtil.Tests/HashTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ type ChecksumTests(output: ITestOutputHelper) =
[| SHA512
"cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e" |]
[| BLAKE3
"af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262" |] ]
"af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262" |]
[| XXHASH3; "2d06800538d394c2" |]
[| CRC32; "00000000" |] ]

[<Theory; MemberData("emptyHashes")>]
member _.``hash empty string``(hashType, expectedHash) =
Expand All @@ -45,7 +47,9 @@ type ChecksumTests(output: ITestOutputHelper) =
"9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043" |]
[| BLAKE3
"hello"
"ea8f163db38682925e4491c5e58d4bb3506ef8c14eb78a86e908c5624a67200f" |] ]
"ea8f163db38682925e4491c5e58d4bb3506ef8c14eb78a86e908c5624a67200f" |]
[| XXHASH3; "hello"; "9555e8555c62dcfd" |]
[| CRC32; "hello"; "86a61036" |] ]


[<Theory; MemberData("simpleStringHashes")>]
Expand All @@ -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 |] ]

[<Theory; MemberData("hashTypeStrings")>]
member _.``parse HashType``(inputStrs, expectedType) =
Expand Down
21 changes: 14 additions & 7 deletions src/HashUtil/Checksum.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

open Microsoft.FSharp.Reflection
open System.IO
open System.IO.Hashing
open System.Security.Cryptography
open System.Text
open Blake3
Expand All @@ -15,6 +16,8 @@ module Checksum =
| SHA384
| SHA512
| BLAKE3
| XXHASH3
| CRC32

let allHashTypes: HashType[] =
typeof<HashType>
Expand All @@ -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())
4 changes: 3 additions & 1 deletion src/HashUtil/HashUtil.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

<ItemGroup>
<Compile Include="Util.fs" />
<Compile Include="NonCryptoWrapper.fs" />
<Compile Include="Checksum.fs" />
<Compile Include="Library.fs" />
<Compile Include="Hashing.fs" />
Expand All @@ -38,9 +39,10 @@
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
<None Include="..\..\LICENSE" Pack="true" PackagePath="\" />

<PackageReference Include="ultimateanu.HashDir.Checksums" Version="1.0.0" />
<PackageReference Include="Blake3" Version="2.0.0" />
<PackageReference Include="Glob" Version="1.2.0-alpha0037" />
<PackageReference Include="System.IO.Hashing" Version="8.0.0" />
<PackageReference Include="ultimateanu.HashDir.Checksums" Version="1.0.0" />
</ItemGroup>

<Target Name="CleanPackages" BeforeTargets="Clean">
Expand Down
15 changes: 15 additions & 0 deletions src/HashUtil/NonCryptoWrapper.fs
Original file line number Diff line number Diff line change
@@ -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<byte>(array, ibStart, cbSize))

override this.HashFinal() = hashAlgo.GetCurrentHash()

override this.Initialize() = hashAlgo.Reset()