snix_store/pathinfoservice/
from_addr.rs

1use super::PathInfoService;
2
3use crate::composition::REG;
4use snix_castore::composition::{
5    CompositionContext, DeserializeWithRegistry, ServiceBuilder, with_registry,
6};
7use std::sync::Arc;
8use url::Url;
9
10/// Constructs a new instance of a [PathInfoService] from an URI.
11///
12/// The following URIs are supported:
13/// - `redb+memory:`
14///   Uses a in-memory implementation.
15/// - `redb:///absolute/path/to/somewhere`
16///   Uses redb, using a path on the disk for persistency. Can be only opened
17///   from one process at the same time.
18/// - `nix+https://cache.nixos.org?trusted_public_keys[0]=cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=`
19///   Exposes the Nix binary cache as a PathInfoService, ingesting NARs into the
20///   {Blob,Directory}Service. You almost certainly want to use this with some cache.
21///   The `trusted_public_keys` URL parameter can be provided, which will then
22///   enable signature verification.
23/// - `grpc+unix:///absolute/path/to/somewhere`
24///   Connects to a local snix-store gRPC service via Unix socket.
25/// - `grpc+http://host:port`, `grpc+https://host:port`
26///   Connects to a (remote) snix-store gRPC service.
27///
28/// As the [PathInfoService] needs to talk to [snix_castore::blobservice::BlobService] and
29/// [snix_castore::directoryservice::DirectoryService], these also need to be passed in.
30pub async fn from_addr(
31    uri: &str,
32    context: Option<&CompositionContext<'_>>,
33) -> Result<Arc<dyn PathInfoService>, Box<dyn std::error::Error + Send + Sync>> {
34    #[allow(unused_mut)]
35    let mut url = Url::parse(uri).map_err(|e| format!("unable to parse url: {e}"))?;
36
37    let path_info_service_config = with_registry(&REG, || {
38        <DeserializeWithRegistry<Box<dyn ServiceBuilder<Output = dyn PathInfoService>>>>::try_from(
39            url,
40        )
41    })?
42    .0;
43    let path_info_service = path_info_service_config
44        .build(
45            "anonymous",
46            context.unwrap_or(&CompositionContext::blank(&REG)),
47        )
48        .await?;
49
50    Ok(path_info_service)
51}
52
53#[cfg(test)]
54mod tests {
55    use super::from_addr;
56    use crate::composition::REG;
57    use rstest::rstest;
58    use snix_castore::blobservice::{BlobService, MemoryBlobServiceConfig};
59    use snix_castore::composition::{Composition, DeserializeWithRegistry, ServiceBuilder};
60    use snix_castore::directoryservice::DirectoryService;
61    use std::sync::LazyLock;
62    use tempfile::TempDir;
63
64    static TMPDIR_REDB_1: LazyLock<TempDir> = LazyLock::new(|| TempDir::new().unwrap());
65    static TMPDIR_REDB_2: LazyLock<TempDir> = LazyLock::new(|| TempDir::new().unwrap());
66
67    // the gRPC tests below don't fail, because we connect lazily.
68
69    #[rstest]
70    /// This uses a unsupported scheme.
71    #[case::unsupported_scheme("http://foo.example/test", false)]
72    /// This configures redb without a path, which should fail.
73    #[case::redb_invalid_missing_path("redb://", false)]
74    /// This configures redb with /, which should fail.
75    #[case::redb_invalid_root("redb:///", false)]
76    /// This configures redb with a host, not path, which should fail.
77    #[case::redb_invalid_host("redb://foo.example", false)]
78    /// This configures redb with a valid path, which should succeed.
79    #[case::redb_valid_path(&format!("redb://{}", &TMPDIR_REDB_1.path().join("foo").to_str().unwrap()), true)]
80    /// This configures redb with a host, and a valid path path, which should fail.
81    #[case::redb_invalid_host_with_valid_path(&format!("redb://foo.example{}", &TMPDIR_REDB_2.path().join("bar").to_str().unwrap()), false)]
82    /// This configures redb in-memory.
83    #[case::redb_memory_valid("redb+memory:", true)]
84    /// This configures redb in-memory, but wrongly adds a path.
85    #[case::redb_memory_invalid_path("redb+memory:/foo/bar", false)]
86    /// This configures redb in-memory, but wrongly adds authority.
87    #[case::redb_memory_invalid_authority("redb+memory://", false)]
88    /// This configures redb in-memory, but wrongly adds a path (with authority).
89    #[case::redb_memory_invalid_authority_path("redb+memory:///foo/bar", false)]
90    #[case::nix_http(
91        "nix+https://cache.nixos.org?trusted_public_keys[0]=cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=",
92        true
93    )]
94    /// Correct scheme to connect to a unix socket.
95    #[case::grpc_valid_unix_socket("grpc+unix:///path/to/somewhere", true)]
96    /// Correct scheme for unix socket, but setting a host too, which is invalid.
97    #[case::grpc_invalid_unix_socket_and_host("grpc+unix://host.example/path/to/somewhere", false)]
98    /// Correct scheme to connect to localhost, with port 12345
99    #[case::grpc_valid_ipv6_localhost_port_12345("grpc+http://[::1]:12345", true)]
100    /// Correct scheme to connect to localhost over http, without specifying a port.
101    #[case::grpc_valid_http_host_without_port("grpc+http://localhost", true)]
102    /// Correct scheme to connect to localhost over http, without specifying a port.
103    #[case::grpc_valid_https_host_without_port("grpc+https://localhost", true)]
104    /// Correct scheme to connect to localhost over http, but with additional path, which is invalid.
105    #[case::grpc_invalid_host_and_path("grpc+http://localhost/some-path", false)]
106    /// A valid example for Bigtable.
107    #[cfg_attr(
108        all(feature = "cloud", feature = "integration"),
109        case::bigtable_valid(
110            "bigtable://instance-1?project_id=project-1&table_name=table-1&family_name=cf1",
111            true
112        )
113    )]
114    /// An invalid example for Bigtable, missing fields
115    #[cfg_attr(
116        all(feature = "cloud", feature = "integration"),
117        case::bigtable_invalid_missing_fields("bigtable://instance-1", false)
118    )]
119    #[tokio::test]
120    async fn test_from_addr_tokio(#[case] uri_str: &str, #[case] exp_succeed: bool) {
121        use snix_castore::directoryservice::RedbDirectoryServiceConfig;
122
123        let mut comp = Composition::new(&REG);
124        comp.extend(vec![(
125            "root".into(),
126            DeserializeWithRegistry(Box::new(MemoryBlobServiceConfig {})
127                as Box<dyn ServiceBuilder<Output = dyn BlobService>>),
128        )]);
129        comp.extend(vec![(
130            "root".into(),
131            DeserializeWithRegistry(Box::new(RedbDirectoryServiceConfig::default())
132                as Box<dyn ServiceBuilder<Output = dyn DirectoryService>>),
133        )]);
134
135        let resp = from_addr(uri_str, Some(&comp.context())).await;
136
137        if exp_succeed {
138            resp.expect("should succeed");
139        } else {
140            assert!(resp.is_err(), "should fail");
141        }
142    }
143}