1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
use crate::lockfile::package::PackageColumns as _;
use std::io::Write as _;
use bun_ast::{Loc, Log};
use bun_core::strings;
use bun_core::{Global, Output};
use bun_js_parser as js_ast;
use bun_semver::{SlicedString, String as SemverString, string::Builder as StringBuilder};
use bun_install::dependency::{self, DependencyExt as _};
use bun_install::{
Dependency, INVALID_PACKAGE_ID, Lockfile, PackageID, PackageManager, PackageNameHash,
};
// `lockfile.packages.items_name()` is provided by an extension trait on
// `MultiArrayList<Package>` (Zig: `lockfile.packages.items(.name)`).
pub struct UpdateRequest {
// TODO(port): lifetime — Zig leaks these (no deinit); using &'static for now
pub name: &'static [u8],
pub name_hash: PackageNameHash,
pub version: dependency::Version,
/// Backing buffer for `version.literal` (and friends) — either a leaked
/// CLI positional (truly process-lifetime) or the active lockfile's
/// `buffers.string_bytes`. Stored as a raw fat pointer because the
/// lockfile buffer's lifetime cannot be expressed as `'static` without UB
/// lifetime extension (PORTING.md §Forbidden patterns), and threading a
/// real `<'a>` through every `&mut [UpdateRequest]` in the install
/// pipeline is a larger reshape. ARENA-class field per the PORTING.md
/// type map: `[]const u8` struct-field, never freed, points into a buffer
/// owned elsewhere → `RawSlice<u8>` (centralises the outlives-holder
/// invariant; see `version_buf()`).
pub version_buf: bun_ptr::RawSlice<u8>,
pub package_id: PackageID,
pub is_aliased: bool,
pub failed: bool,
/// This must be cloned to handle when the AST store resets
// TODO(port): lifetime — ARENA-owned (AST Expr.Data store); raw ptr per LIFETIMES.tsv
pub e_string: Option<*mut js_ast::E::String>,
}
impl Default for UpdateRequest {
fn default() -> Self {
Self {
name: b"",
name_hash: 0,
version: dependency::Version::default(),
version_buf: bun_ptr::RawSlice::EMPTY,
package_id: INVALID_PACKAGE_ID,
is_aliased: false,
failed: false,
e_string: None,
}
}
}
pub type Array = Vec<UpdateRequest>;
/// Park CLI-lifetime bytes in a process-lifetime static so LSan sees them as
/// reachable. `UpdateRequest::name`/`version_buf` store raw `&'static`/
/// `RawSlice` views because they may later be repointed at lockfile buffers.
fn anchor_cli_bytes(b: Box<[u8]>) -> &'static [u8] {
static ANCHOR: bun_threading::Guarded<Vec<Box<[u8]>>> = bun_threading::Guarded::new(Vec::new());
let ptr: *const [u8] = &raw const *b;
ANCHOR.lock().push(b);
// SAFETY: `b`'s heap allocation is owned by `ANCHOR` for the rest of the
// process. `Box<[u8]>` is a fat pointer; pushing it into the Vec moves
// only the pointer, not the heap data.
unsafe { &*ptr }
}
impl UpdateRequest {
/// Borrow the backing string buffer.
///
/// SAFETY for callers: the buffer this points into (leaked CLI input, or
/// `lockfile.buffers.string_bytes` after `clean_with_logger`) must outlive
/// the returned slice and must not be reallocated while the borrow is
/// live. Both invariants hold on every call path today — `string_bytes`
/// is finalized before assignment in `clean_with_logger`, and the lockfile
/// is threaded alongside `updates` everywhere they are read.
#[inline]
pub fn version_buf(&self) -> &[u8] {
// See fn doc. `RawSlice` encapsulates the deref under the
// outlives-holder invariant; `Default` seeds it as `EMPTY` and every
// assignment is `RawSlice::new(&[u8])`.
self.version_buf.slice()
}
#[inline]
pub fn matches(&self, dependency: &Dependency, string_buf: &[u8]) -> bool {
self.name_hash
== if self.name.is_empty() {
StringBuilder::string_hash(dependency.version.literal.slice(string_buf))
} else {
dependency.name_hash
}
}
pub fn get_name(&self) -> &[u8] {
if self.is_aliased {
self.name
} else {
self.version.literal.slice(self.version_buf())
}
}
/// If `self.package_id` is not `invalid_package_id`, it must be less than `lockfile.packages.len`.
pub fn get_name_in_lockfile<'a>(&'a self, lockfile: &'a Lockfile) -> Option<&'a [u8]> {
if self.package_id == INVALID_PACKAGE_ID {
None
} else {
Some(lockfile.packages.items_name()[self.package_id as usize].slice(self.version_buf()))
}
}
/// It is incorrect to call this function before Lockfile.cleanWithLogger() because
/// resolved_name should be populated if possible.
///
/// `self` needs to be a pointer! If `self` is a copy and the name returned from
/// resolved_name is inlined, you will return a pointer to stack memory.
pub fn get_resolved_name<'a>(&'a self, lockfile: &'a Lockfile) -> &'a [u8] {
if self.is_aliased {
self.name
} else if let Some(name) = self.get_name_in_lockfile(lockfile) {
name
} else {
self.version.literal.slice(self.version_buf())
}
}
// NOTE: `pub const fromJS = @import("../../install_jsc/update_request_jsc.zig").fromJS;`
// deleted — in Rust, `from_js` lives on an extension trait in the `*_jsc` crate.
pub fn parse<'a>(
pm: Option<&mut PackageManager>,
log: &mut Log,
positionals: &[&[u8]],
update_requests: &'a mut Array,
subcommand: Subcommand,
) -> &'a mut [UpdateRequest] {
Self::parse_with_error(pm, log, positionals, update_requests, subcommand, true)
.unwrap_or_else(|_| Global::crash())
}
// TODO(port): narrow error set — only `UnrecognizedDependencyFormat` is returned
pub fn parse_with_error<'a>(
mut pm: Option<&mut PackageManager>,
log: &mut Log,
positionals: &[&[u8]],
update_requests: &'a mut Array,
subcommand: Subcommand,
fatal: bool,
) -> Result<&'a mut [UpdateRequest], bun_core::Error> {
// first one is always either:
// add
// remove
'outer: for positional in positionals {
let mut input: Vec<u8> = strings::trim(positional, b" \n\r\t").to_vec();
{
// Replacing "\\\\" (2 bytes) with "/" (1 byte) never grows the string, so a
// buffer of `input.len` bytes is always sufficient. Previously this was a
// fixed `[2048]u8` stack array which overflowed for longer positionals.
let mut temp = vec![0u8; input.len()];
// std.mem.replace(u8, input, "\\\\", "/", temp) — returns replacement count
let len = strings::replace(&input, b"\\\\", b"/", &mut temp);
let new_len = input.len() - len;
let input2 = &mut temp[..new_len];
bun_paths::resolve_path::platform_to_posix_in_place(input2);
input[..new_len].copy_from_slice(input2);
input.truncate(new_len);
}
match subcommand {
Subcommand::Link | Subcommand::Unlink => {
if !input.starts_with(b"link:") {
let mut buf = Vec::with_capacity(input.len() * 2 + 6);
write!(&mut buf, "{0}@link:{0}", bstr::BStr::new(&input))
.expect("unreachable");
input = buf;
}
}
_ => {}
}
// CLI-lifetime allocation: `version_buf` is later reassigned to
// point at lockfile buffers, so the field is a raw `*const [u8]`
// rather than `Box<[u8]>`. Park the bytes in a process-lifetime
// static so LSan sees them as reachable instead of `Vec::leak`.
let input: &'static [u8] = anchor_cli_bytes(input.into_boxed_slice());
let mut value: &'static [u8] = input;
let mut alias: Option<&'static [u8]> = None;
if !Dependency::is_tarball(input) && strings::is_npm_package_name(input) {
alias = Some(input);
value = &input[input.len()..];
} else if input.len() > 1 {
if let Some(at) = strings::index_of_char(&input[1..], b'@') {
let name = &input[0..at as usize + 1];
if strings::is_npm_package_name(name) {
alias = Some(name);
value = &input[at as usize + 2..];
}
}
}
let placeholder = SemverString::from(b"@@@");
let Some(mut version) = Dependency::parse_with_optional_tag(
if let Some(name) = alias {
SemverString::init(input, name)
} else {
placeholder
},
alias.map(StringBuilder::string_hash),
value,
None,
&SlicedString::init(input, value),
Some(&mut *log),
pm.as_deref_mut(),
) else {
if fatal {
Output::err_generic(
"unrecognised dependency format: {}",
format_args!("{}", bstr::BStr::new(positional)),
);
} else {
log.add_error_fmt(
None,
Loc::EMPTY,
format_args!(
"unrecognised dependency format: {}",
bstr::BStr::new(positional)
),
);
}
return Err(bun_core::err!("UnrecognizedDependencyFormat"));
};
// TODO(port): Dependency.Version tag/value layout — Zig uses separate .tag + .value union
if alias.is_some() && version.tag == dependency::version::Tag::Git {
if let Some(ver) = Dependency::parse_with_optional_tag(
placeholder,
None,
input,
None,
&SlicedString::init(input, input),
Some(&mut *log),
pm.as_deref_mut(),
) {
alias = None;
version = ver;
}
}
if match version.tag {
dependency::version::Tag::DistTag => {
version.dist_tag().name.eql(placeholder, input, input)
}
dependency::version::Tag::Npm => version.npm().name.eql(placeholder, input, input),
_ => false,
} {
if fatal {
Output::err_generic(
"unrecognised dependency format: {}",
format_args!("{}", bstr::BStr::new(positional)),
);
} else {
log.add_error_fmt(
None,
Loc::EMPTY,
format_args!(
"unrecognised dependency format: {}",
bstr::BStr::new(positional)
),
);
}
return Err(bun_core::err!("UnrecognizedDependencyFormat"));
}
let mut request = UpdateRequest {
version,
version_buf: bun_ptr::RawSlice::new(input),
..UpdateRequest::default()
};
if let Some(name) = alias {
request.is_aliased = true;
request.name = anchor_cli_bytes(name.to_vec().into_boxed_slice());
request.name_hash = StringBuilder::string_hash(name);
} else {
request.name_hash =
StringBuilder::string_hash(request.version.literal.slice(input));
}
for prev in update_requests.iter() {
if prev.name_hash == request.name_hash && request.name.len() == prev.name.len() {
continue 'outer;
}
}
update_requests.push(request);
}
Ok(update_requests.as_mut_slice())
}
}
pub use super::Subcommand;
pub use bun_install::package_manager::Options;
pub use bun_install::package_manager::command_line_arguments as CommandLineArguments;
// ported from: src/install/PackageManager/UpdateRequest.zig