Skip to main content

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 [BlobStore] / [ChunkStore] and
29/// [DirectoryService], these also need to be passed in.
30///
31/// [BlobStore]: [snix_castore::blobstore::BlobStore]
32/// [ChunkStore]: [snix_castore::chunkstore::ChunkStore]
33/// [DirectoryService]: [snix_castore::directoryservice::DirectoryService]
34pub async fn from_addr(
35    uri: &str,
36    context: Option<&CompositionContext<'_>>,
37) -> Result<Arc<dyn PathInfoService>, Box<dyn std::error::Error + Send + Sync>> {
38    #[allow(unused_mut)]
39    let mut url = Url::parse(uri).map_err(|e| format!("unable to parse url: {e}"))?;
40
41    let path_info_service_config = with_registry(&REG, || {
42        <DeserializeWithRegistry<Box<dyn ServiceBuilder<Output = dyn PathInfoService>>>>::try_from(
43            url,
44        )
45    })?
46    .0;
47    let path_info_service = path_info_service_config
48        .build(
49            "anonymous",
50            context.unwrap_or(&CompositionContext::blank(&REG)),
51        )
52        .await?;
53
54    Ok(path_info_service)
55}
56
57#[cfg(test)]
58mod tests {
59    use super::from_addr;
60    use crate::composition::REG;
61    use rstest::rstest;
62    use snix_castore::{
63        blobstore::{BlobStore, memory::MemoryBlobStoreConfig},
64        chunkstore::{ChunkStore, memory::MemoryChunkStoreConfig},
65        composition::{Composition, DeserializeWithRegistry, ServiceBuilder},
66        directoryservice::DirectoryService,
67    };
68
69    use std::sync::LazyLock;
70    use tempfile::TempDir;
71
72    static TMPDIR_REDB_1: LazyLock<TempDir> = LazyLock::new(|| TempDir::new().unwrap());
73    static TMPDIR_REDB_2: LazyLock<TempDir> = LazyLock::new(|| TempDir::new().unwrap());
74
75    // the gRPC tests below don't fail, because we connect lazily.
76
77    #[rstest]
78    /// This uses a unsupported scheme.
79    #[case::unsupported_scheme("http://foo.example/test", false)]
80    /// This configures redb without a path, which should fail.
81    #[case::redb_invalid_missing_path("redb://", false)]
82    /// This configures redb with /, which should fail.
83    #[case::redb_invalid_root("redb:///", false)]
84    /// This configures redb with a host, not path, which should fail.
85    #[case::redb_invalid_host("redb://foo.example", false)]
86    /// This configures redb with a valid path, but with authority component, which should fail.
87    #[case::redb_invalid_path_authority(&format!("redb://{}", TMPDIR_REDB_1.path().join("foo").to_str().unwrap()), false)]
88    /// This configures redb with a valid path, which should succeed.
89    #[case::redb_valid_path(&format!("redb:{}", TMPDIR_REDB_1.path().join("foo").to_str().unwrap()), true)]
90    /// This configures redb with a host, and a valid path path, which should fail.
91    #[case::redb_invalid_host_with_valid_path(&format!("redb://foo.example{}", TMPDIR_REDB_2.path().join("bar").to_str().unwrap()), false)]
92    /// This configures redb in-memory.
93    #[case::redb_memory_valid("redb+memory:", true)]
94    /// This configures redb in-memory, but wrongly adds a path.
95    #[case::redb_memory_invalid_path("redb+memory:/foo/bar", false)]
96    /// This configures redb in-memory, but wrongly adds authority.
97    #[case::redb_memory_invalid_authority("redb+memory://", false)]
98    /// This configures redb in-memory, but wrongly adds a path (with authority).
99    #[case::redb_memory_invalid_authority_path("redb+memory:///foo/bar", false)]
100    #[case::nix_http(
101        "nix+https://cache.nixos.org?trusted_public_keys[0]=cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=",
102        true
103    )]
104    #[case::nix_http_force_download_nar(
105        "nix+https://cache.nixos.org?trusted_public_keys[0]=cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=&force_download_nar=true",
106        true
107    )]
108    /// Correct scheme for unix socket.
109    #[case::grpc_valid_unix_socket("grpc+unix:/path/to/somewhere", true)]
110    /// Scheme to connect to a unix socket, but with authority.
111    #[case::grpc_invalid_unix_socket_authority("grpc+unix:///path/to/somewhere", false)]
112    /// Scheme to connect to a unix socket, but with authority.
113    #[case::grpc_invalid_unix_socket_and_host("grpc+unix://host.example/path/to/somewhere", false)]
114    /// Scheme to connect to localhost, with port 12345
115    #[case::grpc_valid_ipv6_localhost_port_12345("grpc+http://[::1]:12345", true)]
116    /// Scheme to connect to localhost over http, without specifying a port.
117    #[case::grpc_valid_http_host_without_port("grpc+http://localhost", true)]
118    /// Scheme to connect to localhost over http, without specifying a port.
119    #[case::grpc_valid_https_host_without_port("grpc+https://localhost", true)]
120    /// Scheme to connect to localhost over http, but with additional path, which is invalid.
121    #[case::grpc_invalid_host_and_path("grpc+http://localhost/some-path", false)]
122    /// A valid example for Bigtable.
123    #[cfg_attr(
124        all(feature = "cloud", feature = "integration"),
125        case::bigtable_valid(
126            "bigtable://instance-1?project_id=project-1&table_name=table-1&family_name=cf1",
127            true
128        )
129    )]
130    /// An invalid example for Bigtable, missing fields
131    #[cfg_attr(
132        all(feature = "cloud", feature = "integration"),
133        case::bigtable_invalid_missing_fields("bigtable://instance-1", false)
134    )]
135    #[tokio::test]
136    async fn test_from_addr_tokio(#[case] uri_str: &str, #[case] exp_succeed: bool) {
137        use snix_castore::directoryservice::RedbDirectoryServiceConfig;
138
139        let mut comp = Composition::new(&REG);
140        comp.extend(vec![(
141            "root".into(),
142            DeserializeWithRegistry(Box::new(MemoryBlobStoreConfig {})
143                as Box<dyn ServiceBuilder<Output = dyn BlobStore>>),
144        )]);
145        comp.extend(vec![(
146            "root".into(),
147            DeserializeWithRegistry(Box::new(MemoryChunkStoreConfig {})
148                as Box<dyn ServiceBuilder<Output = dyn ChunkStore>>),
149        )]);
150        comp.extend(vec![(
151            "root".into(),
152            DeserializeWithRegistry(Box::new(RedbDirectoryServiceConfig::default())
153                as Box<dyn ServiceBuilder<Output = dyn DirectoryService>>),
154        )]);
155
156        let resp = from_addr(uri_str, Some(&comp.context())).await;
157
158        if exp_succeed {
159            resp.expect("should succeed");
160        } else {
161            assert!(resp.is_err(), "should fail");
162        }
163    }
164}