Add pack diff command

This commit is contained in:
server
2026-04-14 12:16:41 +02:00
parent 0a3e975431
commit aca8a675d2
6 changed files with 180 additions and 0 deletions

View File

@@ -20,6 +20,7 @@ with.
- `m2pack keygen`
- `m2pack build`
- `m2pack diff`
- `m2pack list`
- `m2pack verify`
- `m2pack extract`
@@ -57,6 +58,7 @@ Exposed tools:
- `pack_keygen`
- `pack_build`
- `pack_diff`
- `pack_list`
- `pack_verify`
- `pack_extract`
@@ -148,6 +150,15 @@ Export a client config header for `m2dev-client-src/src/PackLib/M2PackKeys.h`:
--output /path/to/m2dev-client-src/src/PackLib/M2PackKeys.h
```
Diff a source tree against an archive:
```bash
./build/m2pack diff \
--left /path/to/client/root \
--right out/client.m2p \
--json
```
## Format summary
- Single archive file with a fixed header

View File

@@ -60,6 +60,7 @@ python scripts/mcp_smoke_test.py
- `pack_keygen`
- `pack_build`
- `pack_diff`
- `pack_list`
- `pack_verify`
- `pack_extract`

View File

@@ -44,6 +44,13 @@ Inputs:
- `key_file`
- `signing_secret_key_file`
### `pack_diff`
Inputs:
- `left`
- `right`
### `pack_list`
Inputs:

View File

@@ -96,6 +96,16 @@ server.tool(
),
);
server.tool(
"pack_diff",
"Diff two directories and/or .m2p archives using normalized paths and plaintext hashes.",
{
left: z.string(),
right: z.string(),
},
async ({ left, right }) => toolResult(runCli(["diff", "--left", left, "--right", right])),
);
server.tool(
"pack_list",
"List entries in an .m2p archive.",

View File

@@ -89,6 +89,12 @@ def pack_build(
)
@mcp.tool()
def pack_diff(left: str, right: str) -> dict[str, Any]:
"""Diff two directories and/or .m2p archives using normalized paths and plaintext hashes."""
return _run_cli("diff", "--left", left, "--right", right)
@mcp.tool()
def pack_list(archive: str) -> dict[str, Any]:
"""List entries in an .m2p archive."""

View File

@@ -5,6 +5,7 @@
#include <filesystem>
#include <iostream>
#include <map>
#include <set>
#include <sstream>
#include <sodium.h>
@@ -94,12 +95,151 @@ std::array<std::uint8_t, kAeadKeySize> load_master_key(const std::string& path)
return key;
}
struct SnapshotEntry
{
std::uint64_t size = 0;
std::array<std::uint8_t, kHashSize> hash {};
};
using SnapshotMap = std::map<std::string, SnapshotEntry>;
SnapshotMap snapshot_from_directory(const std::filesystem::path& root)
{
SnapshotMap snapshot;
for (const auto& path : collect_files(root))
{
const auto bytes = read_file(path.string());
snapshot.emplace(normalize_path(root, path), SnapshotEntry {
.size = static_cast<std::uint64_t>(bytes.size()),
.hash = hash_bytes(bytes),
});
}
return snapshot;
}
SnapshotMap snapshot_from_archive(const std::filesystem::path& archive_path)
{
SnapshotMap snapshot;
const auto archive = load_archive(archive_path);
for (const auto& entry : archive.entries)
{
snapshot.emplace(entry.path, SnapshotEntry {
.size = entry.original_size,
.hash = entry.plaintext_hash,
});
}
return snapshot;
}
SnapshotMap load_snapshot(const std::string& input)
{
const std::filesystem::path path(input);
if (std::filesystem::is_directory(path))
{
return snapshot_from_directory(path);
}
if (std::filesystem::is_regular_file(path) && path.extension() == ".m2p")
{
return snapshot_from_archive(path);
}
fail("pack_diff supports directories or .m2p archives: " + input);
}
void command_diff(const ParsedArgs& args)
{
const auto left = require_option(args, "left");
const auto right = require_option(args, "right");
const auto left_snapshot = load_snapshot(left);
const auto right_snapshot = load_snapshot(right);
std::vector<std::string> added;
std::vector<std::string> removed;
std::vector<std::string> changed;
std::size_t unchanged = 0;
std::set<std::string> all_paths;
for (const auto& [path, _] : left_snapshot)
{
all_paths.insert(path);
}
for (const auto& [path, _] : right_snapshot)
{
all_paths.insert(path);
}
for (const auto& path : all_paths)
{
const auto lit = left_snapshot.find(path);
const auto rit = right_snapshot.find(path);
if (lit == left_snapshot.end())
{
added.push_back(path);
continue;
}
if (rit == right_snapshot.end())
{
removed.push_back(path);
continue;
}
if (lit->second.size != rit->second.size || lit->second.hash != rit->second.hash)
{
changed.push_back(path);
continue;
}
unchanged += 1;
}
if (args.json)
{
auto render_array = [](const std::vector<std::string>& values) {
std::ostringstream out;
out << "[";
for (std::size_t i = 0; i < values.size(); ++i)
{
if (i != 0)
{
out << ",";
}
out << "\"" << json_escape(values[i]) << "\"";
}
out << "]";
return out.str();
};
std::cout
<< "{"
<< "\"ok\":true,"
<< "\"left\":\"" << json_escape(left) << "\","
<< "\"right\":\"" << json_escape(right) << "\","
<< "\"added\":" << render_array(added) << ","
<< "\"removed\":" << render_array(removed) << ","
<< "\"changed\":" << render_array(changed) << ","
<< "\"unchanged_count\":" << unchanged
<< "}\n";
return;
}
std::cout
<< "Added: " << added.size() << "\n"
<< "Removed: " << removed.size() << "\n"
<< "Changed: " << changed.size() << "\n"
<< "Unchanged: " << unchanged << "\n";
}
void print_usage()
{
std::cout
<< "m2pack commands:\n"
<< " keygen --out-dir <dir> [--json]\n"
<< " build --input <dir> --output <file> --key <hex-file> --sign-secret-key <hex-file> [--json]\n"
<< " diff --left <dir|archive.m2p> --right <dir|archive.m2p> [--json]\n"
<< " list --archive <file> [--json]\n"
<< " verify --archive <file> [--public-key <hex-file>] [--key <hex-file>] [--json]\n"
<< " extract --archive <file> --output <dir> --key <hex-file> [--json]\n"
@@ -359,6 +499,11 @@ int run_cli(int argc, char** argv)
command_list(args);
return 0;
}
if (args.command == "diff")
{
command_diff(args);
return 0;
}
if (args.command == "verify")
{
command_verify(args);