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
5 changes: 5 additions & 0 deletions include/vix/cli/util/Hash.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ namespace vix::cli::util
std::uint64_t fnv1a64_str(const std::string &s, std::uint64_t seed);
std::string hex64(std::uint64_t v);
std::optional<std::string> read_file_hash_hex(const fs::path &p);

// SHA256 hashing
std::optional<std::string> sha256_file(const fs::path &p);
std::optional<std::string> sha256_directory(const fs::path &dir);

std::string compute_project_files_fingerprint(const fs::path &projectDir);
bool signature_matches(const fs::path &sigFile, const std::string &sig);
std::string signature_join(const std::vector<std::pair<std::string, std::string>> &kvs);
Expand Down
15 changes: 11 additions & 4 deletions src/commands/AddCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include <vix/cli/commands/AddCommand.hpp>
#include <vix/cli/util/Shell.hpp>
#include <vix/cli/util/Ui.hpp>
#include <vix/cli/util/Hash.hpp>
#include <vix/cli/Style.hpp>
#include <vix/utils/Env.hpp>
#include <nlohmann/json.hpp>
Expand Down Expand Up @@ -191,7 +192,8 @@ namespace vix::commands
const PkgSpec &spec,
const std::string &repoUrl,
const std::string &commitSha,
const std::string &tag)
const std::string &tag,
const std::string &contentHash)
{
json lock;

Expand Down Expand Up @@ -222,10 +224,11 @@ namespace vix::commands

json dep;
dep["id"] = wantedId;
dep["version"] = spec.resolvedVersion; // IMPORTANT
dep["version"] = spec.resolvedVersion;
dep["repo"] = repoUrl;
dep["tag"] = tag;
dep["commit"] = commitSha;
dep["hash"] = contentHash;

deps.push_back(dep);

Expand Down Expand Up @@ -536,8 +539,12 @@ namespace vix::commands
outCommit = commit;
outTag = tag;

// lockfile = version exacte résolue + commit
write_lockfile_append(spec, repoUrl, commit, tag);
// Compute content hash for deterministic verify
const auto contentHash = vix::cli::util::sha256_directory(outDir);
const std::string hashStr = contentHash.value_or("");

// lockfile = version exacte résolue + commit + hash
write_lockfile_append(spec, repoUrl, commit, tag, hashStr);

return 0;
}
Expand Down
29 changes: 29 additions & 0 deletions src/commands/DepsCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include <vix/cli/commands/DepsCommand.hpp>
#include <vix/cli/util/Ui.hpp>
#include <vix/cli/util/Shell.hpp>
#include <vix/cli/util/Hash.hpp>
#include <vix/cli/Style.hpp>
#include <vix/utils/Env.hpp>

Expand Down Expand Up @@ -207,6 +208,7 @@ namespace vix::commands
std::string repo;
std::string tag;
std::string commit;
std::string hash;

std::string type; // "header-only" etc.
std::string include; // include folder for header-only
Expand All @@ -222,6 +224,7 @@ namespace vix::commands
dep.repo = d.value("repo", "");
dep.tag = d.value("tag", "");
dep.commit = d.value("commit", "");
dep.hash = d.value("hash", "");

if (dep.id.empty() || dep.repo.empty() || dep.commit.empty())
throw std::runtime_error("invalid dependency entry in vix.lock (missing id/repo/commit)");
Expand Down Expand Up @@ -406,6 +409,32 @@ namespace vix::commands
vix::cli::util::kv(std::cout, "status", "fetched");
}

// Verify hash if present in lockfile
if (!dep.hash.empty())
{
const auto actualHashOpt = vix::cli::util::sha256_directory(dep.checkout);
if (actualHashOpt)
{
if (*actualHashOpt != dep.hash)
{
vix::cli::util::err_line(std::cerr, "integrity check failed: " + dep.id);
vix::cli::util::err_line(std::cerr, " expected: " + dep.hash);
vix::cli::util::err_line(std::cerr, " actual: " + *actualHashOpt);
vix::cli::util::warn_line(std::cerr, "The checkout in store has been modified or is corrupt.");
vix::cli::util::warn_line(std::cerr, "Try: vix store gc && vix deps");
return 1;
}
else
{
vix::cli::util::kv(std::cout, "verify", "ok");
}
}
else
{
vix::cli::util::warn_line(std::cerr, "could not compute hash for: " + dep.id);
}
}

load_dep_manifest(dep);
vix::cli::util::kv(std::cout, "commit", dep.commit);

Expand Down
111 changes: 77 additions & 34 deletions src/commands/StoreCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -169,30 +169,33 @@ namespace vix::commands
return 0;
}

int store_gc_project()
int store_gc_project(bool dryRun = false)
{
const fs::path root = store_root_git();
const fs::path lockP = lock_path();

vix::cli::util::section(std::cout, "Store");
vix::cli::util::kv(std::cout, "action", "gc");
vix::cli::util::section(std::cout, "Store GC");
vix::cli::util::kv(std::cout, "scope", "project");
vix::cli::util::kv(std::cout, "lock", lockP.string());
vix::cli::util::kv(std::cout, "root", root.string());

if (dryRun)
vix::cli::util::kv(std::cout, "mode", "dry-run");

if (!fs::exists(lockP))
{
vix::cli::util::err_line(std::cerr, "missing lock file: " + lockP.string());
vix::cli::util::warn_line(std::cerr, "Tip: run in a project folder containing vix.lock");
return 1;
}

vix::cli::util::warn_line(std::cerr, "WARNING: This will remove any store entries NOT used by THIS project.");
vix::cli::util::warn_line(std::cerr, "If you have other Vix projects, their cached dependencies might be deleted.");
std::cerr << "\n";

if (!fs::exists(root))
{
vix::cli::util::ok_line(std::cout, "GC done.");
vix::cli::util::kv(std::cout, "removed commits", "0");
vix::cli::util::kv(std::cout, "removed packages", "0");
vix::cli::util::kv(std::cout, "freed", "0 B");
vix::cli::util::ok_line(std::cout, "GC done (store empty).");
return 0;
}

Expand Down Expand Up @@ -259,36 +262,52 @@ namespace vix::commands

for (const auto &cdir : commitsToRemove)
{
freed += dir_size_bytes(cdir);

std::error_code rmec;
fs::remove_all(cdir, rmec);
if (!rmec)
const auto sz = dir_size_bytes(cdir);
freed += sz;

if (!dryRun)
{
std::error_code rmec;
fs::remove_all(cdir, rmec);
if (!rmec)
{
vix::cli::util::info_line(std::cout, "removed: " + pkgName + " [" + cdir.filename().string().substr(0, 8) + "...]");
++removedCommits;
}
}
else
{
vix::cli::util::info_line(std::cout, "would remove: " + pkgName + " [" + cdir.filename().string().substr(0, 8) + "...]");
++removedCommits;
}
}

// If package dir empty -> remove it
std::error_code ec3;
bool empty = true;
for (auto it = fs::directory_iterator(pkgDir, fs::directory_options::skip_permission_denied, ec3);
it != fs::directory_iterator(); ++it)
if (!dryRun)
{
empty = false;
break;
}
if (!ec3 && empty)
{
std::error_code rmec2;
fs::remove(pkgDir, rmec2);
if (!rmec2)
++removedPackages;
std::error_code ec3;
bool empty = true;
for (auto it = fs::directory_iterator(pkgDir, fs::directory_options::skip_permission_denied, ec3);
it != fs::directory_iterator(); ++it)
{
empty = false;
break;
}
if (!ec3 && empty)
{
std::error_code rmec2;
fs::remove(pkgDir, rmec2);
if (!rmec2)
++removedPackages;
}
}
}

vix::cli::util::ok_line(std::cout, "GC done.");
vix::cli::util::kv(std::cout, "removed commits", std::to_string(removedCommits));
vix::cli::util::kv(std::cout, "removed packages", std::to_string(removedPackages));
vix::cli::util::kv(std::cout, "freed", human_bytes(freed));
vix::cli::util::one_line_spacer(std::cout);
vix::cli::util::ok_line(std::cout, dryRun ? "GC dry-run finished." : "GC finished.");
vix::cli::util::kv(std::cout, dryRun ? "would remove commits" : "removed commits", std::to_string(removedCommits));
vix::cli::util::kv(std::cout, dryRun ? "would remove packages" : "removed packages", std::to_string(removedPackages));
vix::cli::util::kv(std::cout, dryRun ? "would free" : "freed", human_bytes(freed));
return 0;
}
}
Expand All @@ -304,24 +323,48 @@ namespace vix::commands
return store_path();

if (sub == "gc")
return store_gc_project();
{
bool dryRun = false;
bool projectScope = false;

for (std::size_t i = 1; i < args.size(); ++i)
{
if (args[i] == "--dry-run")
dryRun = true;
else if (args[i] == "--project")
projectScope = true;
}

if (!projectScope)
{
vix::cli::util::err_line(std::cerr, "GC requires --project scope in this version.");
vix::cli::util::warn_line(std::cerr, "Use: vix store gc --project");
return 1;
}

return store_gc_project(dryRun);
}

vix::cli::util::err_line(std::cerr, "unknown store subcommand: " + sub);
vix::cli::util::warn_line(std::cerr, "Try: vix store path");
vix::cli::util::warn_line(std::cerr, "Try: vix store gc");
vix::cli::util::warn_line(std::cerr, "Try: vix store gc --project");
return help();
}

int StoreCommand::help()
{
std::cout
<< "Usage:\n"
<< " vix store <subcommand>\n\n"
<< " vix store <subcommand> [options]\n\n"
<< "Subcommands:\n"
<< " path Print local store root path\n"
<< " gc Garbage collect the store (project scope)\n\n"
<< " gc Garbage collect the store\n\n"
<< "GC Options:\n"
<< " --project Scope GC to the current project (uses vix.lock)\n"
<< " --dry-run List files that would be removed without deleting them\n\n"
<< "Notes:\n"
<< " - GC scope=project keeps commits referenced by ./vix.lock\n";
<< " - GC --project keeps commits referenced by ./vix.lock\n"
<< " - WARNING: GC --project is destructive for other projects sharing the same store.\n";
return 0;
}
}
58 changes: 58 additions & 0 deletions src/util/Hash.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@
#include <algorithm>
#include <fstream>
#include <sstream>
#include <vector>

#include <vix/cli/util/Fs.hpp>
#include <vix/crypto/hash.hpp>
#include <vix/crypto/hex.hpp>

namespace vix::cli::util
{
Expand Down Expand Up @@ -74,6 +77,61 @@ namespace vix::cli::util
return hex64(h);
}

std::optional<std::string> sha256_file(const fs::path &p)
{
std::ifstream ifs(p, std::ios::binary);
if (!ifs)
return std::nullopt;

std::vector<char> buf((std::istreambuf_iterator<char>(ifs)),
(std::istreambuf_iterator<char>()));

std::uint8_t out[32];
auto res = vix::crypto::sha256(std::string_view(buf.data(), buf.size()), out);
if (!res)
return std::nullopt;

return vix::crypto::hex_lower(out);
}

std::optional<std::string> sha256_directory(const fs::path &dir)
{
std::error_code ec;
if (!fs::exists(dir, ec) || !fs::is_directory(dir, ec))
return std::nullopt;

std::vector<fs::path> files;
for (const auto &it : fs::recursive_directory_iterator(dir, ec))
{
if (ec)
break;
if (it.is_regular_file())
files.push_back(it.path());
}

if (ec)
return std::nullopt;

std::sort(files.begin(), files.end());

std::string combined;
for (const auto &p : files)
{
auto rel = fs::relative(p, dir).string();
auto h = sha256_file(p);
if (!h)
return std::nullopt;
combined += rel + ":" + *h + "\n";
}

std::uint8_t out[32];
auto res = vix::crypto::sha256(combined, out);
if (!res)
return std::nullopt;

return vix::crypto::hex_lower(out);
}

std::string compute_project_files_fingerprint(const fs::path &projectDir)
{
std::vector<fs::path> files;
Expand Down