Skip to content
Merged
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
3 changes: 1 addition & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions docs/frozen-fetch.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ authority is the URL-parser domain `localhost` (or no host at all); loopback
IP literals are rejected before `Url::to_file_path()` so Windows cannot
reinterpret them as UNC.

A well-formed `file://` lock source whose registry directory does not exist on
this machine (for example a throwaway publish directory recorded on another
machine) is not a trust input: the pinned digest is. Fetch then reads the pinned
version from the configured registry, and through that registry's GitHub
fallback when zpkg.net is unavailable. It verifies every field above exactly as
for the locked source and reports the substitution as a degraded note that does
not echo the source path. `immutable-nix-store-input` sources never fall back.

## Nix use

A Nix fixed-output derivation should ask `zed fetch` to publish into an existing
Expand Down
84 changes: 83 additions & 1 deletion src/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,24 @@ pub fn run(requested_root: &Path, cfg: &Config, options: FetchArgs) -> Result<Fe
let mut fetched = Vec::with_capacity(packages.len());
let mut degraded: Vec<String> = Vec::new();
for locked in &packages {
let source = effective_source(locked, &cfg.registry);
let locked_source = effective_source(locked, &cfg.registry);
let configured = cfg.registry.trim_end_matches('/');
// A committed lock can name a machine-local `file://` registry that
// does not exist here (a throwaway publish directory, another
// developer's path). The pin, not that path, is the authority, so read
// the pinned version from the configured registry — and through its
// fallback, GitHub — and verify it exactly as before.
let source = if locked_source != configured && missing_local_source(locked_source) {
degraded.push(format!(
"{}@{}: the locked local registry is not present on this machine; \
reading the pinned version from the configured registry",
locked.full_name(),
locked.version
));
configured
} else {
locked_source
};
if !registries.contains_key(source) {
registries.insert(source.to_string(), context.open(source)?);
}
Expand Down Expand Up @@ -396,6 +413,17 @@ fn validate_locked_packages(
Ok(packages)
}

/// A `file://` lock source whose registry directory is absent on this machine.
/// Immutable Nix store inputs never qualify: a missing store path is a broken
/// derivation, not an outage to route around, and must stay hermetic.
fn missing_local_source(source: &str) -> bool {
source_kind(source) == "file"
&& reqwest::Url::parse(source)
.ok()
.and_then(|url| url.to_file_path().ok())
.is_some_and(|path| !path.is_dir())
}

fn effective_source<'a>(package: &'a LockedPackage, fallback_registry: &'a str) -> &'a str {
let source = package.source.trim();
if source.is_empty() {
Expand Down Expand Up @@ -1164,6 +1192,60 @@ mod tests {
assert!(no_fetch_staging(outputs.path()));
}

#[test]
fn missing_local_lock_source_reads_the_pin_from_the_configured_registry() {
let project = tempfile::tempdir().unwrap();
let registry = tempfile::tempdir().unwrap();
let outputs = tempfile::tempdir().unwrap();
let mut locked = seed_package(
registry.path(),
"ores-wasm-loaders",
"owls-interfaces",
"0.1.1",
&[("rust/src/lib.rs", b"pub fn owls() {}\n")],
);
// The lock was written on a machine whose publish directory is gone.
let vanished = outputs.path().join("vanished-publish-registry");
locked.source = format!("file://{}", vanished.display());
write_lock(project.path(), vec![locked.clone()]);
let mut cfg = config(outputs.path().join("home"));
cfg.registry = format!("file://{}", registry.path().display());

let bundle = outputs.path().join("bundle");
let report = run(
project.path(),
&cfg,
FetchArgs {
frozen: true,
output: bundle.clone(),
},
)
.unwrap();

assert_eq!(report.packages, 1);
assert_eq!(report.degraded.len(), 1, "{:?}", report.degraded);
let vanished_text = vanished.to_string_lossy().to_string();
assert!(
!report.degraded[0].contains(&vanished_text),
"diagnostics must not echo the lock source: {:?}",
report.degraded
);
assert!(
bundle
.join("packages")
.join(&locked.sha256)
.join("pkg/rust/src/lib.rs")
.is_file()
);

assert!(missing_local_source(&locked.source));
assert!(!missing_local_source(&cfg.registry));
assert!(!missing_local_source(
"file:///nix/store/00000000000000000000000000000000-missing-zed-registry"
));
assert!(!missing_local_source("https://registry.zpkg.net"));
}

#[test]
fn tampered_lock_digest_fails_before_atomic_publish() {
let project = tempfile::tempdir().unwrap();
Expand Down
1 change: 0 additions & 1 deletion src/install_graph/hardening_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,3 @@ fn sequenced_receive_buffers_later_failures_until_earlier_results_arrive() {
.to_string();
assert!(error.contains("later lockfile entry failed"), "{error}");
}
use crate::registry::registry_for;
Loading
Loading