mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
## Summary
- Fix use-after-free bug when parsing `ca` option from `.npmrc`
- The `ca` string was being stored directly from the parser's arena
without duplication
- Since the parser arena is freed at the end of `loadNpmrc`, this
created a dangling pointer
## The Bug
In `src/ini.zig`, the `ca` string wasn't being duplicated like all other
string properties:
```zig
// Lines 983-986 explicitly warn about this:
// Need to be very, very careful here with strings.
// They are allocated in the Parser's arena, which of course gets
// deinitialized at the end of the scope.
// We need to dupe all strings
// Line 981: Parser arena is freed here
defer parser.deinit();
// Line 1016-1020: THE BUG - string not duped!
if (out.asProperty("ca")) |query| {
if (query.expr.asUtf8StringLiteral()) |str| {
install.ca = .{
.str = str, // ← Dangling pointer after parser.deinit()!
};
```
All other string properties in the same function correctly duplicate:
- `registry` (line 996): `try allocator.dupe(u8, str)`
- `cache` (line 1002): `try allocator.dupe(u8, str)`
- `cafile` (line 1037): `asStringCloned(allocator)`
- `ca` array items (line 1026): `asStringCloned(allocator)`
## User Impact
When a user has `ca=<certificate>` in their `.npmrc` file:
1. The certificate string is parsed and stored
2. The parser arena is freed
3. `install.ca.str` becomes a dangling pointer
4. Later TLS/SSL operations access freed memory
5. Could cause crashes, undefined behavior, or security issues
## Test plan
- Code inspection confirms this matches the pattern used for all other
string properties
- The fix adds `try allocator.dupe(u8, str)` to match `cache`,
`registry`, etc.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
1458 lines
56 KiB
Zig
1458 lines
56 KiB
Zig
pub const Parser = struct {
|
|
opts: Options = .{},
|
|
source: bun.logger.Source,
|
|
src: []const u8,
|
|
out: Expr,
|
|
logger: bun.logger.Log,
|
|
arena: std.heap.ArenaAllocator,
|
|
env: *bun.DotEnv.Loader,
|
|
|
|
const Options = struct {
|
|
bracked_array: bool = true,
|
|
};
|
|
|
|
pub fn init(allocator: Allocator, path: []const u8, src: []const u8, env: *bun.DotEnv.Loader) Parser {
|
|
return .{
|
|
.logger = bun.logger.Log.init(allocator),
|
|
.src = src,
|
|
.out = Expr.init(E.Object, E.Object{}, Loc.Empty),
|
|
.source = bun.logger.Source.initPathString(path, src),
|
|
.arena = std.heap.ArenaAllocator.init(allocator),
|
|
.env = env,
|
|
};
|
|
}
|
|
|
|
pub fn deinit(this: *Parser) void {
|
|
this.logger.deinit();
|
|
this.arena.deinit();
|
|
}
|
|
|
|
inline fn shouldSkipLine(line: []const u8) bool {
|
|
if (line.len == 0 or
|
|
// comments
|
|
line[0] == ';' or
|
|
line[0] == '#') return true;
|
|
|
|
// check the rest is whitespace
|
|
for (line) |c| {
|
|
switch (c) {
|
|
' ', '\t', '\n', '\r' => {},
|
|
'#', ';' => return true,
|
|
else => return false,
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
fn parse(this: *Parser, arena_allocator: Allocator) OOM!void {
|
|
var iter = std.mem.splitScalar(u8, this.src, '\n');
|
|
var head: *E.Object = this.out.data.e_object;
|
|
|
|
// var duplicates = bun.StringArrayHashMapUnmanaged(u32){};
|
|
// defer duplicates.deinit(allocator);
|
|
|
|
var rope_stack = std.heap.stackFallback(@sizeOf(Rope) * 6, arena_allocator);
|
|
const ropealloc = rope_stack.get();
|
|
|
|
var skip_until_next_section: bool = false;
|
|
|
|
while (iter.next()) |line_| {
|
|
const line = if (line_.len > 0 and line_[line_.len - 1] == '\r') line_[0 .. line_.len - 1] else line_;
|
|
if (shouldSkipLine(line)) continue;
|
|
|
|
// Section
|
|
// [foo]
|
|
if (line[0] == '[') treat_as_key: {
|
|
skip_until_next_section = false;
|
|
const close_bracket_idx = std.mem.indexOfScalar(u8, line[0..], ']') orelse continue;
|
|
// Make sure the rest is just whitespace
|
|
if (close_bracket_idx + 1 < line.len) {
|
|
for (line[close_bracket_idx + 1 ..]) |c| if (switch (c) {
|
|
' ', '\t' => false,
|
|
else => true,
|
|
}) break :treat_as_key;
|
|
}
|
|
const section: *Rope = try this.prepareStr(arena_allocator, ropealloc, line[1..close_bracket_idx], @as(i32, @intCast(@intFromPtr(line.ptr) - @intFromPtr(this.src.ptr))) + 1, .section);
|
|
defer rope_stack.fixed_buffer_allocator.reset();
|
|
const parent_object = this.out.data.e_object.getOrPutObject(section, arena_allocator) catch |e| switch (e) {
|
|
error.OutOfMemory => |oom| return oom,
|
|
error.Clobber => {
|
|
// We're in here if key exists but it is not an object
|
|
//
|
|
// This is possible if someone did:
|
|
//
|
|
// ```ini
|
|
// foo = 'bar'
|
|
//
|
|
// [foo]
|
|
// hello = 420
|
|
// ```
|
|
//
|
|
// In the above case, `this.out[section]` would be a string.
|
|
// So what should we do in that case?
|
|
//
|
|
// npm/ini's will chug along happily trying to assign keys to the string.
|
|
//
|
|
// In JS assigning keys to string does nothing.
|
|
//
|
|
// Technically, this would have an effect if the value was an array:
|
|
//
|
|
// ```ini
|
|
// foo[] = 0
|
|
// foo[] = 1
|
|
//
|
|
// [foo]
|
|
// 0 = 420
|
|
// ```
|
|
//
|
|
// This would result in `foo` being `[420, 1]`.
|
|
//
|
|
// To be honest this is kind of crazy behavior so we're just going to skip this for now.
|
|
skip_until_next_section = true;
|
|
continue;
|
|
},
|
|
};
|
|
head = parent_object.data.e_object;
|
|
continue;
|
|
}
|
|
if (skip_until_next_section) continue;
|
|
|
|
// Otherwise it's a key val here
|
|
|
|
const line_offset: i32 = @intCast(@intFromPtr(line.ptr) - @intFromPtr(this.src.ptr));
|
|
|
|
const maybe_eq_sign_idx = std.mem.indexOfScalar(u8, line, '=');
|
|
|
|
const key_raw: []const u8 = try this.prepareStr(arena_allocator, ropealloc, line[0 .. maybe_eq_sign_idx orelse line.len], line_offset, .key);
|
|
const is_array: bool = brk: {
|
|
break :brk key_raw.len > 2 and bun.strings.endsWith(key_raw, "[]");
|
|
// Commenting out because options are not supported but we might
|
|
// support them.
|
|
// if (this.opts.bracked_array) {
|
|
// break :brk key_raw.len > 2 and bun.strings.endsWith(key_raw, "[]");
|
|
// } else {
|
|
// // const gop = try duplicates.getOrPut(allocator, key_raw);
|
|
// // if (gop.found_existing) {
|
|
// // gop.value_ptr.* = 1;
|
|
// // } else gop.value_ptr.* += 1;
|
|
// // break :brk gop.value_ptr.* > 1;
|
|
// @panic("We don't support this right now");
|
|
// }
|
|
};
|
|
|
|
const key = if (is_array and bun.strings.endsWith(key_raw, "[]"))
|
|
key_raw[0 .. key_raw.len - 2]
|
|
else
|
|
key_raw;
|
|
|
|
if (bun.strings.eqlComptime(key, "__proto__")) continue;
|
|
|
|
const value_raw: Expr = brk: {
|
|
if (maybe_eq_sign_idx) |eq_sign_idx| {
|
|
if (eq_sign_idx + 1 < line.len) break :brk try this.prepareStr(
|
|
arena_allocator,
|
|
ropealloc,
|
|
line[eq_sign_idx + 1 ..],
|
|
@intCast(line_offset + @as(i32, @intCast(eq_sign_idx)) + 1),
|
|
.value,
|
|
);
|
|
break :brk Expr.init(E.String, E.String{ .data = "" }, Loc.Empty);
|
|
}
|
|
break :brk Expr.init(E.Boolean, E.Boolean{ .value = true }, Loc.Empty);
|
|
};
|
|
|
|
const value: Expr = switch (value_raw.data) {
|
|
.e_string => |s| if (bun.strings.eqlComptime(s.data, "true"))
|
|
Expr.init(E.Boolean, E.Boolean{ .value = true }, Loc.Empty)
|
|
else if (bun.strings.eqlComptime(s.data, "false"))
|
|
Expr.init(E.Boolean, E.Boolean{ .value = false }, Loc.Empty)
|
|
else if (bun.strings.eqlComptime(s.data, "null"))
|
|
Expr.init(E.Null, E.Null{}, Loc.Empty)
|
|
else
|
|
value_raw,
|
|
else => value_raw,
|
|
};
|
|
|
|
if (is_array) {
|
|
if (head.get(key)) |val| {
|
|
if (val.data != .e_array) {
|
|
var arr = E.Array{};
|
|
try arr.push(arena_allocator, val);
|
|
try head.put(arena_allocator, key, Expr.init(E.Array, arr, Loc.Empty));
|
|
}
|
|
} else {
|
|
try head.put(arena_allocator, key, Expr.init(E.Array, E.Array{}, Loc.Empty));
|
|
}
|
|
}
|
|
|
|
// safeguard against resetting a previously defined
|
|
// array by accidentally forgetting the brackets
|
|
var was_already_array = false;
|
|
if (head.get(key)) |val| {
|
|
if (val.data == .e_array) {
|
|
was_already_array = true;
|
|
try val.data.e_array.push(arena_allocator, value);
|
|
try head.put(arena_allocator, key, val);
|
|
}
|
|
}
|
|
if (!was_already_array) {
|
|
try head.put(arena_allocator, key, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn prepareStr(
|
|
this: *Parser,
|
|
arena_allocator: Allocator,
|
|
ropealloc: Allocator,
|
|
val_: []const u8,
|
|
offset_: i32,
|
|
comptime usage: enum { section, key, value },
|
|
) OOM!switch (usage) {
|
|
.value => Expr,
|
|
.section => *Rope,
|
|
.key => []const u8,
|
|
} {
|
|
var offset = offset_;
|
|
var val = std.mem.trim(u8, val_, " \n\r\t");
|
|
|
|
if (isQuoted(val)) out: {
|
|
// remove single quotes before calling JSON.parse
|
|
if (val.len > 0 and val[0] == '\'') {
|
|
val = if (val.len > 1) val[1 .. val.len - 1] else val[1..];
|
|
offset += 1;
|
|
}
|
|
const src = bun.logger.Source.initPathString(this.source.path.text, val);
|
|
var log = bun.logger.Log.init(arena_allocator);
|
|
defer log.deinit();
|
|
// Try to parse it and if it fails will just treat it as a string
|
|
const json_val: Expr = bun.json.parseUTF8Impl(&src, &log, arena_allocator, true) catch {
|
|
// JSON parse failed (e.g., single-quoted string like '${VAR}')
|
|
// Still need to expand env vars in the content
|
|
if (comptime usage == .value) {
|
|
const expanded = try this.expandEnvVars(arena_allocator, val);
|
|
return Expr.init(E.String, E.String.init(expanded), Loc{ .start = @intCast(offset) });
|
|
}
|
|
break :out;
|
|
};
|
|
|
|
if (json_val.asString(arena_allocator)) |str| {
|
|
// Expand env vars in the JSON-parsed string
|
|
const expanded = if (comptime usage == .value) try this.expandEnvVars(arena_allocator, str) else str;
|
|
if (comptime usage == .value) return Expr.init(E.String, E.String.init(expanded), Loc{ .start = @intCast(offset) });
|
|
if (comptime usage == .section) return strToRope(ropealloc, expanded);
|
|
return expanded;
|
|
}
|
|
|
|
if (comptime usage == .value) return json_val;
|
|
|
|
// unfortunately, we need to match npm/ini behavior here,
|
|
// which requires us to turn these into a string,
|
|
// same behavior as doing this:
|
|
// ```
|
|
// let foo = {}
|
|
// const json_val = { hi: 'hello' }
|
|
// foo[json_val] = 'nice'
|
|
// ```
|
|
switch (json_val.data) {
|
|
.e_object => {
|
|
if (comptime usage == .section) return singleStrRope(ropealloc, "[Object object]");
|
|
return "[Object object]";
|
|
},
|
|
else => {
|
|
const str = try std.fmt.allocPrint(arena_allocator, "{f}", .{ToStringFormatter{ .d = json_val.data }});
|
|
if (comptime usage == .section) return singleStrRope(ropealloc, str);
|
|
return str;
|
|
},
|
|
}
|
|
} else {
|
|
const STACK_BUF_SIZE = 1024;
|
|
// walk the val to find the first non-escaped comment character (; or #)
|
|
var did_any_escape: bool = false;
|
|
var esc = false;
|
|
var sfb = std.heap.stackFallback(STACK_BUF_SIZE, arena_allocator);
|
|
var unesc = try std.array_list.Managed(u8).initCapacity(sfb.get(), STACK_BUF_SIZE);
|
|
|
|
const RopeT = if (comptime usage == .section) *Rope else struct {};
|
|
var rope: ?RopeT = if (comptime usage == .section) null else undefined;
|
|
|
|
var i: usize = 0;
|
|
while (i < val.len) : (i += 1) {
|
|
const c = val[i];
|
|
if (esc) {
|
|
switch (c) {
|
|
'\\' => try unesc.appendSlice(&[_]u8{'\\'}),
|
|
';', '#', '$' => try unesc.append(c),
|
|
'.' => {
|
|
if (comptime usage == .section) {
|
|
try unesc.append('.');
|
|
} else {
|
|
try unesc.appendSlice("\\.");
|
|
}
|
|
},
|
|
else => {
|
|
try unesc.appendSlice(switch (bun.strings.utf8ByteSequenceLength(c)) {
|
|
1 => brk: {
|
|
break :brk &[_]u8{ '\\', c };
|
|
},
|
|
2 => brk: {
|
|
defer i += 1;
|
|
break :brk &[_]u8{ '\\', c, val[i + 1] };
|
|
},
|
|
3 => brk: {
|
|
defer i += 2;
|
|
break :brk &[_]u8{ '\\', c, val[i + 1], val[i + 2] };
|
|
},
|
|
4 => brk: {
|
|
defer i += 3;
|
|
break :brk &[_]u8{ '\\', c, val[i + 1], val[i + 2], val[i + 3] };
|
|
},
|
|
// this means invalid utf8
|
|
else => unreachable,
|
|
});
|
|
},
|
|
}
|
|
|
|
esc = false;
|
|
} else switch (c) {
|
|
'$' => {
|
|
not_env_substitution: {
|
|
if (comptime usage != .value) break :not_env_substitution;
|
|
|
|
if (try this.parseEnvSubstitution(val, i, i, &unesc)) |new_i| {
|
|
// set to true so we heap alloc
|
|
did_any_escape = true;
|
|
i = new_i;
|
|
continue;
|
|
}
|
|
|
|
break :not_env_substitution;
|
|
}
|
|
try unesc.append('$');
|
|
},
|
|
';', '#' => break,
|
|
'\\' => {
|
|
esc = true;
|
|
did_any_escape = true;
|
|
},
|
|
'.' => {
|
|
if (comptime usage == .section) {
|
|
try this.commitRopePart(arena_allocator, ropealloc, &unesc, &rope);
|
|
} else {
|
|
try unesc.append('.');
|
|
}
|
|
},
|
|
else => try unesc.appendSlice(switch (bun.strings.utf8ByteSequenceLength(c)) {
|
|
1 => brk: {
|
|
break :brk &[_]u8{c};
|
|
},
|
|
2 => brk: {
|
|
defer i += 1;
|
|
break :brk &[_]u8{ c, val[i + 1] };
|
|
},
|
|
3 => brk: {
|
|
defer i += 2;
|
|
break :brk &[_]u8{ c, val[i + 1], val[i + 2] };
|
|
},
|
|
4 => brk: {
|
|
defer i += 3;
|
|
break :brk &[_]u8{ c, val[i + 1], val[i + 2], val[i + 3] };
|
|
},
|
|
// this means invalid utf8
|
|
else => unreachable,
|
|
}),
|
|
}
|
|
}
|
|
|
|
if (esc)
|
|
try unesc.append('\\');
|
|
|
|
switch (usage) {
|
|
.section => {
|
|
try this.commitRopePart(arena_allocator, ropealloc, &unesc, &rope);
|
|
return rope.?;
|
|
},
|
|
.value => {
|
|
if (!did_any_escape) return Expr.init(E.String, E.String.init(val[0..]), Loc{ .start = offset });
|
|
if (unesc.items.len <= STACK_BUF_SIZE) return Expr.init(
|
|
E.String,
|
|
E.String.init(try arena_allocator.dupe(u8, unesc.items[0..])),
|
|
Loc{ .start = offset },
|
|
);
|
|
return Expr.init(E.String, E.String.init(unesc.items[0..]), Loc{ .start = offset });
|
|
},
|
|
.key => {
|
|
const thestr: []const u8 = thestr: {
|
|
if (!did_any_escape) break :thestr try arena_allocator.dupe(u8, val[0..]);
|
|
if (unesc.items.len <= STACK_BUF_SIZE) break :thestr try arena_allocator.dupe(u8, unesc.items[0..]);
|
|
break :thestr unesc.items[0..];
|
|
};
|
|
return thestr;
|
|
},
|
|
}
|
|
}
|
|
if (comptime usage == .value) return Expr.init(E.String, E.String.init(val[0..]), Loc{ .start = offset });
|
|
if (comptime usage == .key) return val[0..];
|
|
return strToRope(ropealloc, val[0..]);
|
|
}
|
|
|
|
/// Expands ${VAR} and ${VAR?} environment variable substitutions in a string.
|
|
/// Used for quoted values after JSON parsing has already handled escape sequences.
|
|
///
|
|
/// Behavior (same as unquoted):
|
|
/// - ${VAR} - if VAR is undefined, leave as "${VAR}" (no expansion)
|
|
/// - ${VAR?} - if VAR is undefined, expand to empty string
|
|
/// - Backslash escaping is already handled by JSON parsing
|
|
fn expandEnvVars(this: *Parser, allocator: Allocator, val: []const u8) OOM![]const u8 {
|
|
// Quick check if there are any env vars to expand
|
|
if (std.mem.indexOf(u8, val, "${") == null) {
|
|
return val;
|
|
}
|
|
|
|
var result = try std.array_list.Managed(u8).initCapacity(allocator, val.len);
|
|
var i: usize = 0;
|
|
while (i < val.len) {
|
|
if (val[i] == '$' and i + 2 < val.len and val[i + 1] == '{') {
|
|
// Find the closing brace
|
|
var j = i + 2;
|
|
var depth: usize = 1;
|
|
while (j < val.len and depth > 0) {
|
|
if (val[j] == '{') {
|
|
depth += 1;
|
|
} else if (val[j] == '}') {
|
|
depth -= 1;
|
|
}
|
|
if (depth > 0) j += 1;
|
|
}
|
|
if (depth == 0) {
|
|
const env_var_raw = val[i + 2 .. j];
|
|
const optional = env_var_raw.len > 0 and env_var_raw[env_var_raw.len - 1] == '?';
|
|
const env_var = if (optional) env_var_raw[0 .. env_var_raw.len - 1] else env_var_raw;
|
|
|
|
if (this.env.get(env_var)) |expanded| {
|
|
try result.appendSlice(expanded);
|
|
} else if (!optional) {
|
|
// Not found and not optional: leave as-is
|
|
try result.appendSlice(val[i .. j + 1]);
|
|
}
|
|
// If optional and not found: expand to empty string (append nothing)
|
|
i = j + 1;
|
|
continue;
|
|
}
|
|
}
|
|
try result.append(val[i]);
|
|
i += 1;
|
|
}
|
|
return result.items;
|
|
}
|
|
|
|
/// Returns index to skip or null if not an env substitution
|
|
/// Invariants:
|
|
/// - `i` must be an index into `val` that points to a '$' char
|
|
///
|
|
/// npm/ini uses a regex pattern that will select the inner most ${...}
|
|
/// Supports ${VAR} and ${VAR?} syntax:
|
|
/// - ${VAR} - if undefined, returns null (leaves as-is)
|
|
/// - ${VAR?} - if undefined, expands to empty string
|
|
fn parseEnvSubstitution(this: *Parser, val: []const u8, start: usize, i: usize, unesc: *std.array_list.Managed(u8)) OOM!?usize {
|
|
bun.debugAssert(val[i] == '$');
|
|
var esc = false;
|
|
if (i + "{}".len < val.len and val[i + 1] == '{') {
|
|
var found_closing = false;
|
|
var j = i + 2;
|
|
while (j < val.len) : (j += 1) {
|
|
switch (val[j]) {
|
|
'\\' => esc = !esc,
|
|
'$' => if (!esc) return this.parseEnvSubstitution(val, start, j, unesc),
|
|
'{' => if (!esc) return null,
|
|
'}' => if (!esc) {
|
|
found_closing = true;
|
|
break;
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
if (!found_closing) return null;
|
|
|
|
if (start != i) {
|
|
const missed = val[start..i];
|
|
try unesc.appendSlice(missed);
|
|
}
|
|
|
|
const env_var_raw = val[i + 2 .. j];
|
|
const optional = env_var_raw.len > 0 and env_var_raw[env_var_raw.len - 1] == '?';
|
|
const env_var = if (optional) env_var_raw[0 .. env_var_raw.len - 1] else env_var_raw;
|
|
|
|
// https://github.com/npm/cli/blob/534ad7789e5c61f579f44d782bdd18ea3ff1ee20/workspaces/config/lib/env-replace.js#L6
|
|
if (this.env.get(env_var)) |expanded| {
|
|
try unesc.appendSlice(expanded);
|
|
} else if (!optional) {
|
|
// Not found and not optional: return null to leave as-is
|
|
return null;
|
|
}
|
|
// If optional and not found: expand to empty string (append nothing)
|
|
|
|
return j;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
fn singleStrRope(ropealloc: Allocator, str: []const u8) OOM!*Rope {
|
|
const rope = try ropealloc.create(Rope);
|
|
rope.* = .{
|
|
.head = Expr.init(E.String, E.String.init(str), Loc.Empty),
|
|
};
|
|
return rope;
|
|
}
|
|
|
|
fn nextDot(key: []const u8) ?usize {
|
|
return std.mem.indexOfScalar(u8, key, '.');
|
|
}
|
|
|
|
fn commitRopePart(this: *Parser, arena_allocator: Allocator, ropealloc: Allocator, unesc: *std.array_list.Managed(u8), existing_rope: *?*Rope) OOM!void {
|
|
_ = this; // autofix
|
|
const slice = try arena_allocator.dupe(u8, unesc.items[0..]);
|
|
const expr = Expr.init(E.String, E.String{ .data = slice }, Loc.Empty);
|
|
if (existing_rope.*) |_r| {
|
|
const r: *Rope = _r;
|
|
_ = try r.append(expr, ropealloc);
|
|
} else {
|
|
existing_rope.* = try ropealloc.create(Rope);
|
|
existing_rope.*.?.* = Rope{
|
|
.head = expr,
|
|
};
|
|
}
|
|
unesc.clearRetainingCapacity();
|
|
}
|
|
|
|
fn strToRope(ropealloc: Allocator, key: []const u8) OOM!*Rope {
|
|
var dot_idx = nextDot(key) orelse {
|
|
const rope = try ropealloc.create(Rope);
|
|
rope.* = .{
|
|
.head = Expr.init(E.String, E.String.init(key), Loc.Empty),
|
|
};
|
|
return rope;
|
|
};
|
|
var rope = try ropealloc.create(Rope);
|
|
const head = rope;
|
|
rope.* = .{
|
|
.head = Expr.init(E.String, E.String.init(key[0..dot_idx]), Loc.Empty),
|
|
.next = null,
|
|
};
|
|
|
|
while (dot_idx + 1 < key.len) {
|
|
const next_dot_idx = dot_idx + 1 + (nextDot(key[dot_idx + 1 ..]) orelse {
|
|
const rest = key[dot_idx + 1 ..];
|
|
rope = try rope.append(Expr.init(E.String, E.String.init(rest), Loc.Empty), ropealloc);
|
|
break;
|
|
});
|
|
const part = key[dot_idx + 1 .. next_dot_idx];
|
|
rope = try rope.append(Expr.init(E.String, E.String.init(part), Loc.Empty), ropealloc);
|
|
dot_idx = next_dot_idx;
|
|
}
|
|
|
|
return head;
|
|
}
|
|
|
|
fn isQuoted(val: []const u8) bool {
|
|
return (bun.strings.startsWithChar(val, '"') and bun.strings.endsWithChar(val, '"')) or
|
|
(bun.strings.startsWithChar(val, '\'') and bun.strings.endsWithChar(val, '\''));
|
|
}
|
|
};
|
|
|
|
/// Used in JS tests, see `internal-for-testing.ts` and shell tests.
|
|
pub const IniTestingAPIs = struct {
|
|
const jsc = bun.jsc;
|
|
|
|
pub fn loadNpmrcFromJS(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
|
|
const arg = callframe.argument(0);
|
|
const npmrc_contents = try arg.toBunString(globalThis);
|
|
defer npmrc_contents.deref();
|
|
const npmrc_utf8 = npmrc_contents.toUTF8(bun.default_allocator);
|
|
defer npmrc_utf8.deinit();
|
|
const source = &bun.logger.Source.initPathString("<js>", npmrc_utf8.slice());
|
|
|
|
var log = bun.logger.Log.init(bun.default_allocator);
|
|
defer log.deinit();
|
|
|
|
var arena = bun.ArenaAllocator.init(bun.default_allocator);
|
|
const allocator = arena.allocator();
|
|
defer arena.deinit();
|
|
|
|
const envjs = callframe.argument(1);
|
|
const env = if (envjs.isEmptyOrUndefinedOrNull()) globalThis.bunVM().transpiler.env else brk: {
|
|
var envmap = bun.DotEnv.Map.HashTable.init(allocator);
|
|
const envobj = envjs.getObject() orelse return globalThis.throwTypeError("env must be an object", .{});
|
|
var object_iter = try jsc.JSPropertyIterator(.{
|
|
.skip_empty_name = false,
|
|
.include_value = true,
|
|
}).init(globalThis, envobj);
|
|
defer object_iter.deinit();
|
|
|
|
try envmap.ensureTotalCapacity(object_iter.len);
|
|
|
|
while (try object_iter.next()) |key| {
|
|
const keyslice = try key.toOwnedSlice(allocator);
|
|
var value = object_iter.value;
|
|
if (value.isUndefined()) continue;
|
|
|
|
const value_str = try value.getZigString(globalThis);
|
|
const slice = try value_str.toOwnedSlice(allocator);
|
|
|
|
envmap.put(keyslice, .{
|
|
.value = slice,
|
|
.conditional = false,
|
|
}) catch return globalThis.throwOutOfMemoryValue();
|
|
}
|
|
|
|
const map = try allocator.create(bun.DotEnv.Map);
|
|
map.* = .{
|
|
.map = envmap,
|
|
};
|
|
|
|
const env = bun.DotEnv.Loader.init(map, allocator);
|
|
const envstable = try allocator.create(bun.DotEnv.Loader);
|
|
envstable.* = env;
|
|
break :brk envstable;
|
|
};
|
|
|
|
const install = try allocator.create(bun.schema.api.BunInstall);
|
|
install.* = std.mem.zeroes(bun.schema.api.BunInstall);
|
|
var configs = std.array_list.Managed(ConfigIterator.Item).init(allocator);
|
|
defer configs.deinit();
|
|
loadNpmrc(allocator, install, env, ".npmrc", &log, source, &configs) catch {
|
|
return log.toJS(globalThis, allocator, "error");
|
|
};
|
|
|
|
const default_registry_url, const default_registry_token, const default_registry_username, const default_registry_password, const default_registry_email = brk: {
|
|
const default_registry = install.default_registry orelse break :brk .{
|
|
bun.String.static(Registry.default_url[0..]),
|
|
bun.String.empty,
|
|
bun.String.empty,
|
|
bun.String.empty,
|
|
bun.String.empty,
|
|
};
|
|
|
|
break :brk .{
|
|
bun.String.fromBytes(default_registry.url),
|
|
bun.String.fromBytes(default_registry.token),
|
|
bun.String.fromBytes(default_registry.username),
|
|
bun.String.fromBytes(default_registry.password),
|
|
bun.String.fromBytes(default_registry.email),
|
|
};
|
|
};
|
|
defer {
|
|
default_registry_url.deref();
|
|
default_registry_token.deref();
|
|
default_registry_username.deref();
|
|
default_registry_password.deref();
|
|
default_registry_email.deref();
|
|
}
|
|
|
|
return (try jsc.JSObject.create(.{
|
|
.default_registry_url = default_registry_url,
|
|
.default_registry_token = default_registry_token,
|
|
.default_registry_username = default_registry_username,
|
|
.default_registry_password = default_registry_password,
|
|
.default_registry_email = default_registry_email,
|
|
}, globalThis)).toJS();
|
|
}
|
|
|
|
pub fn parse(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
|
|
const arguments_ = callframe.arguments_old(1);
|
|
const arguments = arguments_.slice();
|
|
|
|
const jsstr = arguments[0];
|
|
const bunstr = try jsstr.toBunString(globalThis);
|
|
defer bunstr.deref();
|
|
const utf8str = bunstr.toUTF8(bun.default_allocator);
|
|
defer utf8str.deinit();
|
|
|
|
var parser = Parser.init(bun.default_allocator, "<src>", utf8str.slice(), globalThis.bunVM().transpiler.env);
|
|
defer parser.deinit();
|
|
|
|
try parser.parse(parser.arena.allocator());
|
|
|
|
return parser.out.toJS(bun.default_allocator, globalThis) catch |e| {
|
|
return globalThis.throwError(e, "failed to turn AST into JS");
|
|
};
|
|
}
|
|
};
|
|
|
|
pub const ToStringFormatter = struct {
|
|
d: js_ast.Expr.Data,
|
|
|
|
pub fn format(this: *const @This(), writer: *std.Io.Writer) !void {
|
|
switch (this.d) {
|
|
.e_array => {
|
|
const last = this.d.e_array.items.len -| 1;
|
|
for (this.d.e_array.items.slice(), 0..) |*e, i| {
|
|
const is_last = i == last;
|
|
try writer.print("{f}{s}", .{ ToStringFormatter{ .d = e.data }, if (is_last) "" else "," });
|
|
}
|
|
},
|
|
.e_object => try writer.print("[Object object]", .{}),
|
|
.e_boolean => try writer.print("{s}", .{if (this.d.e_boolean.value) "true" else "false"}),
|
|
.e_number => try writer.print("{d}", .{this.d.e_number.value}),
|
|
.e_string => try writer.print("{s}", .{this.d.e_string.data}),
|
|
.e_null => try writer.print("null", .{}),
|
|
|
|
else => |tag| if (bun.Environment.isDebug) {
|
|
Output.panic("Unexpected AST node: {s}", .{@tagName(tag)});
|
|
},
|
|
}
|
|
}
|
|
};
|
|
|
|
pub fn Option(comptime T: type) type {
|
|
return union(enum) {
|
|
some: T,
|
|
none,
|
|
|
|
pub fn get(this: @This()) ?T {
|
|
return switch (this) {
|
|
.some => this.some,
|
|
.none => null,
|
|
};
|
|
}
|
|
};
|
|
}
|
|
|
|
pub const ConfigIterator = struct {
|
|
allocator: Allocator,
|
|
config: *E.Object,
|
|
source: *const bun.logger.Source,
|
|
log: *bun.logger.Log,
|
|
|
|
prop_idx: usize = 0,
|
|
|
|
pub const Item = struct {
|
|
registry_url: []const u8,
|
|
optname: Opt,
|
|
value: []const u8,
|
|
loc: Loc,
|
|
|
|
pub const Opt = enum {
|
|
/// `${username}:${password}` encoded in base64
|
|
_auth,
|
|
|
|
/// authentication string
|
|
_authToken,
|
|
|
|
username,
|
|
|
|
/// this is encoded as base64 in .npmrc
|
|
_password,
|
|
|
|
email,
|
|
|
|
/// path to certificate file
|
|
certfile,
|
|
|
|
/// path to key file
|
|
keyfile,
|
|
|
|
pub fn isBase64Encoded(this: Opt) bool {
|
|
return switch (this) {
|
|
._auth, ._password => true,
|
|
else => false,
|
|
};
|
|
}
|
|
};
|
|
|
|
/// Duplicate ConfigIterator.Item
|
|
pub fn dupe(this: *const Item, allocator: Allocator) OOM!?Item {
|
|
return .{
|
|
.registry_url = try allocator.dupe(u8, this.registry_url),
|
|
.optname = this.optname,
|
|
.value = try allocator.dupe(u8, this.value),
|
|
.loc = this.loc,
|
|
};
|
|
}
|
|
|
|
/// Duplicate the value, decoding it if it is base64 encoded.
|
|
pub fn dupeValueDecoded(
|
|
this: *const Item,
|
|
allocator: Allocator,
|
|
log: *bun.logger.Log,
|
|
source: *const bun.logger.Source,
|
|
) OOM!?[]const u8 {
|
|
if (this.optname.isBase64Encoded()) {
|
|
if (this.value.len == 0) return "";
|
|
const len = bun.base64.decodeLen(this.value);
|
|
var slice = try allocator.alloc(u8, len);
|
|
const result = bun.base64.decode(slice[0..], this.value);
|
|
if (result.status != .success) {
|
|
try log.addErrorFmtOpts(
|
|
allocator,
|
|
"{s} is not valid base64",
|
|
.{@tagName(this.optname)},
|
|
.{
|
|
.source = source,
|
|
.loc = this.loc,
|
|
},
|
|
);
|
|
return null;
|
|
}
|
|
return try allocator.dupe(u8, slice[0..result.count]);
|
|
}
|
|
return try allocator.dupe(u8, this.value);
|
|
}
|
|
|
|
pub fn format(this: *const @This(), writer: *std.Io.Writer) !void {
|
|
try writer.print("//{s}:{s}={s}", .{ this.registry_url, @tagName(this.optname), this.value });
|
|
}
|
|
|
|
pub fn deinit(self: *Item, allocator: Allocator) void {
|
|
allocator.free(self.registry_url);
|
|
allocator.free(self.value);
|
|
}
|
|
};
|
|
|
|
pub fn next(this: *ConfigIterator) ?Option(Item) {
|
|
if (this.prop_idx >= this.config.properties.len) return null;
|
|
defer this.prop_idx += 1;
|
|
|
|
const prop = this.config.properties.ptr[this.prop_idx];
|
|
|
|
if (prop.key) |keyexpr| {
|
|
if (keyexpr.asUtf8StringLiteral()) |key| {
|
|
if (bun.strings.hasPrefixComptime(key, "//")) {
|
|
const optnames = comptime brk: {
|
|
const names = std.meta.fieldNames(Item.Opt);
|
|
var names2: [names.len][:0]const u8 = undefined;
|
|
// we need to make sure to reverse this
|
|
// because _auth could match when it actually had _authToken
|
|
// so go backwards since _authToken is last
|
|
for (0..names.len) |i| {
|
|
names2[names2.len - i - 1] = names[i];
|
|
}
|
|
break :brk names2;
|
|
};
|
|
|
|
inline for (optnames) |name| {
|
|
var buf: [name.len + 1]u8 = undefined;
|
|
buf[0] = ':';
|
|
@memcpy(buf[1 .. name.len + 1], name);
|
|
const name_with_eq = buf[0..];
|
|
|
|
if (std.mem.lastIndexOf(u8, key, name_with_eq)) |index| {
|
|
const url_part = key[2..index];
|
|
if (prop.value) |value_expr| {
|
|
if (value_expr.asUtf8StringLiteral()) |value| {
|
|
return .{
|
|
.some = Item{
|
|
.registry_url = url_part,
|
|
.value = value,
|
|
.optname = std.meta.stringToEnum(Item.Opt, name).?,
|
|
.loc = prop.key.?.loc,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return .none;
|
|
}
|
|
};
|
|
|
|
const NodeLinkerMap = bun.ComptimeStringMap(bun.install.PackageManager.Options.NodeLinker, .{
|
|
// yarn
|
|
.{ "pnpm", .isolated },
|
|
.{ "node-modules", .hoisted },
|
|
|
|
// pnpm
|
|
.{ "isolated", .isolated },
|
|
.{ "hoisted", .hoisted },
|
|
});
|
|
|
|
pub const ScopeIterator = struct {
|
|
allocator: Allocator,
|
|
config: *E.Object,
|
|
source: *const bun.logger.Source,
|
|
log: *bun.logger.Log,
|
|
|
|
prop_idx: usize = 0,
|
|
count: bool = false,
|
|
|
|
const Error = error{
|
|
no_value,
|
|
};
|
|
|
|
const Item = struct { scope: []const u8, registry: bun.schema.api.NpmRegistry };
|
|
|
|
pub fn next(this: *ScopeIterator) OOM!?Option(Item) {
|
|
if (this.prop_idx >= this.config.properties.len) return null;
|
|
defer this.prop_idx += 1;
|
|
|
|
const prop = this.config.properties.ptr[this.prop_idx];
|
|
|
|
if (prop.key) |keyexpr| {
|
|
if (keyexpr.asUtf8StringLiteral()) |key| {
|
|
if (bun.strings.hasPrefixComptime(key, "@") and bun.strings.endsWith(key, ":registry")) {
|
|
if (!this.count) {
|
|
return .{
|
|
.some = .{
|
|
.scope = key[1 .. key.len - ":registry".len],
|
|
.registry = brk: {
|
|
if (prop.value) |value| {
|
|
if (value.asUtf8StringLiteral()) |str| {
|
|
var parser = bun.schema.api.NpmRegistry.Parser{
|
|
.log = this.log,
|
|
.source = this.source,
|
|
.allocator = this.allocator,
|
|
};
|
|
break :brk try parser.parseRegistryURLStringImpl(str);
|
|
}
|
|
}
|
|
return .none;
|
|
},
|
|
},
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return .none;
|
|
}
|
|
};
|
|
|
|
pub fn loadNpmrcConfig(
|
|
allocator: std.mem.Allocator,
|
|
install: *bun.schema.api.BunInstall,
|
|
env: *bun.DotEnv.Loader,
|
|
auto_loaded: bool,
|
|
npmrc_paths: []const [:0]const u8,
|
|
) void {
|
|
var log = bun.logger.Log.init(allocator);
|
|
defer log.deinit();
|
|
|
|
// npmrc registry configurations are shared between all npmrc files
|
|
// so we need to collect them as we go for the final registry map
|
|
// to be created at the end.
|
|
var configs = std.array_list.Managed(ConfigIterator.Item).init(allocator);
|
|
defer {
|
|
for (configs.items) |*item| {
|
|
item.deinit(allocator);
|
|
}
|
|
configs.deinit();
|
|
}
|
|
|
|
for (npmrc_paths) |npmrc_path| {
|
|
const source = &(bun.sys.File.toSource(npmrc_path, allocator, .{ .convert_bom = true }).unwrap() catch |err| {
|
|
if (auto_loaded) continue;
|
|
Output.err(err, "failed to read .npmrc: \"{s}\"", .{npmrc_path});
|
|
Global.crash();
|
|
});
|
|
defer allocator.free(source.contents);
|
|
|
|
loadNpmrc(allocator, install, env, npmrc_path, &log, source, &configs) catch |err| {
|
|
switch (err) {
|
|
error.OutOfMemory => bun.outOfMemory(),
|
|
}
|
|
};
|
|
if (log.hasErrors()) {
|
|
if (log.errors == 1)
|
|
Output.warn("Encountered an error while reading <b>{s}<r>:\n\n", .{npmrc_path})
|
|
else
|
|
Output.warn("Encountered errors while reading <b>{s}<r>:\n\n", .{npmrc_path});
|
|
Output.flush();
|
|
}
|
|
log.print(Output.errorWriter()) catch {};
|
|
}
|
|
}
|
|
|
|
pub fn loadNpmrc(
|
|
allocator: std.mem.Allocator,
|
|
install: *bun.schema.api.BunInstall,
|
|
env: *bun.DotEnv.Loader,
|
|
npmrc_path: [:0]const u8,
|
|
log: *bun.logger.Log,
|
|
source: *const bun.logger.Source,
|
|
configs: *std.array_list.Managed(ConfigIterator.Item),
|
|
) OOM!void {
|
|
var parser = bun.ini.Parser.init(allocator, npmrc_path, source.contents, env);
|
|
defer parser.deinit();
|
|
try parser.parse(parser.arena.allocator());
|
|
// Need to be very, very careful here with strings.
|
|
// They are allocated in the Parser's arena, which of course gets
|
|
// deinitialized at the end of the scope.
|
|
// We need to dupe all strings
|
|
const out = parser.out;
|
|
|
|
if (out.asProperty("registry")) |query| {
|
|
if (query.expr.asUtf8StringLiteral()) |str| {
|
|
var p = bun.schema.api.NpmRegistry.Parser{
|
|
.allocator = allocator,
|
|
.log = log,
|
|
.source = source,
|
|
};
|
|
install.default_registry = try p.parseRegistryURLStringImpl(try allocator.dupe(u8, str));
|
|
}
|
|
}
|
|
|
|
if (out.asProperty("cache")) |query| {
|
|
if (query.expr.asUtf8StringLiteral()) |str| {
|
|
install.cache_directory = try allocator.dupe(u8, str);
|
|
} else if (query.expr.asBool()) |b| {
|
|
install.disable_cache = !b;
|
|
}
|
|
}
|
|
|
|
if (out.asProperty("dry-run")) |query| {
|
|
if (query.expr.asUtf8StringLiteral()) |str| {
|
|
install.dry_run = bun.strings.eqlComptime(str, "true");
|
|
} else if (query.expr.asBool()) |b| {
|
|
install.dry_run = b;
|
|
}
|
|
}
|
|
|
|
if (out.asProperty("ca")) |query| {
|
|
if (query.expr.asUtf8StringLiteral()) |str| {
|
|
install.ca = .{
|
|
.str = try allocator.dupe(u8, str),
|
|
};
|
|
} else if (query.expr.isArray()) {
|
|
const arr = query.expr.data.e_array;
|
|
var list = try allocator.alloc([]const u8, arr.items.len);
|
|
var i: usize = 0;
|
|
for (arr.items.slice()) |item| {
|
|
list[i] = try item.asStringCloned(allocator) orelse continue;
|
|
i += 1;
|
|
}
|
|
|
|
install.ca = .{
|
|
.list = list,
|
|
};
|
|
}
|
|
}
|
|
|
|
if (out.asProperty("cafile")) |query| {
|
|
if (try query.expr.asStringCloned(allocator)) |cafile| {
|
|
install.cafile = cafile;
|
|
}
|
|
}
|
|
|
|
if (out.asProperty("omit")) |omit| {
|
|
switch (omit.expr.data) {
|
|
.e_string => |str| {
|
|
if (str.eqlComptime("dev")) {
|
|
install.save_dev = false;
|
|
} else if (str.eqlComptime("peer")) {
|
|
install.save_peer = false;
|
|
} else if (str.eqlComptime("optional")) {
|
|
install.save_optional = false;
|
|
}
|
|
},
|
|
.e_array => |arr| {
|
|
for (arr.items.slice()) |item| {
|
|
switch (item.data) {
|
|
.e_string => |str| {
|
|
if (str.eqlComptime("dev")) {
|
|
install.save_dev = false;
|
|
} else if (str.eqlComptime("peer")) {
|
|
install.save_peer = false;
|
|
} else if (str.eqlComptime("optional")) {
|
|
install.save_optional = false;
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
if (out.asProperty("include")) |omit| {
|
|
switch (omit.expr.data) {
|
|
.e_string => |str| {
|
|
if (str.eqlComptime("dev")) {
|
|
install.save_dev = true;
|
|
} else if (str.eqlComptime("peer")) {
|
|
install.save_peer = true;
|
|
} else if (str.eqlComptime("optional")) {
|
|
install.save_optional = true;
|
|
}
|
|
},
|
|
.e_array => |arr| {
|
|
for (arr.items.slice()) |item| {
|
|
switch (item.data) {
|
|
.e_string => |str| {
|
|
if (str.eqlComptime("dev")) {
|
|
install.save_dev = true;
|
|
} else if (str.eqlComptime("peer")) {
|
|
install.save_peer = true;
|
|
} else if (str.eqlComptime("optional")) {
|
|
install.save_optional = true;
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
if (out.get("ignore-scripts")) |*ignore_scripts| {
|
|
if (ignore_scripts.asBool()) |ignore| {
|
|
install.ignore_scripts = ignore;
|
|
}
|
|
}
|
|
|
|
if (out.get("link-workspace-packages")) |*link_workspace_packages| {
|
|
if (link_workspace_packages.asBool()) |link| {
|
|
install.link_workspace_packages = link;
|
|
}
|
|
}
|
|
|
|
if (out.get("save-exact")) |*save_exact| {
|
|
if (save_exact.asBool()) |exact| {
|
|
install.exact = exact;
|
|
}
|
|
}
|
|
|
|
if (out.get("install-strategy")) |install_strategy_expr| {
|
|
if (install_strategy_expr.asString(allocator)) |install_strategy_str| {
|
|
if (bun.strings.eqlComptime(install_strategy_str, "hoisted")) {
|
|
install.node_linker = .hoisted;
|
|
} else if (bun.strings.eqlComptime(install_strategy_str, "linked")) {
|
|
install.node_linker = .isolated;
|
|
} else if (bun.strings.eqlComptime(install_strategy_str, "nested")) {
|
|
// TODO
|
|
} else if (bun.strings.eqlComptime(install_strategy_str, "shallow")) {
|
|
// TODO
|
|
}
|
|
}
|
|
}
|
|
|
|
// yarn & pnpm option
|
|
if (out.get("node-linker")) |node_linker_expr| {
|
|
if (node_linker_expr.asString(allocator)) |node_linker_str| {
|
|
if (NodeLinkerMap.get(node_linker_str)) |node_linker| {
|
|
install.node_linker = node_linker;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (out.get("public-hoist-pattern")) |public_hoist_pattern_expr| {
|
|
install.public_hoist_pattern = bun.install.PnpmMatcher.fromExpr(
|
|
allocator,
|
|
public_hoist_pattern_expr,
|
|
log,
|
|
source,
|
|
) catch |err| switch (err) {
|
|
error.OutOfMemory => |oom| return oom,
|
|
error.InvalidRegExp, error.UnexpectedExpr => patterns: {
|
|
log.reset();
|
|
break :patterns null;
|
|
},
|
|
};
|
|
}
|
|
|
|
if (out.get("hoist-pattern")) |hoist_pattern_expr| {
|
|
install.hoist_pattern = bun.install.PnpmMatcher.fromExpr(
|
|
allocator,
|
|
hoist_pattern_expr,
|
|
log,
|
|
source,
|
|
) catch |err| switch (err) {
|
|
error.OutOfMemory => |oom| return oom,
|
|
error.InvalidRegExp, error.UnexpectedExpr => patterns: {
|
|
log.reset();
|
|
break :patterns null;
|
|
},
|
|
};
|
|
}
|
|
|
|
var registry_map = install.scoped orelse bun.schema.api.NpmRegistryMap{};
|
|
|
|
// Process scopes
|
|
{
|
|
var iter = bun.ini.ScopeIterator{
|
|
.config = parser.out.data.e_object,
|
|
.count = true,
|
|
.source = source,
|
|
.log = log,
|
|
.allocator = allocator,
|
|
};
|
|
|
|
const scope_count = brk: {
|
|
var count: usize = 0;
|
|
while (try iter.next()) |o| {
|
|
if (o == .some) {
|
|
count += 1;
|
|
}
|
|
}
|
|
break :brk count;
|
|
};
|
|
|
|
defer install.scoped = registry_map;
|
|
try registry_map.scopes.ensureUnusedCapacity(allocator, scope_count);
|
|
|
|
iter.prop_idx = 0;
|
|
iter.count = false;
|
|
|
|
while (try iter.next()) |val| {
|
|
if (val.get()) |result| {
|
|
const registry = result.registry.dupe(allocator);
|
|
try registry_map.scopes.put(
|
|
allocator,
|
|
try allocator.dupe(u8, result.scope),
|
|
registry,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process registry configuration
|
|
out: {
|
|
const count = brk: {
|
|
var count: usize = configs.items.len;
|
|
for (parser.out.data.e_object.properties.slice()) |prop| {
|
|
if (prop.key) |keyexpr| {
|
|
if (keyexpr.asUtf8StringLiteral()) |key| {
|
|
if (bun.strings.hasPrefixComptime(key, "//")) {
|
|
count += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
break :brk count;
|
|
};
|
|
|
|
if (count == 0) break :out;
|
|
|
|
const default_registry_url: bun.URL = brk: {
|
|
if (install.default_registry) |dr|
|
|
break :brk bun.URL.parse(dr.url);
|
|
|
|
break :brk bun.URL.parse(Registry.default_url);
|
|
};
|
|
|
|
// I don't like having to do this but we'll need a mapping of scope -> bun.URL
|
|
// Because we need to check different parts of the URL, for instance in this
|
|
// example .npmrc:
|
|
_ =
|
|
\\ @myorg:registry=https://somewhere-else.com/myorg
|
|
\\ @another:registry=https://somewhere-else.com/another
|
|
\\
|
|
\\ //somewhere-else.com/myorg/:_authToken=MYTOKEN1
|
|
\\
|
|
\\ //somewhere-else.com/:username=foobar
|
|
\\
|
|
;
|
|
// The line that sets the auth token should only apply to the @myorg scope
|
|
// The line that sets the username would apply to both @myorg and @another
|
|
var url_map = url_map: {
|
|
var url_map = bun.StringArrayHashMap(bun.URL).init(parser.arena.allocator());
|
|
try url_map.ensureTotalCapacity(registry_map.scopes.keys().len);
|
|
|
|
for (registry_map.scopes.keys(), registry_map.scopes.values()) |*k, *v| {
|
|
const url = bun.URL.parse(v.url);
|
|
try url_map.put(k.*, url);
|
|
}
|
|
|
|
break :url_map url_map;
|
|
};
|
|
|
|
defer url_map.deinit();
|
|
|
|
var iter = bun.ini.ConfigIterator{
|
|
.config = parser.out.data.e_object,
|
|
.source = source,
|
|
.log = log,
|
|
.allocator = allocator,
|
|
};
|
|
|
|
while (iter.next()) |val| {
|
|
if (val.get()) |conf_item_| {
|
|
// `conf_item` will look like:
|
|
//
|
|
// - localhost:4873/
|
|
// - somewhere-else.com/myorg/
|
|
//
|
|
// Scoped registries are set like this:
|
|
// - @myorg:registry=https://somewhere-else.com/myorg
|
|
const conf_item: bun.ini.ConfigIterator.Item = conf_item_;
|
|
switch (conf_item.optname) {
|
|
.certfile, .keyfile => {
|
|
try log.addWarningFmt(
|
|
source,
|
|
iter.config.properties.at(iter.prop_idx - 1).key.?.loc,
|
|
allocator,
|
|
"The following .npmrc registry option was not applied:\n\n <b>{f}<r>\n\nBecause we currently don't support the <b>{s}<r> option.",
|
|
.{
|
|
conf_item,
|
|
@tagName(conf_item.optname),
|
|
},
|
|
);
|
|
continue;
|
|
},
|
|
else => {},
|
|
}
|
|
if (try conf_item_.dupe(allocator)) |x| try configs.append(x);
|
|
}
|
|
}
|
|
|
|
for (configs.items) |conf_item| {
|
|
const conf_item_url = bun.URL.parse(conf_item.registry_url);
|
|
|
|
if (std.mem.eql(u8, bun.strings.withoutTrailingSlash(default_registry_url.host), bun.strings.withoutTrailingSlash(conf_item_url.host))) {
|
|
// Apply config to default registry
|
|
const v: *bun.schema.api.NpmRegistry = brk: {
|
|
if (install.default_registry) |*r| break :brk r;
|
|
install.default_registry = bun.schema.api.NpmRegistry{
|
|
.password = "",
|
|
.token = "",
|
|
.username = "",
|
|
.url = Registry.default_url,
|
|
.email = "",
|
|
};
|
|
break :brk &install.default_registry.?;
|
|
};
|
|
|
|
switch (conf_item.optname) {
|
|
._authToken => {
|
|
if (try conf_item.dupeValueDecoded(allocator, log, source)) |x| v.token = x;
|
|
},
|
|
.username => {
|
|
if (try conf_item.dupeValueDecoded(allocator, log, source)) |x| v.username = x;
|
|
},
|
|
._password => {
|
|
if (try conf_item.dupeValueDecoded(allocator, log, source)) |x| v.password = x;
|
|
},
|
|
._auth => {
|
|
try @"handle _auth"(allocator, v, &conf_item, log, source);
|
|
},
|
|
.email => {
|
|
if (try conf_item.dupeValueDecoded(allocator, log, source)) |x| v.email = x;
|
|
},
|
|
.certfile, .keyfile => unreachable,
|
|
}
|
|
}
|
|
|
|
for (registry_map.scopes.keys(), registry_map.scopes.values()) |*k, *v| {
|
|
const url = url_map.get(k.*) orelse unreachable;
|
|
|
|
if (std.mem.eql(u8, bun.strings.withoutTrailingSlash(url.host), bun.strings.withoutTrailingSlash(conf_item_url.host))) {
|
|
if (conf_item_url.hostname.len > 0) {
|
|
if (!std.mem.eql(u8, bun.strings.withoutTrailingSlash(url.hostname), bun.strings.withoutTrailingSlash(conf_item_url.hostname))) {
|
|
continue;
|
|
}
|
|
}
|
|
// Apply config to scoped registry
|
|
switch (conf_item.optname) {
|
|
._authToken => {
|
|
if (try conf_item.dupeValueDecoded(allocator, log, source)) |x| v.token = x;
|
|
},
|
|
.username => {
|
|
if (try conf_item.dupeValueDecoded(allocator, log, source)) |x| v.username = x;
|
|
},
|
|
._password => {
|
|
if (try conf_item.dupeValueDecoded(allocator, log, source)) |x| v.password = x;
|
|
},
|
|
._auth => {
|
|
try @"handle _auth"(allocator, v, &conf_item, log, source);
|
|
},
|
|
.email => {
|
|
if (try conf_item.dupeValueDecoded(allocator, log, source)) |x| v.email = x;
|
|
},
|
|
.certfile, .keyfile => unreachable,
|
|
}
|
|
// We have to keep going as it could match multiple scopes
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn @"handle _auth"(
|
|
allocator: Allocator,
|
|
v: *bun.schema.api.NpmRegistry,
|
|
conf_item: *const ConfigIterator.Item,
|
|
log: *bun.logger.Log,
|
|
source: *const bun.logger.Source,
|
|
) OOM!void {
|
|
if (conf_item.value.len == 0) {
|
|
try log.addErrorOpts(
|
|
"invalid _auth value, expected base64 encoded \"<username>:<password>\", received an empty string",
|
|
.{
|
|
.source = source,
|
|
.loc = conf_item.loc,
|
|
.redact_sensitive_information = true,
|
|
},
|
|
);
|
|
return;
|
|
}
|
|
const decode_len = bun.base64.decodeLen(conf_item.value);
|
|
const decoded = try allocator.alloc(u8, decode_len);
|
|
const result = bun.base64.decode(decoded[0..], conf_item.value);
|
|
if (!result.isSuccessful()) {
|
|
defer allocator.free(decoded);
|
|
try log.addErrorOpts(
|
|
"invalid _auth value, expected valid base64",
|
|
.{
|
|
.source = source,
|
|
.loc = conf_item.loc,
|
|
.redact_sensitive_information = true,
|
|
},
|
|
);
|
|
return;
|
|
}
|
|
const @"username:password" = decoded[0..result.count];
|
|
const colon_idx = std.mem.indexOfScalar(u8, @"username:password", ':') orelse {
|
|
defer allocator.free(@"username:password");
|
|
try log.addErrorOpts(
|
|
"invalid _auth value, expected base64 encoded \"<username>:<password>\"",
|
|
.{
|
|
.source = source,
|
|
.loc = conf_item.loc,
|
|
.redact_sensitive_information = true,
|
|
},
|
|
);
|
|
return;
|
|
};
|
|
const username = @"username:password"[0..colon_idx];
|
|
if (colon_idx + 1 >= @"username:password".len) {
|
|
defer allocator.free(@"username:password");
|
|
try log.addErrorOpts(
|
|
"invalid _auth value, expected base64 encoded \"<username>:<password>\"",
|
|
.{
|
|
.source = source,
|
|
.loc = conf_item.loc,
|
|
.redact_sensitive_information = true,
|
|
},
|
|
);
|
|
return;
|
|
}
|
|
const password = @"username:password"[colon_idx + 1 ..];
|
|
v.username = username;
|
|
v.password = password;
|
|
return;
|
|
}
|
|
|
|
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
const bun = @import("bun");
|
|
const Global = bun.Global;
|
|
const OOM = bun.OOM;
|
|
const Output = bun.Output;
|
|
const Loc = bun.logger.Loc;
|
|
const Registry = bun.install.Npm.Registry;
|
|
|
|
const js_ast = bun.ast;
|
|
const E = bun.ast.E;
|
|
const Expr = bun.ast.Expr;
|
|
const Rope = js_ast.E.Object.Rope;
|