mirror of
https://codeberg.org/ziglang/zig.git
synced 2025-12-06 13:54:21 +00:00
This commit reworks how anonymous struct literals and tuples work.
Previously, an untyped anonymous struct literal
(e.g. `const x = .{ .a = 123 }`) was given an "anonymous struct type",
which is a special kind of struct which coerces using structural
equivalence. This mechanism was a holdover from before we used
RLS / result types as the primary mechanism of type inference. This
commit changes the language so that the type assigned here is a "normal"
struct type. It uses a form of equivalence based on the AST node and the
type's structure, much like a reified (`@Type`) type.
Additionally, tuples have been simplified. The distinction between
"simple" and "complex" tuple types is eliminated. All tuples, even those
explicitly declared using `struct { ... }` syntax, use structural
equivalence, and do not undergo staged type resolution. Tuples are very
restricted: they cannot have non-`auto` layouts, cannot have aligned
fields, and cannot have default values with the exception of `comptime`
fields. Tuples currently do not have optimized layout, but this can be
changed in the future.
This change simplifies the language, and fixes some problematic
coercions through pointers which led to unintuitive behavior.
Resolves: #16865
373 lines
13 KiB
Zig
373 lines
13 KiB
Zig
// https://github.com/P-H-C/phc-string-format
|
|
|
|
const std = @import("std");
|
|
const fmt = std.fmt;
|
|
const io = std.io;
|
|
const mem = std.mem;
|
|
const meta = std.meta;
|
|
|
|
const fields_delimiter = "$";
|
|
const fields_delimiter_scalar = '$';
|
|
const version_param_name = "v";
|
|
const params_delimiter = ",";
|
|
const params_delimiter_scalar = ',';
|
|
const kv_delimiter = "=";
|
|
const kv_delimiter_scalar = '=';
|
|
|
|
pub const Error = std.crypto.errors.EncodingError || error{NoSpaceLeft};
|
|
|
|
const B64Decoder = std.base64.standard_no_pad.Decoder;
|
|
const B64Encoder = std.base64.standard_no_pad.Encoder;
|
|
|
|
/// A wrapped binary value whose maximum size is `max_len`.
|
|
///
|
|
/// This type must be used whenever a binary value is encoded in a PHC-formatted string.
|
|
/// This includes `salt`, `hash`, and any other binary parameters such as keys.
|
|
///
|
|
/// Once initialized, the actual value can be read with the `constSlice()` function.
|
|
pub fn BinValue(comptime max_len: usize) type {
|
|
return struct {
|
|
const Self = @This();
|
|
const capacity = max_len;
|
|
const max_encoded_length = B64Encoder.calcSize(max_len);
|
|
|
|
buf: [max_len]u8 = undefined,
|
|
len: usize = 0,
|
|
|
|
/// Wrap an existing byte slice
|
|
pub fn fromSlice(slice: []const u8) Error!Self {
|
|
if (slice.len > capacity) return Error.NoSpaceLeft;
|
|
var bin_value: Self = undefined;
|
|
@memcpy(bin_value.buf[0..slice.len], slice);
|
|
bin_value.len = slice.len;
|
|
return bin_value;
|
|
}
|
|
|
|
/// Return the slice containing the actual value.
|
|
pub fn constSlice(self: *const Self) []const u8 {
|
|
return self.buf[0..self.len];
|
|
}
|
|
|
|
fn fromB64(self: *Self, str: []const u8) !void {
|
|
const len = B64Decoder.calcSizeForSlice(str) catch return Error.InvalidEncoding;
|
|
if (len > self.buf.len) return Error.NoSpaceLeft;
|
|
B64Decoder.decode(&self.buf, str) catch return Error.InvalidEncoding;
|
|
self.len = len;
|
|
}
|
|
|
|
fn toB64(self: *const Self, buf: []u8) ![]const u8 {
|
|
const value = self.constSlice();
|
|
const len = B64Encoder.calcSize(value.len);
|
|
if (len > buf.len) return Error.NoSpaceLeft;
|
|
return B64Encoder.encode(buf, value);
|
|
}
|
|
};
|
|
}
|
|
|
|
/// Deserialize a PHC-formatted string into a structure `HashResult`.
|
|
///
|
|
/// Required field in the `HashResult` structure:
|
|
/// - `alg_id`: algorithm identifier
|
|
/// Optional, special fields:
|
|
/// - `alg_version`: algorithm version (unsigned integer)
|
|
/// - `salt`: salt
|
|
/// - `hash`: output of the hash function
|
|
///
|
|
/// Other fields will also be deserialized from the function parameters section.
|
|
pub fn deserialize(comptime HashResult: type, str: []const u8) Error!HashResult {
|
|
var out = mem.zeroes(HashResult);
|
|
var it = mem.splitScalar(u8, str, fields_delimiter_scalar);
|
|
var set_fields: usize = 0;
|
|
|
|
while (true) {
|
|
// Read the algorithm identifier
|
|
if ((it.next() orelse return Error.InvalidEncoding).len != 0) return Error.InvalidEncoding;
|
|
out.alg_id = it.next() orelse return Error.InvalidEncoding;
|
|
set_fields += 1;
|
|
|
|
// Read the optional version number
|
|
var field = it.next() orelse break;
|
|
if (kvSplit(field)) |opt_version| {
|
|
if (mem.eql(u8, opt_version.key, version_param_name)) {
|
|
if (@hasField(HashResult, "alg_version")) {
|
|
const value_type_info = switch (@typeInfo(@TypeOf(out.alg_version))) {
|
|
.optional => |opt| @typeInfo(opt.child),
|
|
else => |t| t,
|
|
};
|
|
out.alg_version = fmt.parseUnsigned(
|
|
@Type(value_type_info),
|
|
opt_version.value,
|
|
10,
|
|
) catch return Error.InvalidEncoding;
|
|
set_fields += 1;
|
|
}
|
|
field = it.next() orelse break;
|
|
}
|
|
} else |_| {}
|
|
|
|
// Read optional parameters
|
|
var has_params = false;
|
|
var it_params = mem.splitScalar(u8, field, params_delimiter_scalar);
|
|
while (it_params.next()) |params| {
|
|
const param = kvSplit(params) catch break;
|
|
var found = false;
|
|
inline for (comptime meta.fields(HashResult)) |p| {
|
|
if (mem.eql(u8, p.name, param.key)) {
|
|
switch (@typeInfo(p.type)) {
|
|
.int => @field(out, p.name) = fmt.parseUnsigned(
|
|
p.type,
|
|
param.value,
|
|
10,
|
|
) catch return Error.InvalidEncoding,
|
|
.pointer => |ptr| {
|
|
if (!ptr.is_const) @compileError("Value slice must be constant");
|
|
@field(out, p.name) = param.value;
|
|
},
|
|
.@"struct" => try @field(out, p.name).fromB64(param.value),
|
|
else => std.debug.panic(
|
|
"Value for [{s}] must be an integer, a constant slice or a BinValue",
|
|
.{p.name},
|
|
),
|
|
}
|
|
set_fields += 1;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) return Error.InvalidEncoding; // An unexpected parameter was found in the string
|
|
has_params = true;
|
|
}
|
|
|
|
// No separator between an empty parameters set and the salt
|
|
if (has_params) field = it.next() orelse break;
|
|
|
|
// Read an optional salt
|
|
if (@hasField(HashResult, "salt")) {
|
|
try out.salt.fromB64(field);
|
|
set_fields += 1;
|
|
} else {
|
|
return Error.InvalidEncoding;
|
|
}
|
|
|
|
// Read an optional hash
|
|
field = it.next() orelse break;
|
|
if (@hasField(HashResult, "hash")) {
|
|
try out.hash.fromB64(field);
|
|
set_fields += 1;
|
|
} else {
|
|
return Error.InvalidEncoding;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Check that all the required fields have been set, excluding optional values and parameters
|
|
// with default values
|
|
var expected_fields: usize = 0;
|
|
inline for (comptime meta.fields(HashResult)) |p| {
|
|
if (@typeInfo(p.type) != .optional and p.default_value == null) {
|
|
expected_fields += 1;
|
|
}
|
|
}
|
|
if (set_fields < expected_fields) return Error.InvalidEncoding;
|
|
|
|
return out;
|
|
}
|
|
|
|
/// Serialize parameters into a PHC string.
|
|
///
|
|
/// Required field for `params`:
|
|
/// - `alg_id`: algorithm identifier
|
|
/// Optional, special fields:
|
|
/// - `alg_version`: algorithm version (unsigned integer)
|
|
/// - `salt`: salt
|
|
/// - `hash`: output of the hash function
|
|
///
|
|
/// `params` can also include any additional parameters.
|
|
pub fn serialize(params: anytype, str: []u8) Error![]const u8 {
|
|
var buf = io.fixedBufferStream(str);
|
|
try serializeTo(params, buf.writer());
|
|
return buf.getWritten();
|
|
}
|
|
|
|
/// Compute the number of bytes required to serialize `params`
|
|
pub fn calcSize(params: anytype) usize {
|
|
var buf = io.countingWriter(io.null_writer);
|
|
serializeTo(params, buf.writer()) catch unreachable;
|
|
return @as(usize, @intCast(buf.bytes_written));
|
|
}
|
|
|
|
fn serializeTo(params: anytype, out: anytype) !void {
|
|
const HashResult = @TypeOf(params);
|
|
try out.writeAll(fields_delimiter);
|
|
try out.writeAll(params.alg_id);
|
|
|
|
if (@hasField(HashResult, "alg_version")) {
|
|
if (@typeInfo(@TypeOf(params.alg_version)) == .optional) {
|
|
if (params.alg_version) |alg_version| {
|
|
try out.print(
|
|
"{s}{s}{s}{}",
|
|
.{ fields_delimiter, version_param_name, kv_delimiter, alg_version },
|
|
);
|
|
}
|
|
} else {
|
|
try out.print(
|
|
"{s}{s}{s}{}",
|
|
.{ fields_delimiter, version_param_name, kv_delimiter, params.alg_version },
|
|
);
|
|
}
|
|
}
|
|
|
|
var has_params = false;
|
|
inline for (comptime meta.fields(HashResult)) |p| {
|
|
if (comptime !(mem.eql(u8, p.name, "alg_id") or
|
|
mem.eql(u8, p.name, "alg_version") or
|
|
mem.eql(u8, p.name, "hash") or
|
|
mem.eql(u8, p.name, "salt")))
|
|
{
|
|
const value = @field(params, p.name);
|
|
try out.writeAll(if (has_params) params_delimiter else fields_delimiter);
|
|
if (@typeInfo(p.type) == .@"struct") {
|
|
var buf: [@TypeOf(value).max_encoded_length]u8 = undefined;
|
|
try out.print("{s}{s}{s}", .{ p.name, kv_delimiter, try value.toB64(&buf) });
|
|
} else {
|
|
try out.print(
|
|
if (@typeInfo(@TypeOf(value)) == .pointer) "{s}{s}{s}" else "{s}{s}{}",
|
|
.{ p.name, kv_delimiter, value },
|
|
);
|
|
}
|
|
has_params = true;
|
|
}
|
|
}
|
|
|
|
var has_salt = false;
|
|
if (@hasField(HashResult, "salt")) {
|
|
var buf: [@TypeOf(params.salt).max_encoded_length]u8 = undefined;
|
|
try out.print("{s}{s}", .{ fields_delimiter, try params.salt.toB64(&buf) });
|
|
has_salt = true;
|
|
}
|
|
|
|
if (@hasField(HashResult, "hash")) {
|
|
var buf: [@TypeOf(params.hash).max_encoded_length]u8 = undefined;
|
|
if (!has_salt) try out.writeAll(fields_delimiter);
|
|
try out.print("{s}{s}", .{ fields_delimiter, try params.hash.toB64(&buf) });
|
|
}
|
|
}
|
|
|
|
// Split a `key=value` string into `key` and `value`
|
|
fn kvSplit(str: []const u8) !struct { key: []const u8, value: []const u8 } {
|
|
var it = mem.splitScalar(u8, str, kv_delimiter_scalar);
|
|
const key = it.first();
|
|
const value = it.next() orelse return Error.InvalidEncoding;
|
|
return .{ .key = key, .value = value };
|
|
}
|
|
|
|
test "phc format - encoding/decoding" {
|
|
const Input = struct {
|
|
str: []const u8,
|
|
HashResult: type,
|
|
};
|
|
const inputs = [_]Input{
|
|
.{
|
|
.str = "$argon2id$v=19$key=a2V5,m=4096,t=0,p=1$X1NhbHQAAAAAAAAAAAAAAA$bWh++MKN1OiFHKgIWTLvIi1iHicmHH7+Fv3K88ifFfI",
|
|
.HashResult = struct {
|
|
alg_id: []const u8,
|
|
alg_version: u16,
|
|
key: BinValue(16),
|
|
m: usize,
|
|
t: u64,
|
|
p: u32,
|
|
salt: BinValue(16),
|
|
hash: BinValue(32),
|
|
},
|
|
},
|
|
.{
|
|
.str = "$scrypt$v=1$ln=15,r=8,p=1$c2FsdHNhbHQ$dGVzdHBhc3M",
|
|
.HashResult = struct {
|
|
alg_id: []const u8,
|
|
alg_version: ?u30,
|
|
ln: u6,
|
|
r: u30,
|
|
p: u30,
|
|
salt: BinValue(16),
|
|
hash: BinValue(16),
|
|
},
|
|
},
|
|
.{
|
|
.str = "$scrypt",
|
|
.HashResult = struct { alg_id: []const u8 },
|
|
},
|
|
.{ .str = "$scrypt$v=1", .HashResult = struct { alg_id: []const u8, alg_version: u16 } },
|
|
.{
|
|
.str = "$scrypt$ln=15,r=8,p=1",
|
|
.HashResult = struct { alg_id: []const u8, alg_version: ?u30, ln: u6, r: u30, p: u30 },
|
|
},
|
|
.{
|
|
.str = "$scrypt$c2FsdHNhbHQ",
|
|
.HashResult = struct { alg_id: []const u8, salt: BinValue(16) },
|
|
},
|
|
.{
|
|
.str = "$scrypt$v=1$ln=15,r=8,p=1$c2FsdHNhbHQ",
|
|
.HashResult = struct {
|
|
alg_id: []const u8,
|
|
alg_version: u16,
|
|
ln: u6,
|
|
r: u30,
|
|
p: u30,
|
|
salt: BinValue(16),
|
|
},
|
|
},
|
|
.{
|
|
.str = "$scrypt$v=1$ln=15,r=8,p=1",
|
|
.HashResult = struct { alg_id: []const u8, alg_version: ?u30, ln: u6, r: u30, p: u30 },
|
|
},
|
|
.{
|
|
.str = "$scrypt$v=1$c2FsdHNhbHQ$dGVzdHBhc3M",
|
|
.HashResult = struct {
|
|
alg_id: []const u8,
|
|
alg_version: u16,
|
|
salt: BinValue(16),
|
|
hash: BinValue(16),
|
|
},
|
|
},
|
|
.{
|
|
.str = "$scrypt$v=1$c2FsdHNhbHQ",
|
|
.HashResult = struct { alg_id: []const u8, alg_version: u16, salt: BinValue(16) },
|
|
},
|
|
.{
|
|
.str = "$scrypt$c2FsdHNhbHQ$dGVzdHBhc3M",
|
|
.HashResult = struct { alg_id: []const u8, salt: BinValue(16), hash: BinValue(16) },
|
|
},
|
|
};
|
|
inline for (inputs) |input| {
|
|
const v = try deserialize(input.HashResult, input.str);
|
|
var buf: [input.str.len]u8 = undefined;
|
|
const s1 = try serialize(v, &buf);
|
|
try std.testing.expectEqualSlices(u8, input.str, s1);
|
|
}
|
|
}
|
|
|
|
test "phc format - empty input string" {
|
|
const s = "";
|
|
const v = deserialize(struct { alg_id: []const u8 }, s);
|
|
try std.testing.expectError(Error.InvalidEncoding, v);
|
|
}
|
|
|
|
test "phc format - hash without salt" {
|
|
const s = "$scrypt";
|
|
const v = deserialize(struct { alg_id: []const u8, hash: BinValue(16) }, s);
|
|
try std.testing.expectError(Error.InvalidEncoding, v);
|
|
}
|
|
|
|
test "phc format - calcSize" {
|
|
const s = "$scrypt$v=1$ln=15,r=8,p=1$c2FsdHNhbHQ$dGVzdHBhc3M";
|
|
const v = try deserialize(struct {
|
|
alg_id: []const u8,
|
|
alg_version: u16,
|
|
ln: u6,
|
|
r: u30,
|
|
p: u30,
|
|
salt: BinValue(8),
|
|
hash: BinValue(8),
|
|
}, s);
|
|
try std.testing.expectEqual(calcSize(v), s.len);
|
|
}
|