From d21e3f88ecd9a5dfd557ee34e13858e065fd5465 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Thu, 24 Jul 2025 14:44:05 -0400 Subject: [PATCH] Implement support for Git hashing with SHA-256 SHA-256 is Git's next hash algorithm. The world is still basically stuck on SHA-1 with git, but shouldn't be. We can at least do our part to get ready. On the C++ implementation side, only a little bit of generalization was needed, and that was fairly straight-forward. The tests (unit and system) were actually bigger, and care was taken to make sure they were all cover both algorithms equally. --- maintainers/flake-module.nix | 3 - src/libstore/store-api.cc | 7 +- src/libutil-tests/data/git/check-data.sh | 44 +++--- .../data/git/{tree.bin => tree-sha1.bin} | Bin .../data/git/{tree.txt => tree-sha1.txt} | 0 src/libutil-tests/data/git/tree-sha256.bin | Bin 0 -> 181 bytes src/libutil-tests/data/git/tree-sha256.txt | 4 + src/libutil-tests/git.cc | 148 +++++++++++++----- src/libutil/git.cc | 23 ++- src/libutil/hash.cc | 17 -- src/libutil/include/nix/util/git.hh | 11 +- src/libutil/include/nix/util/hash.hh | 25 ++- tests/functional/fixed.nix | 7 +- tests/functional/git-hashing/common.sh | 2 + tests/functional/git-hashing/fixed.sh | 6 +- tests/functional/git-hashing/meson.build | 3 +- tests/functional/git-hashing/simple-common.sh | 96 ++++++++++++ tests/functional/git-hashing/simple-sha1.sh | 27 ++++ tests/functional/git-hashing/simple-sha256.sh | 29 ++++ tests/functional/git-hashing/simple.sh | 79 ---------- 20 files changed, 350 insertions(+), 181 deletions(-) mode change 100644 => 100755 src/libutil-tests/data/git/check-data.sh rename src/libutil-tests/data/git/{tree.bin => tree-sha1.bin} (100%) rename src/libutil-tests/data/git/{tree.txt => tree-sha1.txt} (100%) create mode 100644 src/libutil-tests/data/git/tree-sha256.bin create mode 100644 src/libutil-tests/data/git/tree-sha256.txt create mode 100644 tests/functional/git-hashing/simple-common.sh create mode 100755 tests/functional/git-hashing/simple-sha1.sh create mode 100755 tests/functional/git-hashing/simple-sha256.sh delete mode 100755 tests/functional/git-hashing/simple.sh diff --git a/maintainers/flake-module.nix b/maintainers/flake-module.nix index ee9a8bdad..b6686f134 100644 --- a/maintainers/flake-module.nix +++ b/maintainers/flake-module.nix @@ -262,8 +262,6 @@ ''^tests/functional/gc-concurrent\.sh$'' ''^tests/functional/gc-concurrent2\.builder\.sh$'' ''^tests/functional/gc-non-blocking\.sh$'' - ''^tests/functional/git-hashing/common\.sh$'' - ''^tests/functional/git-hashing/simple\.sh$'' ''^tests/functional/hash-convert\.sh$'' ''^tests/functional/impure-derivations\.sh$'' ''^tests/functional/impure-eval\.sh$'' @@ -339,7 +337,6 @@ ''^tests/functional/user-envs\.builder\.sh$'' ''^tests/functional/user-envs\.sh$'' ''^tests/functional/why-depends\.sh$'' - ''^src/libutil-tests/data/git/check-data\.sh$'' ]; }; }; diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc index b80d82b99..1465d9b42 100644 --- a/src/libstore/store-api.cc +++ b/src/libstore/store-api.cc @@ -107,8 +107,11 @@ static std::string makeType(const MixStoreDirMethods & store, std::string && typ StorePath MixStoreDirMethods::makeFixedOutputPath(std::string_view name, const FixedOutputInfo & info) const { - if (info.method == FileIngestionMethod::Git && info.hash.algo != HashAlgorithm::SHA1) - throw Error("Git file ingestion must use SHA-1 hash"); + if (info.method == FileIngestionMethod::Git + && !(info.hash.algo == HashAlgorithm::SHA1 || info.hash.algo == HashAlgorithm::SHA256)) { + throw Error( + "Git file ingestion must use SHA-1 or SHA-256 hash, but instead using: %s", printHashAlgo(info.hash.algo)); + } if (info.hash.algo == HashAlgorithm::SHA256 && info.method == FileIngestionMethod::NixArchive) { return makeStorePath(makeType(*this, "source", info.references), info.hash, name); diff --git a/src/libutil-tests/data/git/check-data.sh b/src/libutil-tests/data/git/check-data.sh old mode 100644 new mode 100755 index b3f59c4f1..d3be5c41a --- a/src/libutil-tests/data/git/check-data.sh +++ b/src/libutil-tests/data/git/check-data.sh @@ -2,30 +2,34 @@ set -eu -o pipefail -export TEST_ROOT=$(realpath ${TMPDIR:-/tmp}/nix-test)/git-hashing/check-data -mkdir -p $TEST_ROOT +TEST_ROOT=$(realpath "${TMPDIR:-/tmp}/nix-test")/git-hashing/check-data +export TEST_ROOT +mkdir -p "$TEST_ROOT" -repo="$TEST_ROOT/scratch" -git init "$repo" +for hash in sha1 sha256; do + repo="$TEST_ROOT/scratch-$hash" + git init "$repo" --object-format="$hash" -git -C "$repo" config user.email "you@example.com" -git -C "$repo" config user.name "Your Name" + git -C "$repo" config user.email "you@example.com" + git -C "$repo" config user.name "Your Name" -# `-w` to write for tree test -freshlyAddedHash=$(git -C "$repo" hash-object -w -t blob --stdin < "./hello-world.bin") -encodingHash=$(sha1sum -b < "./hello-world-blob.bin" | head -c 40) + # `-w` to write for tree test + freshlyAddedHash=$(git -C "$repo" hash-object -w -t blob --stdin < "./hello-world.bin") + encodingHash=$("${hash}sum" -b < "./hello-world-blob.bin" | sed 's/ .*//') -# If the hashes match, then `hello-world-blob.bin` must be the encoding -# of `hello-world.bin`. -[[ "$encodingHash" == "$freshlyAddedHash" ]] + # If the hashes match, then `hello-world-blob.bin` must be the encoding + # of `hello-world.bin`. + [[ "$encodingHash" == "$freshlyAddedHash" ]] -# Create empty directory object for tree test -echo -n | git -C "$repo" hash-object -w -t tree --stdin + # Create empty directory object for tree test + echo -n | git -C "$repo" hash-object -w -t tree --stdin -# Relies on both child hashes already existing in the git store -freshlyAddedHash=$(git -C "$repo" mktree < "./tree.txt") -encodingHash=$(sha1sum -b < "./tree.bin" | head -c 40) + # Relies on both child hashes already existing in the git store + tree=tree-${hash} + freshlyAddedHash=$(git -C "$repo" mktree < "${tree}.txt") + encodingHash=$("${hash}sum" -b < "${tree}.bin" | sed 's/ .*//') -# If the hashes match, then `tree.bin` must be the encoding of the -# directory denoted by `tree.txt` interpreted as git directory listing. -[[ "$encodingHash" == "$freshlyAddedHash" ]] + # If the hashes match, then `tree.bin` must be the encoding of the + # directory denoted by `tree.txt` interpreted as git directory listing. + [[ "$encodingHash" == "$freshlyAddedHash" ]] +done diff --git a/src/libutil-tests/data/git/tree.bin b/src/libutil-tests/data/git/tree-sha1.bin similarity index 100% rename from src/libutil-tests/data/git/tree.bin rename to src/libutil-tests/data/git/tree-sha1.bin diff --git a/src/libutil-tests/data/git/tree.txt b/src/libutil-tests/data/git/tree-sha1.txt similarity index 100% rename from src/libutil-tests/data/git/tree.txt rename to src/libutil-tests/data/git/tree-sha1.txt diff --git a/src/libutil-tests/data/git/tree-sha256.bin b/src/libutil-tests/data/git/tree-sha256.bin new file mode 100644 index 0000000000000000000000000000000000000000..87380f4126e6eb45aa817cda8e5dbb3ec2479a42 GIT binary patch literal 181 zcmXRZN=;QTG&f=}G%zqTF;Q^K&u2K7@O5p)g04dzMej?lceD4iS3eSZzR0c9N-B10 z?e7yC4Gj#;O-&V&9E%7lH8C(SFi=QJjAF?9INMPvCOGrsL6$4Ay{+$8ZV2CMEwW|j XPRqL`_s#?>8XAF>7M7Mq5HuA4hrv-b literal 0 HcmV?d00001 diff --git a/src/libutil-tests/data/git/tree-sha256.txt b/src/libutil-tests/data/git/tree-sha256.txt new file mode 100644 index 000000000..93568a063 --- /dev/null +++ b/src/libutil-tests/data/git/tree-sha256.txt @@ -0,0 +1,4 @@ +100644 blob ce60f5ad78a08ac24872ef74d78b078f077be212e7a246893a1a5d957dfbc8b1 Foo +100755 blob ce60f5ad78a08ac24872ef74d78b078f077be212e7a246893a1a5d957dfbc8b1 bAr +040000 tree 6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321 baZ +120000 blob ce60f5ad78a08ac24872ef74d78b078f077be212e7a246893a1a5d957dfbc8b1 quuX diff --git a/src/libutil-tests/git.cc b/src/libutil-tests/git.cc index 389f8583d..d9926b9b6 100644 --- a/src/libutil-tests/git.cc +++ b/src/libutil-tests/git.cc @@ -97,7 +97,7 @@ TEST_F(GitTest, blob_write) * so that we can check our test data in a small shell script test test * (`src/libutil-tests/data/git/check-data.sh`). */ -const static Tree tree = { +const static Tree treeSha1 = { { "Foo", { @@ -133,9 +133,48 @@ const static Tree tree = { }, }; -TEST_F(GitTest, tree_read) +/** + * Same conceptual object as `treeSha1`, just different hash algorithm. + * See that one for details. + */ +const static Tree treeSha256 = { + { + "Foo", + { + .mode = Mode::Regular, + .hash = Hash::parseAny( + "ce60f5ad78a08ac24872ef74d78b078f077be212e7a246893a1a5d957dfbc8b1", HashAlgorithm::SHA256), + }, + }, + { + "bAr", + { + .mode = Mode::Executable, + .hash = Hash::parseAny( + "ce60f5ad78a08ac24872ef74d78b078f077be212e7a246893a1a5d957dfbc8b1", HashAlgorithm::SHA256), + }, + }, + { + "baZ/", + { + .mode = Mode::Directory, + .hash = Hash::parseAny( + "6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321", HashAlgorithm::SHA256), + }, + }, + { + "quuX", + { + .mode = Mode::Symlink, + .hash = Hash::parseAny( + "ce60f5ad78a08ac24872ef74d78b078f077be212e7a246893a1a5d957dfbc8b1", HashAlgorithm::SHA256), + }, + }, +}; + +static auto mkTreeReadTest(HashAlgorithm hashAlgo, Tree tree, const ExperimentalFeatureSettings & mockXpSettings) { - readTest("tree.bin", [&](const auto & encoded) { + return [hashAlgo, tree, mockXpSettings](const auto & encoded) { StringSource in{encoded}; NullFileSystemObjectSink out; Tree got; @@ -144,6 +183,7 @@ TEST_F(GitTest, tree_read) out, CanonPath::root, in, + hashAlgo, [&](auto & name, auto entry) { auto name2 = std::string{name.rel()}; if (entry.mode == Mode::Directory) @@ -153,14 +193,33 @@ TEST_F(GitTest, tree_read) mockXpSettings); ASSERT_EQ(got, tree); + }; +} + +TEST_F(GitTest, tree_sha1_read) +{ + readTest("tree-sha1.bin", mkTreeReadTest(HashAlgorithm::SHA1, treeSha1, mockXpSettings)); +} + +TEST_F(GitTest, tree_sha256_read) +{ + readTest("tree-sha256.bin", mkTreeReadTest(HashAlgorithm::SHA256, treeSha256, mockXpSettings)); +} + +TEST_F(GitTest, tree_sha1_write) +{ + writeTest("tree-sha1.bin", [&]() { + StringSink s; + dumpTree(treeSha1, s, mockXpSettings); + return s.s; }); } -TEST_F(GitTest, tree_write) +TEST_F(GitTest, tree_sha256_write) { - writeTest("tree.bin", [&]() { + writeTest("tree-sha256.bin", [&]() { StringSink s; - dumpTree(tree, s, mockXpSettings); + dumpTree(treeSha256, s, mockXpSettings); return s.s; }); } @@ -202,51 +261,54 @@ TEST_F(GitTest, both_roundrip) }, }; - std::map cas; + for (const auto hashAlgo : {HashAlgorithm::SHA1, HashAlgorithm::SHA256}) { + std::map cas; - std::function dumpHook; - dumpHook = [&](const SourcePath & path) { - StringSink s; - HashSink hashSink{HashAlgorithm::SHA1}; - TeeSink s2{s, hashSink}; - auto mode = dump(path, s2, dumpHook, defaultPathFilter, mockXpSettings); - auto hash = hashSink.finish().first; - cas.insert_or_assign(hash, std::move(s.s)); - return TreeEntry{ - .mode = mode, - .hash = hash, + std::function dumpHook; + dumpHook = [&](const SourcePath & path) { + StringSink s; + HashSink hashSink{hashAlgo}; + TeeSink s2{s, hashSink}; + auto mode = dump(path, s2, dumpHook, defaultPathFilter, mockXpSettings); + auto hash = hashSink.finish().first; + cas.insert_or_assign(hash, std::move(s.s)); + return TreeEntry{ + .mode = mode, + .hash = hash, + }; }; - }; - auto root = dumpHook({files}); + auto root = dumpHook({files}); - auto files2 = make_ref(); + auto files2 = make_ref(); - MemorySink sinkFiles2{*files2}; + MemorySink sinkFiles2{*files2}; - std::function mkSinkHook; - mkSinkHook = [&](auto prefix, auto & hash, auto blobMode) { - StringSource in{cas[hash]}; - parse( - sinkFiles2, - prefix, - in, - blobMode, - [&](const CanonPath & name, const auto & entry) { - mkSinkHook( - prefix / name, - entry.hash, - // N.B. this cast would not be acceptable in real - // code, because it would make an assert reachable, - // but it should harmless in this test. - static_cast(entry.mode)); - }, - mockXpSettings); - }; + std::function mkSinkHook; + mkSinkHook = [&](auto prefix, auto & hash, auto blobMode) { + StringSource in{cas[hash]}; + parse( + sinkFiles2, + prefix, + in, + blobMode, + hashAlgo, + [&](const CanonPath & name, const auto & entry) { + mkSinkHook( + prefix / name, + entry.hash, + // N.B. this cast would not be acceptable in real + // code, because it would make an assert reachable, + // but it should harmless in this test. + static_cast(entry.mode)); + }, + mockXpSettings); + }; - mkSinkHook(CanonPath::root, root.hash, BlobMode::Regular); + mkSinkHook(CanonPath::root, root.hash, BlobMode::Regular); - ASSERT_EQ(files->root, files2->root); + EXPECT_EQ(files->root, files2->root); + } } TEST(GitLsRemote, parseSymrefLineWithReference) diff --git a/src/libutil/git.cc b/src/libutil/git.cc index e87d5550b..bee354da4 100644 --- a/src/libutil/git.cc +++ b/src/libutil/git.cc @@ -59,7 +59,7 @@ void parseBlob( { xpSettings.require(Xp::GitHashing); - unsigned long long size = std::stoi(getStringUntil(source, 0)); + const unsigned long long size = std::stoi(getStringUntil(source, 0)); auto doRegularFile = [&](bool executable) { sink.createRegularFile(sinkPath, [&](auto & crf) { @@ -114,10 +114,11 @@ void parseTree( FileSystemObjectSink & sink, const CanonPath & sinkPath, Source & source, + HashAlgorithm hashAlgo, std::function hook, const ExperimentalFeatureSettings & xpSettings) { - unsigned long long size = std::stoi(getStringUntil(source, 0)); + const unsigned long long size = std::stoi(getStringUntil(source, 0)); unsigned long long left = size; sink.createDirectory(sinkPath); @@ -137,10 +138,15 @@ void parseTree( left -= name.size(); left -= 1; - std::string hashs = getString(source, 20); - left -= 20; + const auto hashSize = regularHashSize(hashAlgo); + std::string hashs = getString(source, hashSize); + left -= hashSize; - Hash hash(HashAlgorithm::SHA1); + if (!(hashAlgo == HashAlgorithm::SHA1 || hashAlgo == HashAlgorithm::SHA256)) { + throw Error("Unsupported hash algorithm for git trees: %s", printHashAlgo(hashAlgo)); + } + + Hash hash(hashAlgo); std::copy(hashs.begin(), hashs.end(), hash.hash); hook( @@ -171,6 +177,7 @@ void parse( const CanonPath & sinkPath, Source & source, BlobMode rootModeIfBlob, + HashAlgorithm hashAlgo, std::function hook, const ExperimentalFeatureSettings & xpSettings) { @@ -183,7 +190,7 @@ void parse( parseBlob(sink, sinkPath, source, rootModeIfBlob, xpSettings); break; case ObjectType::Tree: - parseTree(sink, sinkPath, source, hook, xpSettings); + parseTree(sink, sinkPath, source, hashAlgo, hook, xpSettings); break; default: assert(false); @@ -210,9 +217,9 @@ std::optional convertMode(SourceAccessor::Type type) } } -void restore(FileSystemObjectSink & sink, Source & source, std::function hook) +void restore(FileSystemObjectSink & sink, Source & source, HashAlgorithm hashAlgo, std::function hook) { - parse(sink, CanonPath::root, source, BlobMode::Regular, [&](CanonPath name, TreeEntry entry) { + parse(sink, CanonPath::root, source, BlobMode::Regular, hashAlgo, [&](CanonPath name, TreeEntry entry) { auto [accessor, from] = hook(entry.hash); auto stat = accessor->lstat(from); auto gotOpt = convertMode(stat.type); diff --git a/src/libutil/hash.cc b/src/libutil/hash.cc index 8ee725d2d..38ef5dd90 100644 --- a/src/libutil/hash.cc +++ b/src/libutil/hash.cc @@ -20,23 +20,6 @@ namespace nix { -static size_t regularHashSize(HashAlgorithm type) -{ - switch (type) { - case HashAlgorithm::BLAKE3: - return blake3HashSize; - case HashAlgorithm::MD5: - return md5HashSize; - case HashAlgorithm::SHA1: - return sha1HashSize; - case HashAlgorithm::SHA256: - return sha256HashSize; - case HashAlgorithm::SHA512: - return sha512HashSize; - } - unreachable(); -} - const StringSet hashAlgorithms = {"blake3", "md5", "sha1", "sha256", "sha512"}; const StringSet hashFormats = {"base64", "nix32", "base16", "sri"}; diff --git a/src/libutil/include/nix/util/git.hh b/src/libutil/include/nix/util/git.hh index 97008c53a..5140c76c4 100644 --- a/src/libutil/include/nix/util/git.hh +++ b/src/libutil/include/nix/util/git.hh @@ -94,10 +94,14 @@ void parseBlob( BlobMode blobMode, const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); +/** + * @param hashAlgo must be `HashAlgo::SHA1` or `HashAlgo::SHA256` for now. + */ void parseTree( FileSystemObjectSink & sink, const CanonPath & sinkPath, Source & source, + HashAlgorithm hashAlgo, std::function hook, const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); @@ -107,12 +111,15 @@ void parseTree( * @param rootModeIfBlob How to interpret a root blob, for which there is no * disambiguating dir entry to answer that questino. If the root it not * a blob, this is ignored. + * + * @param hashAlgo must be `HashAlgo::SHA1` or `HashAlgo::SHA256` for now. */ void parse( FileSystemObjectSink & sink, const CanonPath & sinkPath, Source & source, BlobMode rootModeIfBlob, + HashAlgorithm hashAlgo, std::function hook, const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); @@ -131,8 +138,10 @@ using RestoreHook = SourcePath(Hash); /** * Wrapper around `parse` and `RestoreSink` + * + * @param hashAlgo must be `HashAlgo::SHA1` or `HashAlgo::SHA256` for now. */ -void restore(FileSystemObjectSink & sink, Source & source, std::function hook); +void restore(FileSystemObjectSink & sink, Source & source, HashAlgorithm hashAlgo, std::function hook); /** * Dumps a single file to a sink diff --git a/src/libutil/include/nix/util/hash.hh b/src/libutil/include/nix/util/hash.hh index 4237d7660..daacd7adf 100644 --- a/src/libutil/include/nix/util/hash.hh +++ b/src/libutil/include/nix/util/hash.hh @@ -12,11 +12,26 @@ MakeError(BadHash, Error); enum struct HashAlgorithm : char { MD5 = 42, SHA1, SHA256, SHA512, BLAKE3 }; -const int blake3HashSize = 32; -const int md5HashSize = 16; -const int sha1HashSize = 20; -const int sha256HashSize = 32; -const int sha512HashSize = 64; +/** + * @return the size of a hash for the given algorithm + */ +constexpr inline size_t regularHashSize(HashAlgorithm type) +{ + switch (type) { + case HashAlgorithm::BLAKE3: + return 32; + case HashAlgorithm::MD5: + return 16; + case HashAlgorithm::SHA1: + return 20; + case HashAlgorithm::SHA256: + return 32; + case HashAlgorithm::SHA512: + return 64; + default: + assert(false); + } +} extern const StringSet hashAlgorithms; diff --git a/tests/functional/fixed.nix b/tests/functional/fixed.nix index eab3ee707..1a743ee39 100644 --- a/tests/functional/fixed.nix +++ b/tests/functional/fixed.nix @@ -74,5 +74,10 @@ rec { nar-not-recursive = f2 "foo" ./fixed.builder2.sh "nar" "md5" "3670af73070fa14077ad74e0f5ea4e42"; # Experimental feature - git = f2 "foo" ./fixed.builder2.sh "git" "sha1" "cd44baf36915d5dec8374232ea7e2057f3b4494e"; + + git-sha1 = f2 "foo" ./fixed.builder2.sh "git" "sha1" "cd44baf36915d5dec8374232ea7e2057f3b4494e"; + + git-sha256 = + f2 "foo" ./fixed.builder2.sh "git" "sha256" + "3c957653f90c34c0a8badf343b61393936cddf4a2ca93f64b21f02303ddedcc2"; } diff --git a/tests/functional/git-hashing/common.sh b/tests/functional/git-hashing/common.sh index 29c518fea..2d41c0ff8 100644 --- a/tests/functional/git-hashing/common.sh +++ b/tests/functional/git-hashing/common.sh @@ -1,3 +1,5 @@ +# shellcheck shell=bash + source ../common.sh TODO_NixOS # Need to enable git hashing feature and make sure test is ok for store we don't clear diff --git a/tests/functional/git-hashing/fixed.sh b/tests/functional/git-hashing/fixed.sh index f33d95cfa..080bc6485 100755 --- a/tests/functional/git-hashing/fixed.sh +++ b/tests/functional/git-hashing/fixed.sh @@ -5,4 +5,8 @@ source common.sh # Store layer needs bugfix requireDaemonNewerThan "2.27pre20250122" -nix-build ../fixed.nix -A git --no-out-link +nix-build ../fixed.nix -A git-sha1 --no-out-link + +if isDaemonNewer "2.31pre20250724"; then + nix-build ../fixed.nix -A git-sha256 --no-out-link +fi diff --git a/tests/functional/git-hashing/meson.build b/tests/functional/git-hashing/meson.build index d6a782cdc..342c2799c 100644 --- a/tests/functional/git-hashing/meson.build +++ b/tests/functional/git-hashing/meson.build @@ -2,7 +2,8 @@ suites += { 'name': 'git-hashing', 'deps': [], 'tests': [ - 'simple.sh', + 'simple-sha1.sh', + 'simple-sha256.sh', 'fixed.sh', ], 'workdir': meson.current_source_dir(), diff --git a/tests/functional/git-hashing/simple-common.sh b/tests/functional/git-hashing/simple-common.sh new file mode 100644 index 000000000..08b5c0e71 --- /dev/null +++ b/tests/functional/git-hashing/simple-common.sh @@ -0,0 +1,96 @@ +# shellcheck shell=bash + +source common.sh + +# Assert is set +[[ ${hashAlgo+x} ]] + +repo="$TEST_ROOT/scratch" + +initRepo () { + git init "$repo" --object-format="$hashAlgo" + + git -C "$repo" config user.email "you@example.com" + git -C "$repo" config user.name "Your Name" +} + +# Compare Nix's and git's implementation of git hashing +try () { + local expected="$1" + + local hash + hash=$(nix hash path --mode git --format base16 --algo "$hashAlgo" "$TEST_ROOT/hash-path") + [[ "$hash" == "$expected" ]] + + git -C "$repo" rm -rf hash-path || true + cp -r "$TEST_ROOT/hash-path" "$repo/hash-path" + git -C "$repo" add hash-path + git -C "$repo" commit -m "x" + git -C "$repo" status + local hash2 + hash2=$(git -C "$repo" rev-parse HEAD:hash-path) + [[ "$hash2" = "$expected" ]] +} + +# Check Nix added object has matching git hash +try2 () { + local hashPath="$1" + local expected="$2" + + local path + path=$(nix store add --mode git --hash-algo "$hashAlgo" "$repo/$hashPath") + + git -C "$repo" add "$hashPath" + git -C "$repo" commit -m "x" + git -C "$repo" status + local hashFromGit + hashFromGit=$(git -C "$repo" rev-parse "HEAD:$hashPath") + [[ "$hashFromGit" == "$expected" ]] + + local caFromNix + caFromNix=$(nix path-info --json "$path" | jq -r ".[] | .ca") + [[ "fixed:git:$hashAlgo:$(nix hash convert --to nix32 "$hashAlgo:$hashFromGit")" = "$caFromNix" ]] +} + +test0 () { + rm -rf "$TEST_ROOT/hash-path" + echo "Hello World" > "$TEST_ROOT/hash-path" +} + +test1 () { + rm -rf "$TEST_ROOT/hash-path" + mkdir "$TEST_ROOT/hash-path" + echo "Hello World" > "$TEST_ROOT/hash-path/hello" + echo "Run Hello World" > "$TEST_ROOT/hash-path/executable" + chmod +x "$TEST_ROOT/hash-path/executable" +} + +test2 () { + rm -rf "$repo/dummy1" + echo Hello World! > "$repo/dummy1" +} + +test3 () { + rm -rf "$repo/dummy2" + mkdir -p "$repo/dummy2" + echo Hello World! > "$repo/dummy2/hello" +} + +test4 () { + rm -rf "$repo/dummy3" + mkdir -p "$repo/dummy3" + mkdir -p "$repo/dummy3/dir" + touch "$repo/dummy3/dir/file" + echo Hello World! > "$repo/dummy3/dir/file" + touch "$repo/dummy3/dir/executable" + chmod +x "$repo/dummy3/dir/executable" + echo Run Hello World! > "$repo/dummy3/dir/executable" +} + +test5 () { + rm -rf "$repo/dummy4" + mkdir -p "$repo/dummy4" + mkdir -p "$repo/dummy4/dir" + touch "$repo/dummy4/dir/file" + ln -s './hello/world.txt' "$repo/dummy4/dir/symlink" +} diff --git a/tests/functional/git-hashing/simple-sha1.sh b/tests/functional/git-hashing/simple-sha1.sh new file mode 100755 index 000000000..a883ea848 --- /dev/null +++ b/tests/functional/git-hashing/simple-sha1.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +hashAlgo=sha1 + +source simple-common.sh + +initRepo + +# blob +test0 +try "557db03de997c86a4a028e1ebd3a1ceb225be238" + +# tree with children +test1 +try "e5c0a11a556801a5c9dcf330ca9d7e2c572697f4" + +test2 +try2 dummy1 "980a0d5f19a64b4b30a87d4206aade58726b60e3" + +test3 +try2 dummy2 "8b8e43b937854f4083ea56777821abda2799e850" + +test4 +try2 dummy3 "f227adfaf60d2778aabbf93df6dd061272d2dc85" + +test5 +try2 dummy4 "06f3e789820fc488d602358f03e3a1cbf993bf33" diff --git a/tests/functional/git-hashing/simple-sha256.sh b/tests/functional/git-hashing/simple-sha256.sh new file mode 100755 index 000000000..c7da71e00 --- /dev/null +++ b/tests/functional/git-hashing/simple-sha256.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +hashAlgo=sha256 + +source simple-common.sh + +requireDaemonNewerThan 2.31pre20250724 + +initRepo + +# blob +test0 +try "7c5c8610459154bdde4984be72c48fb5d9c1c4ac793a6b5976fe38fd1b0b1284" + +# tree with children +test1 +try "cd79952f42462467d0ea574b0283bb6eb77e15b2b86891e29f2b981650365474" + +test2 +try2 dummy1 "f5b5cec05fb6f9302b507a48c1573e6f36075e954d97caa8667f784e9cdb0d13" + +test3 +try2 dummy2 "399d851c74ceac2c2b61b53b13dcf5e88df3b6135c7df1f248a323c3c2f9aa78" + +test4 +try2 dummy3 "d3ae8fc87e76b9b871bd06a58c925c5fb5f83b5393f9f58e4f6dba3f59470289" + +test5 +try2 dummy4 "8c090dd057e8e01ffe1fec24a3133dfe52ba4eda822e67ee7fefc2af7c6a2906" diff --git a/tests/functional/git-hashing/simple.sh b/tests/functional/git-hashing/simple.sh deleted file mode 100755 index e02d8b297..000000000 --- a/tests/functional/git-hashing/simple.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env bash - -source common.sh - -repo="$TEST_ROOT/scratch" -git init "$repo" - -git -C "$repo" config user.email "you@example.com" -git -C "$repo" config user.name "Your Name" - -# Compare Nix's and git's implementation of git hashing -try () { - local hash=$(nix hash path --mode git --format base16 --algo sha1 $TEST_ROOT/hash-path) - [[ "$hash" == "$1" ]] - - git -C "$repo" rm -rf hash-path || true - cp -r "$TEST_ROOT/hash-path" "$TEST_ROOT/scratch/hash-path" - git -C "$repo" add hash-path - git -C "$repo" commit -m "x" - git -C "$repo" status - local hash2=$(git -C "$TEST_ROOT/scratch" rev-parse HEAD:hash-path) - [[ "$hash2" = "$1" ]] -} - -# blob -rm -rf $TEST_ROOT/hash-path -echo "Hello World" > $TEST_ROOT/hash-path -try "557db03de997c86a4a028e1ebd3a1ceb225be238" - -# tree with children -rm -rf $TEST_ROOT/hash-path -mkdir $TEST_ROOT/hash-path -echo "Hello World" > $TEST_ROOT/hash-path/hello -echo "Run Hello World" > $TEST_ROOT/hash-path/executable -chmod +x $TEST_ROOT/hash-path/executable -try "e5c0a11a556801a5c9dcf330ca9d7e2c572697f4" - -# Check Nix added object has matching git hash -try2 () { - local hashPath="$1" - local expected="$2" - - local path=$(nix store add --mode git --hash-algo sha1 "$repo/$hashPath") - - git -C "$repo" add "$hashPath" - git -C "$repo" commit -m "x" - git -C "$repo" status - local hashFromGit=$(git -C "$repo" rev-parse "HEAD:$hashPath") - [[ "$hashFromGit" == "$2" ]] - - local caFromNix=$(nix path-info --json "$path" | jq -r ".[] | .ca") - [[ "fixed:git:sha1:$(nix hash convert --to nix32 "sha1:$hashFromGit")" = "$caFromNix" ]] -} - -rm -rf "$repo/dummy1" -echo Hello World! > "$repo/dummy1" -try2 dummy1 "980a0d5f19a64b4b30a87d4206aade58726b60e3" - -rm -rf "$repo/dummy2" -mkdir -p "$repo/dummy2" -echo Hello World! > "$repo/dummy2/hello" -try2 dummy2 "8b8e43b937854f4083ea56777821abda2799e850" - -rm -rf "$repo/dummy3" -mkdir -p "$repo/dummy3" -mkdir -p "$repo/dummy3/dir" -touch "$repo/dummy3/dir/file" -echo Hello World! > "$repo/dummy3/dir/file" -touch "$repo/dummy3/dir/executable" -chmod +x "$repo/dummy3/dir/executable" -echo Run Hello World! > "$repo/dummy3/dir/executable" -try2 dummy3 "f227adfaf60d2778aabbf93df6dd061272d2dc85" - -rm -rf "$repo/dummy4" -mkdir -p "$repo/dummy4" -mkdir -p "$repo/dummy4/dir" -touch "$repo/dummy4/dir/file" -ln -s './hello/world.txt' "$repo/dummy4/dir/symlink" -try2 dummy4 "06f3e789820fc488d602358f03e3a1cbf993bf33"