Add pack diff command
This commit is contained in:
11
README.md
11
README.md
@@ -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
|
||||
|
||||
@@ -60,6 +60,7 @@ python scripts/mcp_smoke_test.py
|
||||
|
||||
- `pack_keygen`
|
||||
- `pack_build`
|
||||
- `pack_diff`
|
||||
- `pack_list`
|
||||
- `pack_verify`
|
||||
- `pack_extract`
|
||||
|
||||
@@ -44,6 +44,13 @@ Inputs:
|
||||
- `key_file`
|
||||
- `signing_secret_key_file`
|
||||
|
||||
### `pack_diff`
|
||||
|
||||
Inputs:
|
||||
|
||||
- `left`
|
||||
- `right`
|
||||
|
||||
### `pack_list`
|
||||
|
||||
Inputs:
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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."""
|
||||
|
||||
145
src/cli.cpp
145
src/cli.cpp
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user