Skip to content
Closed
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
25 changes: 25 additions & 0 deletions doc/manual/rl-next/hash-convert-json.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
synopsis: "`nix hash convert` supports JSON format"
prs: []
issues: []
---

`nix hash convert` now supports a `json-base16` format for both input (`--from`) and output (`--to`).

This format represents hashes as structured JSON objects:

```json
{
"format": "base16",
"algorithm": "sha256",
"hash": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
}
```

Currently, the `format` field must always `"base16"` (hexadecimal) for input, and will always be that for output.

This is used in the new structured JSON outputs for store path info and derivations, and will be used whenever JSON formats needs to contain hashes going forward.

JSON input is also auto-detected when `--from` is not specified.

See [`nix hash convert`](@docroot@/command-ref/new-cli/nix3-hash-convert.md) for usage examples.
3 changes: 2 additions & 1 deletion doc/manual/rl-next/json-format-changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ The new structured format follows the [JSON guidelines](@docroot@/development/js
- Old: `"narHash": "sha256:FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc="`
- New: `"narHash": {"algorithm": "sha256", "format": "base16", "hash": "15e3c5608946..."}`
- Same structure applies to `downloadHash` in NAR info contexts
- The `format` field is always `"base16"` (hexadecimal)

See `nix hash convert`'s new support for the JSON format for details on this format, and how to convert between it and other hash formats.

Nix currently only produces, and doesn't consume this format.

Expand Down
64 changes: 62 additions & 2 deletions src/libcmd/include/nix/cmd/misc-store-flags.hh
Original file line number Diff line number Diff line change
@@ -1,5 +1,65 @@
#include "nix/util/args.hh"
#include "nix/store/content-address.hh"
#include "nix/main/common-args.hh"

namespace nix {

/**
* @brief Tag type for JSON hash output format.
*
* JSON format outputs `{"algorithm": "<algo>", "hash": "<base16>"}`.
*/
struct OutputFormatJSON
{
bool operator==(const OutputFormatJSON &) const = default;
auto operator<=>(const OutputFormatJSON &) const = default;
};

/**
* @brief Output hash format: either a HashFormat or JSON.
*/
struct OutputHashFormat
{
using Raw = std::variant<HashFormat, OutputFormatJSON>;
Raw raw;

MAKE_WRAPPER_CONSTRUCTOR(OutputHashFormat);

bool operator==(const OutputHashFormat &) const = default;
auto operator<=>(const OutputHashFormat &) const = default;

/// Convenience constant for JSON format
static constexpr struct OutputFormatJSON JSON{};

/**
* Parse an output hash format from a string.
*
* Accepts all HashFormat names plus "json-base16".
*/
static OutputHashFormat parse(std::string_view s);

/**
* The reverse of parse.
*/
std::string_view print() const;

/**
* Parse a hash from a string representation, returning both the hash
* and the output format it was parsed from.
*
* Tries to parse as JSON first (returning OutputFormatJSON if successful),
* then falls back to Hash::parseAnyReturningFormat.
*/
static std::pair<Hash, OutputHashFormat>
parseAnyReturningFormat(std::string_view s, std::optional<HashAlgorithm> optAlgo);
};

/**
* Print a hash in the specified output format.
*/
void printHash(const Hash & h, const OutputHashFormat & format, MixPrintJSON & printer);

} // namespace nix

namespace nix::flag {

Expand All @@ -11,8 +71,8 @@ static inline Args::Flag hashAlgo(HashAlgorithm * ha)
}

Args::Flag hashAlgoOpt(std::string && longName, std::optional<HashAlgorithm> * oha);
Args::Flag hashFormatWithDefault(std::string && longName, HashFormat * hf);
Args::Flag hashFormatOpt(std::string && longName, std::optional<HashFormat> * ohf);
Args::Flag hashFormatWithDefault(std::string && longName, OutputHashFormat * hf);
Args::Flag hashFormatOpt(std::string && longName, std::optional<OutputHashFormat> * ohf);

static inline Args::Flag hashAlgoOpt(std::optional<HashAlgorithm> * oha)
{
Expand Down
73 changes: 67 additions & 6 deletions src/libcmd/misc-store-flags.cc
Original file line number Diff line number Diff line change
@@ -1,4 +1,61 @@
#include "nix/cmd/misc-store-flags.hh"
#include "nix/util/json-utils.hh"

namespace nix {

constexpr static std::string_view jsonFormatName = "json-base16";

OutputHashFormat OutputHashFormat::parse(std::string_view s)
{
if (s == jsonFormatName) {
return OutputHashFormat::JSON;
}
return parseHashFormat(s);
}

std::string_view OutputHashFormat::print() const
{
return std::visit(
overloaded{
[](const HashFormat & hf) -> std::string_view { return printHashFormat(hf); },
[](const OutputFormatJSON &) -> std::string_view { return jsonFormatName; },
},
raw);
}

std::pair<Hash, OutputHashFormat>
OutputHashFormat::parseAnyReturningFormat(std::string_view s, std::optional<HashAlgorithm> optAlgo)
{
/* Try parsing as JSON first. If it is valid JSON, it must also be
in the right format. Otherwise, parse string formats. */
std::optional<nlohmann::json> jsonOpt;
try {
jsonOpt = nlohmann::json::parse(s);
} catch (nlohmann::json::parse_error &) {
}

if (jsonOpt) {
auto hash = jsonOpt->get<Hash>();
if (optAlgo && hash.algo != *optAlgo)
throw BadHash("hash '%s' should have type '%s'", s, printHashAlgo(*optAlgo));
return {hash, OutputHashFormat::JSON};
}

auto [hash, format] = Hash::parseAnyReturningFormat(s, optAlgo);
return {hash, format};
}

void printHash(const Hash & h, const OutputHashFormat & format, MixPrintJSON & printer)
{
std::visit(
overloaded{
[&](const HashFormat & hf) { logger->cout(h.to_string(hf, hf == HashFormat::SRI)); },
[&](const OutputFormatJSON &) { printer.printJSON(nlohmann::json(h)); },
},
format.raw);
}

} // namespace nix

namespace nix::flag {

Expand All @@ -9,27 +66,31 @@ static void hashFormatCompleter(AddCompletions & completions, size_t index, std:
completions.add(format);
}
}
auto jsonName = OutputHashFormat(OutputHashFormat::JSON).print();
if (hasPrefix(jsonName, prefix)) {
completions.add(std::string{jsonName});
}
}

Args::Flag hashFormatWithDefault(std::string && longName, HashFormat * hf)
Args::Flag hashFormatWithDefault(std::string && longName, OutputHashFormat * hf)
{
assert(*hf == nix::HashFormat::SRI);
return Args::Flag{
.longName = std::move(longName),
.description = "Hash format (`base16`, `nix32`, `base64`, `sri`). Default: `sri`.",
.description = "Hash format (`base16`, `nix32`, `base64`, `sri`, `json-base16`). Default: `sri`.",
.labels = {"hash-format"},
.handler = {[hf](std::string s) { *hf = parseHashFormat(s); }},
.handler = {[hf](std::string s) { *hf = OutputHashFormat::parse(s); }},
.completer = hashFormatCompleter,
};
}

Args::Flag hashFormatOpt(std::string && longName, std::optional<HashFormat> * ohf)
Args::Flag hashFormatOpt(std::string && longName, std::optional<OutputHashFormat> * ohf)
{
return Args::Flag{
.longName = std::move(longName),
.description = "Hash format (`base16`, `nix32`, `base64`, `sri`).",
.description = "Hash format (`base16`, `nix32`, `base64`, `sri`, `json-base16`).",
.labels = {"hash-format"},
.handler = {[ohf](std::string s) { *ohf = std::optional<HashFormat>{parseHashFormat(s)}; }},
.handler = {[ohf](std::string s) { *ohf = std::optional<OutputHashFormat>{OutputHashFormat::parse(s)}; }},
.completer = hashFormatCompleter,
};
}
Expand Down
14 changes: 8 additions & 6 deletions src/libutil/include/nix/util/variant-wrapper.hh
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@
*
* The moral equivalent of `using Raw::Raw;`
*/
#define MAKE_WRAPPER_CONSTRUCTOR(CLASS_NAME) \
FORCE_DEFAULT_CONSTRUCTORS(CLASS_NAME) \
\
CLASS_NAME(auto &&... arg) \
: raw(std::forward<decltype(arg)>(arg)...) \
{ \
#define MAKE_WRAPPER_CONSTRUCTOR(CLASS_NAME) \
FORCE_DEFAULT_CONSTRUCTORS(CLASS_NAME) \
\
template<typename... Args> \
requires(!(sizeof...(Args) == 1 && (std::is_same_v<std::remove_cvref_t<Args>, CLASS_NAME> && ...))) \
CLASS_NAME(Args &&... arg) \
: raw(std::forward<Args>(arg)...) \
{ \
}
22 changes: 22 additions & 0 deletions src/nix/hash-convert.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,28 @@ R""(
sha256-ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0=
```

* Convert a hash to the `json-base16` format:

```console
$ nix hash convert --to json-base16 "sha256-ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0="
{
"algorithm":"sha256",
"format":"base16",
"hash":"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
}
```

* Convert a hash from the `json-base16` format:

```console
$ nix hash convert --from json-base16 '{
"format": "base16",
"algorithm": "sha256",
"hash": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
}'
sha256-ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0=
```

# Description

`nix hash convert` converts hashes from one encoding to another.
Expand Down
29 changes: 14 additions & 15 deletions src/nix/hash.cc
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ using namespace nix;
*
* Deprecation Issue: https://github.com/NixOS/nix/issues/8876
*/
struct CmdHashBase : Command
struct CmdHashBase : Command, MixPrintJSON
{
FileIngestionMethod mode;
HashFormat hashFormat = HashFormat::SRI;
OutputHashFormat hashFormat = HashFormat::SRI;
bool truncate = false;
HashAlgorithm hashAlgo = HashAlgorithm::SHA256;
std::vector<std::string> paths;
Expand All @@ -37,25 +37,25 @@ struct CmdHashBase : Command
addFlag({
.longName = "sri",
.description = "Print the hash in SRI format.",
.handler = {&hashFormat, HashFormat::SRI},
.handler = {&hashFormat, OutputHashFormat{HashFormat::SRI}},
});

addFlag({
.longName = "base64",
.description = "Print the hash in base-64 format.",
.handler = {&hashFormat, HashFormat::Base64},
.handler = {&hashFormat, OutputHashFormat{HashFormat::Base64}},
});

addFlag({
.longName = "base32",
.description = "Print the hash in base-32 (Nix-specific) format.",
.handler = {&hashFormat, HashFormat::Nix32},
.handler = {&hashFormat, OutputHashFormat{HashFormat::Nix32}},
});

addFlag({
.longName = "base16",
.description = "Print the hash in base-16 format.",
.handler = {&hashFormat, HashFormat::Base16},
.handler = {&hashFormat, OutputHashFormat{HashFormat::Base16}},
});

addFlag(flag::hashAlgo("type", &hashAlgo));
Expand Down Expand Up @@ -129,7 +129,7 @@ struct CmdHashBase : Command

if (truncate && h.hashSize > 20)
h = compressHash(h, 20);
logger->cout(h.to_string(hashFormat, hashFormat == HashFormat::SRI));
printHash(h, hashFormat, *this);
}
}
};
Expand Down Expand Up @@ -209,15 +209,14 @@ struct CmdToBase : Command
/**
* `nix hash convert`
*/
struct CmdHashConvert : Command
struct CmdHashConvert : Command, MixPrintJSON
{
std::optional<HashFormat> from;
HashFormat to;
std::optional<OutputHashFormat> from;
OutputHashFormat to = HashFormat::SRI;
std::optional<HashAlgorithm> algo;
std::vector<std::string> hashStrings;

CmdHashConvert()
: to(HashFormat::SRI)
{
addFlag(flag::hashFormatOpt("from", &from));
addFlag(flag::hashFormatWithDefault("to", &to));
Expand Down Expand Up @@ -248,15 +247,15 @@ struct CmdHashConvert : Command
void run() override
{
for (const auto & s : hashStrings) {
auto [h, parsedFormat] = Hash::parseAnyReturningFormat(s, algo);
auto [h, parsedFormat] = OutputHashFormat::parseAnyReturningFormat(s, algo);
if (from && *from != parsedFormat) {
throw BadHash(
"input hash '%s' has format '%s', but '--from %s' was specified",
s,
printHashFormat(parsedFormat),
printHashFormat(*from));
parsedFormat.print(),
from->print());
}
logger->cout(h.to_string(to, to == HashFormat::SRI));
printHash(h, to, *this);
}
}
};
Expand Down
Loading
Loading