Skip to content

Commit

Permalink
Add support for downloading the sparse index config
Browse files Browse the repository at this point in the history
  • Loading branch information
Nemo157 committed Apr 7, 2024
1 parent 289617b commit 2b8460c
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 16 deletions.
99 changes: 83 additions & 16 deletions src/sparse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ impl SparseIndex {
Self { path, url }
}

/// Get the global configuration of the index.
/// Get the global configuration of the index. There are no guarantees around freshness,
/// and if the config is not available, no fetch will be performed.
pub fn index_config(&self) -> Result<IndexConfig, Error> {
let path = self.path.join("config.json");
let bytes = std::fs::read(path).map_err(Error::Io)?;
Expand Down Expand Up @@ -137,24 +138,10 @@ impl SparseIndex {
version
}

/// Creates an HTTP request that can be sent via your HTTP client of choice
/// to retrieve the current metadata for the specified crate
///
/// See [`Self::parse_cache_response`] processing the response from the remote
/// index
///
/// It is highly recommended to assume HTTP/2 when making requests to remote
/// indices, at least crates.io
#[cfg(feature = "sparse")]
pub fn make_cache_request(&self, name: &str) -> Result<http::request::Builder, Error> {
fn make_request(&self, url: &str, cache_version: Option<&str>) -> Result<http::request::Builder, Error> {
use http::header;

let url = self
.crate_url(name)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "crate name is invalid"))?;

let cache_version = self.read_cache_version(name);

let mut req = http::Request::get(url).version(http::Version::HTTP_2);

{
Expand Down Expand Up @@ -198,6 +185,86 @@ impl SparseIndex {
Ok(req)
}

/// Creates an HTTP request that can be sent via your HTTP client of choice
/// to retrieve the config for this index
///
/// See [`Self::parse_config_response`] processing the response from the remote
/// index
///
/// It is highly recommended to assume HTTP/2 when making requests to remote
/// indices, at least crates.io
#[cfg(feature = "sparse")]
pub fn make_config_request(&self) -> Result<http::request::Builder, Error> {
self.make_request(&format!("{}config.json", self.url()), None)
}

/// Creates an HTTP request that can be sent via your HTTP client of choice
/// to retrieve the current metadata for the specified crate
///
/// See [`Self::parse_cache_response`] processing the response from the remote
/// index
///
/// It is highly recommended to assume HTTP/2 when making requests to remote
/// indices, at least crates.io
#[cfg(feature = "sparse")]
pub fn make_cache_request(&self, name: &str) -> Result<http::request::Builder, Error> {
self.make_request(
&self
.crate_url(name)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "crate name is invalid"))?,
self.read_cache_version(name).as_deref(),
)
}

/// Process the response to a request created by [`Self::make_config_request`]
///
/// You may specify whether an updated config file is written locally to the
/// cache or not
///
/// Note that responses from sparse HTTP indices, at least crates.io, may
/// send responses with `gzip` compression, it is your responsibility to
/// decompress it before sending to this function
#[cfg(feature = "sparse")]
pub fn parse_config_response(
&self,
response: http::Response<Vec<u8>>,
write_cache_entry: bool,
) -> Result<IndexConfig, Error> {
use http::StatusCode;
let (parts, body) = response.into_parts();

match parts.status {
// The server responded with the full contents of the config
StatusCode::OK => {
if write_cache_entry {
let path = self.path.join("config.json");
if std::fs::create_dir_all(path.parent().unwrap()).is_ok() {
// It's unfortunate if this fails for some reason, but
// not writing the cache entry shouldn't stop the user
// from getting the config
let _ = std::fs::write(&path, &body);
}
}

serde_json::from_slice(&body).map_err(Error::Json)
}
// The server requires authorization but the user didn't provide it
StatusCode::UNAUTHORIZED => {
Err(io::Error::new(io::ErrorKind::PermissionDenied, "the request was not authorized").into())
}
StatusCode::NOT_FOUND => {
Err(io::Error::new(io::ErrorKind::NotFound, "config.json not found in registry").into())
}
other => Err(io::Error::new(
io::ErrorKind::Unsupported,
format!(
"the server responded with status code '{other}', which is not supported in the current protocol"
),
)
.into()),
}
}

/// Process the response to a request created by [`Self::make_cache_request`]
///
/// This handles both the scenario where the local cache is missing the specified
Expand Down
4 changes: 4 additions & 0 deletions tests/fixtures/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"dl": "https://static.crates.io/crates",
"api": "https://crates.io"
}
72 changes: 72 additions & 0 deletions tests/sparse_index/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,76 @@ mod with_sparse_http_feature {
assert!(index.parse_cache_response("serde", response, false).unwrap().is_none());
}
}

mod make_config_request {
use crate::sparse_index::with_sparse_http_feature::crates_io;
use http::{header, Request};

#[test]
fn generates_request() {
let index = crates_io();
let builder = index.make_config_request().unwrap();
let req: Request<Vec<u8>> = builder.body(vec![]).unwrap();

assert_eq!(req.uri(), format!("{}config.json", index.url()).as_str());
assert!(req.headers().get(header::IF_NONE_MATCH).is_none());
assert!(req.headers().get(header::IF_MODIFIED_SINCE).is_none());
assert_eq!(req.headers().get(header::ACCEPT_ENCODING).unwrap(), "gzip,identity");
assert_eq!(
req.headers()
.get(header::HeaderName::from_static("cargo-protocol"))
.unwrap(),
"version=1"
);
assert_eq!(req.headers().get(header::ACCEPT).unwrap(), "text/plain");
}
}

mod parse_config_response {
use crates_index::{Error, SparseIndex};
use std::io;

// curl -v -H 'accept-encoding: gzip,identity' https://index.crates.io/config.json
const CONFIG_JSON: &[u8] = include_bytes!("../../tests/fixtures/config.json");

fn crates_io_tmp() -> (tempfile::TempDir, SparseIndex) {
let dir = tempfile::tempdir().unwrap();
let index = SparseIndex::with_path(dir.path(), crates_index::sparse::URL).unwrap();
(dir, index)
}

fn make_response() -> http::Response<Vec<u8>> {
http::Response::builder()
.status(http::StatusCode::OK)
.body(CONFIG_JSON.to_vec())
.unwrap()
}

#[test]
fn parses_response() {
let (_dir, index) = crates_io_tmp();

let config = index.parse_config_response(make_response(), false).unwrap();

assert_eq!(config.dl, "https://static.crates.io/crates");
assert_eq!(config.api.as_deref(), Some("https://crates.io"));
}

#[test]
fn stores_response() {
let (_dir, index) = crates_io_tmp();

let Err(Error::Io(err)) = index.index_config() else {
panic!("expected to get an io error")
};
assert!(err.kind() == io::ErrorKind::NotFound);

let config = index.parse_config_response(make_response(), true).unwrap();

let stored_config = index.index_config().unwrap();

assert_eq!(config.dl, stored_config.dl);
assert_eq!(config.api, stored_config.api);
}
}
}

0 comments on commit 2b8460c

Please sign in to comment.