Skip to main content

syncweb_core/indexing/
links.rs

1//! Stable content links, signed mutable names, capabilities, and mirrors.
2//!
3//! Links are deliberately independent of the document and blob stores.  A
4//! caller can persist the records in either store, or use [`LinkResolver`] as
5//! a small in-memory resolver for local indexing and tests.  Immutable links
6//! identify a hash directly; mutable links identify a signed, monotonically
7//! advancing pointer.
8
9use std::{
10    collections::{BTreeMap, HashMap, HashSet},
11    fmt,
12    future::Future,
13    path::{Path, PathBuf},
14    pin::Pin,
15    str::{FromStr, Split},
16    sync::{Arc, Mutex},
17};
18
19use ed25519_dalek::SigningKey;
20use iroh::{PublicKey, SecretKey};
21use iroh_blobs::{Hash, ticket::BlobTicket};
22use iroh_docs::NamespaceId;
23use serde::{Deserialize, Serialize};
24
25use super::IndexingDatabase;
26use crate::{
27    constants::{LINK_SCHEME, LINK_SIGNATURE_CONTEXT},
28    error::{Result, SyncwebError},
29    folder::FolderManager,
30    node::{identity::IdentityManager, iroh_node::IrohNode},
31};
32
33const DEFAULT_PRIVATE_LINK_TTL: u64 = 30 * 24 * 60 * 60;
34
35/// An immutable content-addressed link.
36#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
37#[non_exhaustive]
38pub struct ContentLink {
39    /// The content or manifest hash identified by this link.
40    pub hash: Hash,
41}
42
43impl ContentLink {
44    /// Create an immutable content link.
45    #[must_use]
46    pub const fn new(hash: Hash) -> Self {
47        Self { hash }
48    }
49
50    /// Return the content hash.
51    #[must_use]
52    pub const fn hash(self) -> Hash {
53        self.hash
54    }
55
56    /// Parse an immutable content URI.
57    ///
58    /// # Errors
59    ///
60    /// Returns an error if the URI or hash is malformed.
61    pub fn parse(value: &str) -> Result<Self> {
62        value.parse()
63    }
64
65    /// Return the canonical URI.
66    #[must_use]
67    pub fn uri(self) -> String {
68        self.to_string()
69    }
70
71    /// Validate this link.
72    ///
73    /// # Errors
74    ///
75    /// This currently cannot fail, but is provided so all link kinds have a
76    /// common validation API.
77    pub const fn validate(self) -> Result<()> {
78        Ok(())
79    }
80}
81
82impl fmt::Display for ContentLink {
83    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
84        write!(formatter, "{LINK_SCHEME}content/{}", self.hash)
85    }
86}
87
88impl FromStr for ContentLink {
89    type Err = SyncwebError;
90
91    fn from_str(value: &str) -> Result<Self> {
92        let hash_text = value
93            .strip_prefix("syncweb://content/")
94            .ok_or_else(|| SyncwebError::InvalidConfig("content link must use syncweb://content/".to_owned()))?;
95        if hash_text.is_empty() || hash_text.contains('/') || hash_text.contains('?') || hash_text.contains('#') {
96            return Err(SyncwebError::InvalidConfig(
97                "content link has an invalid hash".to_owned(),
98            ));
99        }
100        let hash = hash_text
101            .parse::<Hash>()
102            .map_err(|error| SyncwebError::InvalidConfig(format!("invalid content link hash: {error}")))?;
103        Ok(Self::new(hash))
104    }
105}
106
107/// Alias used by callers that prefer the term immutable link.
108pub type ImmutableLink = ContentLink;
109
110/// A stable publisher/alias portion of a mutable link.
111#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
112#[non_exhaustive]
113pub struct NameLink {
114    /// The publisher whose signed pointer is followed.
115    pub publisher: PublicKey,
116    /// The publisher-controlled alias.
117    pub alias: String,
118}
119
120impl NameLink {
121    /// Create a mutable name link.
122    ///
123    /// # Errors
124    ///
125    /// Returns an error for an empty publisher alias or an alias that could
126    /// change URI path semantics.
127    pub fn new(publisher: PublicKey, alias: impl Into<String>) -> Result<Self> {
128        let link = Self {
129            publisher,
130            alias: alias.into(),
131        };
132        link.validate()?;
133        Ok(link)
134    }
135
136    /// Parse a mutable name URI.
137    ///
138    /// # Errors
139    ///
140    /// Returns an error if the URI or publisher is malformed.
141    pub fn parse(value: &str) -> Result<Self> {
142        value.parse()
143    }
144
145    /// Return the canonical URI.
146    #[must_use]
147    pub fn uri(&self) -> String {
148        self.to_string()
149    }
150
151    /// Validate the publisher alias.
152    ///
153    /// # Errors
154    ///
155    /// Returns an error if the alias is empty or contains URI delimiters.
156    pub fn validate(&self) -> Result<()> {
157        validate_path_segment(&self.alias, "mutable link alias")
158    }
159}
160
161impl fmt::Display for NameLink {
162    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
163        write!(formatter, "{LINK_SCHEME}name/{}/{}", self.publisher, self.alias)
164    }
165}
166
167impl FromStr for NameLink {
168    type Err = SyncwebError;
169
170    fn from_str(value: &str) -> Result<Self> {
171        let remainder = value
172            .strip_prefix("syncweb://name/")
173            .ok_or_else(|| SyncwebError::InvalidConfig("name link must use syncweb://name/".to_owned()))?;
174        let mut parts = remainder.split('/');
175        let publisher_text = next_part(&mut parts, "name link publisher")?;
176        let alias = next_part(&mut parts, "name link alias")?;
177        if parts.next().is_some() {
178            return Err(SyncwebError::InvalidConfig(
179                "name link has too many path segments".to_owned(),
180            ));
181        }
182        let publisher = publisher_text
183            .parse::<PublicKey>()
184            .map_err(|error| SyncwebError::InvalidIdentity(format!("invalid name link publisher: {error}")))?;
185        Self::new(publisher, alias)
186    }
187}
188
189/// Alias used by callers that prefer the term mutable link.
190pub type MutableLink = NameLink;
191
192/// A signed mutable pointer to an immutable manifest.
193///
194/// Every accepted pointer has a publisher, alias, sequence, and manifest
195/// bound by an Ed25519 signature.  A resolver accepts only a sequence greater
196/// than the currently accepted sequence for that publisher and alias.
197#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
198#[non_exhaustive]
199pub struct MutablePointer {
200    /// Publisher identity; this must match the signing key.
201    pub publisher: PublicKey,
202    /// Publisher-controlled alias.
203    pub alias: String,
204    /// Monotonically increasing update number.
205    pub sequence: u64,
206    /// Immutable manifest identified by this pointer.
207    pub manifest: Hash,
208    /// Optional semantic version used for version pinning.
209    #[serde(default)]
210    pub version: Option<String>,
211    /// Hex-encoded Ed25519 signature over the pointer without this field.
212    #[serde(default)]
213    pub signature: Option<String>,
214}
215
216impl MutablePointer {
217    /// Create an unsigned pointer.
218    ///
219    /// Call [`Self::sign`] before registering it with a resolver.
220    ///
221    /// # Errors
222    ///
223    /// Returns an error if the alias or sequence is invalid.
224    pub fn new(publisher: PublicKey, alias: impl Into<String>, manifest: Hash, sequence: u64) -> Result<Self> {
225        let pointer = Self {
226            publisher,
227            alias: alias.into(),
228            sequence,
229            manifest,
230            version: None,
231            signature: None,
232        };
233        pointer.validate()?;
234        Ok(pointer)
235    }
236
237    /// Create and sign a pointer with a Dalek signing key.
238    ///
239    /// # Errors
240    ///
241    /// Returns an error if the pointer is invalid or the signer does not
242    /// match the publisher.
243    pub fn signed(
244        publisher: PublicKey,
245        alias: impl Into<String>,
246        manifest: Hash,
247        sequence: u64,
248        signing_key: &SigningKey,
249    ) -> Result<Self> {
250        let mut pointer = Self::new(publisher, alias, manifest, sequence)?;
251        pointer.sign(signing_key)?;
252        Ok(pointer)
253    }
254
255    /// Create and sign a pointer with an iroh secret key.
256    ///
257    /// # Errors
258    ///
259    /// Returns an error if the pointer is invalid or the signer does not
260    /// match the publisher.
261    pub fn signed_with_secret_key(
262        publisher: PublicKey,
263        alias: impl Into<String>,
264        manifest: Hash,
265        sequence: u64,
266        secret_key: &SecretKey,
267    ) -> Result<Self> {
268        let signing_key = SigningKey::from_bytes(&secret_key.to_bytes());
269        Self::signed(publisher, alias, manifest, sequence, &signing_key)
270    }
271
272    /// Set a semantic version on this pointer.
273    #[must_use]
274    pub fn with_version(mut self, version: impl Into<String>) -> Self {
275        self.version = Some(version.into());
276        self.signature = None;
277        self
278    }
279
280    /// Return the corresponding stable name link.
281    ///
282    /// # Errors
283    ///
284    /// Returns an error if the alias is invalid.
285    pub fn link(&self) -> Result<NameLink> {
286        NameLink::new(self.publisher, self.alias.clone())
287    }
288
289    /// Return the immutable manifest hash targeted by this pointer.
290    #[must_use]
291    pub const fn manifest_hash(&self) -> Hash {
292        self.manifest
293    }
294
295    /// Serialize the pointer without its signature.
296    ///
297    /// # Errors
298    ///
299    /// Returns an error if the pointer is invalid or cannot be serialized.
300    pub fn unsigned_bytes(&self) -> Result<Vec<u8>> {
301        let mut unsigned = self.clone();
302        unsigned.signature = None;
303        unsigned.validate()?;
304        let mut bytes = LINK_SIGNATURE_CONTEXT.to_vec();
305        let encoded = serde_json::to_vec(&unsigned)
306            .map_err(|error| SyncwebError::operation("failed to serialize mutable link pointer", error))?;
307        bytes.extend_from_slice(&encoded);
308        Ok(bytes)
309    }
310
311    /// Sign this pointer with an Ed25519 key.
312    ///
313    /// # Errors
314    ///
315    /// Returns an error if the signer does not match the publisher or the
316    /// pointer cannot be serialized.
317    pub fn sign(&mut self, signing_key: &SigningKey) -> Result<()> {
318        if signing_key.verifying_key().to_bytes() != *self.publisher.as_bytes() {
319            return Err(SyncwebError::InvalidIdentity(
320                "mutable link signer does not match publisher".to_owned(),
321            ));
322        }
323        self.validate()?;
324        self.signature = Some(crate::signature::sign_hex(signing_key, &self.unsigned_bytes()?));
325        Ok(())
326    }
327
328    /// Verify the pointer's signature and all provider leases.
329    ///
330    /// # Errors
331    ///
332    /// Returns an error if the signature, pointer, or provider lease is
333    /// invalid.
334    pub fn verify_signature(&self) -> Result<()> {
335        self.validate()?;
336        let signature_text = self
337            .signature
338            .as_deref()
339            .ok_or_else(|| SyncwebError::InvalidConfig("mutable link pointer must be signed".to_owned()))?;
340        crate::signature::verify_hex(
341            &hex::encode(self.publisher.as_bytes()),
342            &self.unsigned_bytes()?,
343            signature_text,
344        )
345    }
346
347    /// Validate non-cryptographic pointer fields.
348    ///
349    /// # Errors
350    ///
351    /// Returns an error if a pointer field or provider lease is invalid.
352    pub fn validate(&self) -> Result<()> {
353        validate_path_segment(&self.alias, "mutable link alias")?;
354        if self.sequence == 0 {
355            return Err(SyncwebError::InvalidConfig(
356                "mutable link sequence must be greater than zero".to_owned(),
357            ));
358        }
359        if let Some(version) = &self.version
360            && version.trim().is_empty()
361        {
362            return Err(SyncwebError::InvalidConfig(
363                "mutable link version cannot be empty".to_owned(),
364            ));
365        }
366        Ok(())
367    }
368
369    /// Encode the signed pointer as JSON.
370    ///
371    /// # Errors
372    ///
373    /// Returns an error if the pointer is unsigned, invalid, or cannot be
374    /// serialized.
375    pub fn to_bytes(&self) -> Result<Vec<u8>> {
376        self.verify_signature()?;
377        serde_json::to_vec(self)
378            .map_err(|error| SyncwebError::operation("failed to serialize mutable link pointer", error))
379    }
380
381    /// Decode and validate a signed pointer.
382    ///
383    /// # Errors
384    ///
385    /// Returns an error if the bytes do not contain a valid signed pointer.
386    pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self> {
387        let pointer: Self = serde_json::from_slice(bytes.as_ref())
388            .map_err(|error| SyncwebError::operation("failed to deserialize mutable link pointer", error))?;
389        pointer.verify_signature()?;
390        Ok(pointer)
391    }
392}
393
394/// Alias for the signed mutable pointer terminology used by the protocol.
395pub type SignedMutablePointer = MutablePointer;
396
397/// A bearer capability link to a private manifest.
398#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
399#[non_exhaustive]
400pub struct PrivateLink {
401    /// Immutable manifest granted by this capability.
402    pub manifest: Hash,
403    /// Secret read capability.  It is intentionally included in the URI.
404    pub capability: [u8; 32],
405    /// Unix timestamp after which the capability is no longer accepted.
406    pub expires_at: u64,
407}
408
409impl PrivateLink {
410    /// Generate a private capability link with a cryptographically random
411    /// bearer token.
412    ///
413    /// # Errors
414    ///
415    /// Returns an error if the expiration is invalid.
416    pub fn generate(manifest: Hash, expires_at: u64) -> Result<Self> {
417        let capability = rand::random::<[u8; 32]>();
418        Self::new(manifest, capability, expires_at)
419    }
420
421    /// Parse a private capability URI.
422    ///
423    /// # Errors
424    ///
425    /// Returns an error if the URI, hash, capability, or expiration is
426    /// malformed.
427    pub fn parse(value: &str) -> Result<Self> {
428        value.parse()
429    }
430
431    /// Create a private capability link.
432    ///
433    /// # Errors
434    ///
435    /// Returns an error if the capability or expiration is invalid.
436    pub fn new(manifest: Hash, capability: [u8; 32], expires_at: u64) -> Result<Self> {
437        let link = Self {
438            manifest,
439            capability,
440            expires_at,
441        };
442        link.validate()?;
443        Ok(link)
444    }
445
446    /// Return the canonical capability URI.
447    #[must_use]
448    pub fn uri(&self) -> String {
449        self.to_string()
450    }
451
452    /// Validate the capability and expiry.
453    ///
454    /// # Errors
455    ///
456    /// Returns an error if the expiration is zero.
457    pub fn validate(&self) -> Result<()> {
458        if self.expires_at == 0 {
459            return Err(SyncwebError::InvalidConfig(
460                "private link expiration must be greater than zero".to_owned(),
461            ));
462        }
463        Ok(())
464    }
465
466    /// Return whether this link is expired at `now`.
467    #[must_use]
468    pub const fn is_expired_at(&self, now: u64) -> bool {
469        self.expires_at <= now
470    }
471
472    /// Return a stable key used by revocation lists.
473    #[must_use]
474    pub fn revocation_key(&self) -> (Hash, Vec<u8>) {
475        (self.manifest, self.capability.to_vec())
476    }
477}
478
479impl fmt::Display for PrivateLink {
480    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
481        write!(
482            formatter,
483            "{LINK_SCHEME}private/{}/{}?expires={}",
484            self.manifest,
485            hex::encode(self.capability),
486            self.expires_at
487        )
488    }
489}
490
491impl FromStr for PrivateLink {
492    type Err = SyncwebError;
493
494    fn from_str(value: &str) -> Result<Self> {
495        let (path, query) = value
496            .strip_prefix("syncweb://private/")
497            .ok_or_else(|| SyncwebError::InvalidConfig("private link must use syncweb://private/".to_owned()))?
498            .split_once('?')
499            .ok_or_else(|| SyncwebError::InvalidConfig("private link must contain an expiration".to_owned()))?;
500        let mut parts = path.split('/');
501        let manifest = next_part(&mut parts, "private link manifest")?;
502        let capability_hex = next_part(&mut parts, "private link capability")?;
503        if parts.next().is_some() {
504            return Err(SyncwebError::InvalidConfig(
505                "private link has too many path segments".to_owned(),
506            ));
507        }
508        let manifest_hash = manifest
509            .parse::<Hash>()
510            .map_err(|error| SyncwebError::InvalidConfig(format!("invalid private link manifest: {error}")))?;
511        let capability_bytes = hex::decode(capability_hex)
512            .map_err(|error| SyncwebError::InvalidConfig(format!("invalid private link capability: {error}")))?;
513        let capability: [u8; 32] = capability_bytes.try_into().map_err(|bytes: Vec<u8>| {
514            SyncwebError::InvalidConfig(format!("private link capability must be 32 bytes, got {}", bytes.len()))
515        })?;
516        let expires_text = query
517            .strip_prefix("expires=")
518            .ok_or_else(|| SyncwebError::InvalidConfig("private link query must contain expires".to_owned()))?;
519        let expires_at = expires_text
520            .parse::<u64>()
521            .map_err(|error| SyncwebError::InvalidConfig(format!("invalid private link expiration: {error}")))?;
522        Self::new(manifest_hash, capability, expires_at)
523    }
524}
525
526/// Alias for capability links.
527pub type CapabilityLink = PrivateLink;
528
529/// A direct provider ticket registered as a mirror.
530#[derive(Clone, Debug, Eq, PartialEq)]
531#[non_exhaustive]
532pub struct Mirror {
533    /// Hash served by this mirror.
534    pub hash: Hash,
535    /// Blob ticket for the mirror.
536    pub ticket: BlobTicket,
537}
538
539impl Mirror {
540    /// Construct a mirror from a blob ticket.
541    ///
542    /// # Errors
543    ///
544    /// Returns an error if the ticket does not match its hash.
545    pub fn new(ticket: BlobTicket) -> Result<Self> {
546        let mirror = Self {
547            hash: ticket.hash(),
548            ticket,
549        };
550        mirror.validate()?;
551        Ok(mirror)
552    }
553
554    /// Validate that the ticket is consistent with the mirror hash.
555    ///
556    /// # Errors
557    ///
558    /// Returns an error if the ticket does not match its hash.
559    pub fn validate(&self) -> Result<()> {
560        if self.ticket.hash() != self.hash {
561            return Err(SyncwebError::InvalidTicket(
562                "mirror ticket does not match its content hash".to_owned(),
563            ));
564        }
565        Ok(())
566    }
567}
568
569/// Result of resolving any public or private link.
570#[derive(Clone, Debug, Eq, PartialEq)]
571#[non_exhaustive]
572pub struct LinkResolution {
573    /// Manifest or content hash identified by the link.
574    pub manifest: Hash,
575    /// Semantic version selected by a mutable link, if available.
576    pub version: Option<String>,
577    /// Mutable pointer sequence, if the link is mutable.
578    pub sequence: Option<u64>,
579    /// Direct blob tickets in mirror fallback order.
580    pub tickets: Vec<BlobTicket>,
581}
582
583impl LinkResolution {
584    /// Create a new resolution containing just a manifest hash.
585    #[must_use]
586    pub const fn new(manifest: Hash) -> Self {
587        Self {
588            manifest,
589            version: None,
590            sequence: None,
591            tickets: Vec::new(),
592        }
593    }
594
595    /// Return the resolved hash.
596    #[must_use]
597    pub const fn hash(&self) -> Hash {
598        self.manifest
599    }
600
601    /// Return provider and mirror tickets in fallback order.
602    #[must_use]
603    pub fn provider_tickets(&self) -> &[BlobTicket] {
604        &self.tickets
605    }
606
607    /// Fetch using the first provider that succeeds.
608    ///
609    /// # Errors
610    ///
611    /// Returns an error when no provider exists or every provider fails.
612    pub fn fetch_with<T, Fetch>(&self, mut fetch: Fetch) -> Result<T>
613    where
614        Fetch: FnMut(&BlobTicket) -> Result<T>,
615    {
616        fetch_from_mirrors(&self.tickets, &mut fetch)
617    }
618}
619
620/// Alias used by callers that prefer the shorter resolution name.
621pub type ResolvedLink = LinkResolution;
622
623/// A parsed syncweb stable link.
624#[derive(Clone, Debug, Eq, PartialEq, Hash)]
625#[non_exhaustive]
626pub enum Link {
627    /// Immutable content link.
628    Content(ContentLink),
629    /// Mutable publisher name.
630    Name(NameLink),
631    /// Private bearer capability.
632    Private(PrivateLink),
633}
634
635impl Link {
636    /// Parse a stable link.
637    ///
638    /// # Errors
639    ///
640    /// Returns an error if the URI is not a supported syncweb link.
641    pub fn parse(value: &str) -> Result<Self> {
642        value.parse()
643    }
644
645    /// Return the link URI.
646    #[must_use]
647    pub fn uri(&self) -> String {
648        self.to_string()
649    }
650}
651
652impl fmt::Display for Link {
653    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
654        match self {
655            Self::Content(link) => link.fmt(formatter),
656            Self::Name(link) => link.fmt(formatter),
657            Self::Private(link) => link.fmt(formatter),
658        }
659    }
660}
661
662impl FromStr for Link {
663    type Err = SyncwebError;
664
665    fn from_str(value: &str) -> Result<Self> {
666        if value.starts_with("syncweb://content/") {
667            return Ok(Self::Content(value.parse()?));
668        }
669        if value.starts_with("syncweb://name/") {
670            return Ok(Self::Name(value.parse()?));
671        }
672        if value.starts_with("syncweb://private/") {
673            return Ok(Self::Private(value.parse()?));
674        }
675        Err(SyncwebError::InvalidConfig(
676            "unsupported syncweb link; expected content, name, or private".to_owned(),
677        ))
678    }
679}
680
681/// Options for a link resolution operation.
682#[derive(Clone, Debug, Default, Eq, PartialEq)]
683#[non_exhaustive]
684pub struct ResolveOptions {
685    /// Select an exact semantic version from a mutable pointer history.
686    pub version: Option<String>,
687    /// Evaluate expiry and provider leases at this timestamp.
688    pub now: Option<u64>,
689}
690
691impl ResolveOptions {
692    /// Select an exact version.
693    #[must_use]
694    pub fn version(version: impl Into<String>) -> Self {
695        Self {
696            version: Some(version.into()),
697            now: None,
698        }
699    }
700
701    /// Evaluate the link at an explicit time.
702    #[must_use]
703    pub const fn at(now: u64) -> Self {
704        Self {
705            version: None,
706            now: Some(now),
707        }
708    }
709
710    /// Set an exact version while retaining the other options.
711    #[must_use]
712    pub fn with_version(mut self, version: impl Into<String>) -> Self {
713        self.version = Some(version.into());
714        self
715    }
716}
717
718#[derive(Clone, Debug, Default)]
719struct ResolverState {
720    mirrors: HashMap<Hash, Vec<Mirror>>,
721    pointers: HashMap<(PublicKey, String), PointerHistory>,
722    revoked: HashSet<(Hash, Vec<u8>)>,
723}
724
725#[derive(Clone, Debug, Default)]
726struct PointerHistory {
727    current: Option<MutablePointer>,
728    versions: BTreeMap<String, MutablePointer>,
729}
730
731/// Provider fetch hook for network-aware resolution.
732pub type ProviderFetch =
733    Arc<dyn Fn(NameLink) -> Pin<Box<dyn Future<Output = Result<LinkResolution>> + Send>> + Send + Sync>;
734
735/// In-memory resolver and mirror registry.
736#[derive(Clone)]
737pub struct LinkResolver {
738    state: Arc<Mutex<ResolverState>>,
739    database: Option<IndexingDatabase>,
740    provider_fetch: Option<ProviderFetch>,
741}
742
743impl fmt::Debug for LinkResolver {
744    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
745        f.debug_struct("LinkResolver")
746            .field("state", &self.state)
747            .field("database", &self.database)
748            .finish_non_exhaustive()
749    }
750}
751
752impl Default for LinkResolver {
753    fn default() -> Self {
754        Self {
755            state: Arc::new(Mutex::new(ResolverState::default())),
756            database: None,
757            provider_fetch: None,
758        }
759    }
760}
761
762impl LinkResolver {
763    /// Create an empty resolver.
764    #[must_use]
765    pub fn new() -> Self {
766        Self::default()
767    }
768
769    /// Create a resolver with a provider fetch hook for network resolution.
770    ///
771    /// The fetch function is called when a name link cannot be resolved
772    /// locally, allowing the caller to query remote peers via iroh-docs.
773    #[must_use]
774    pub fn with_provider_fetch(fetch: ProviderFetch) -> Self {
775        Self {
776            state: Arc::new(Mutex::new(ResolverState::default())),
777            database: None,
778            provider_fetch: Some(fetch),
779        }
780    }
781
782    /// Attempt to resolve a name link by querying remote peers.
783    ///
784    /// Returns `None` if no provider fetch hook is registered or the link
785    /// is not a name link. The caller is responsible for falling back to
786    /// local resolution.
787    ///
788    /// # Errors
789    ///
790    /// Returns an error if the remote fetch fails.
791    pub async fn resolve_remote(&self, link: &Link) -> Result<Option<LinkResolution>> {
792        match link {
793            Link::Name(name) => {
794                if let Some(ref fetch) = self.provider_fetch {
795                    fetch(name.clone()).await.map(Some)
796                } else {
797                    Ok(None)
798                }
799            }
800            Link::Content(_) | Link::Private(_) => Ok(None),
801        }
802    }
803
804    /// Create a resolver backed by an indexing database.
805    ///
806    /// Pointers and mirrors are hydrated from the database on construction.
807    /// Mutations are persisted through the database.
808    ///
809    /// # Errors
810    ///
811    /// Returns an error if the database cannot be read.
812    pub fn with_database(database: IndexingDatabase) -> Result<Self> {
813        let result = database.load_links()?;
814        let mut state = ResolverState::default();
815        for pointer in result.0 {
816            let key = (pointer.publisher, pointer.alias.clone());
817            let history = state.pointers.entry(key).or_default();
818            if let Some(current) = &history.current
819                && pointer.sequence <= current.sequence
820            {
821                continue;
822            }
823            if let Some(version) = &pointer.version {
824                history.versions.insert(version.clone(), pointer.clone());
825            }
826            history.current = Some(pointer);
827        }
828        for ticket_str in result.1 {
829            if let Ok(ticket) = ticket_str.parse::<iroh_blobs::ticket::BlobTicket>()
830                && let Ok(mirror) = Mirror::new(ticket)
831            {
832                let mirrors = state.mirrors.entry(mirror.hash).or_default();
833                if !mirrors.iter().any(|existing| existing.ticket == mirror.ticket) {
834                    mirrors.push(mirror);
835                }
836            }
837        }
838        for link in result.2 {
839            state.revoked.insert(link.revocation_key());
840        }
841        Ok(Self {
842            state: Arc::new(Mutex::new(state)),
843            database: Some(database),
844            provider_fetch: None,
845        })
846    }
847
848    /// Register a direct mirror ticket for a hash.
849    ///
850    /// # Errors
851    ///
852    /// Returns an error if the ticket is malformed.
853    pub fn register_mirror(&self, ticket: BlobTicket) -> Result<()> {
854        let mirror = Mirror::new(ticket)?;
855        let mut state = self.lock_state()?;
856        let mirrors = state.mirrors.entry(mirror.hash).or_default();
857        if !mirrors.iter().any(|existing| existing.ticket == mirror.ticket) {
858            mirrors.push(mirror.clone());
859            if let Some(ref database) = self.database {
860                let _ = database.upsert_link_mirror(&mirror.ticket.to_string());
861            }
862        }
863        drop(state);
864        Ok(())
865    }
866
867    /// Return registered direct mirror tickets in registration order.
868    ///
869    /// # Errors
870    ///
871    /// Returns an error if the resolver state lock is poisoned.
872    pub fn mirrors(&self, hash: Hash) -> Result<Vec<BlobTicket>> {
873        let state = self.lock_state()?;
874        Ok(state
875            .mirrors
876            .get(&hash)
877            .map(|mirrors| mirrors.iter().map(|mirror| mirror.ticket.clone()).collect())
878            .unwrap_or_default())
879    }
880
881    /// Accept a signed mutable pointer, enforcing monotonic sequence.
882    ///
883    /// # Errors
884    ///
885    /// Returns an error if the pointer is unsigned, invalid, or older than
886    /// the current pointer.
887    pub fn publish(&self, pointer: &MutablePointer) -> Result<()> {
888        pointer.verify_signature()?;
889        let key = (pointer.publisher, pointer.alias.clone());
890        let mut state = self.lock_state()?;
891        let history = state.pointers.entry(key).or_default();
892        if let Some(current) = &history.current
893            && pointer.sequence <= current.sequence
894        {
895            return Err(SyncwebError::InvalidConfig(format!(
896                "mutable link sequence {} must be greater than {}",
897                pointer.sequence, current.sequence
898            )));
899        }
900        if let Some(version) = &pointer.version {
901            if history.versions.contains_key(version) {
902                return Err(SyncwebError::InvalidConfig(format!(
903                    "mutable link version {version:?} is already published"
904                )));
905            }
906            history.versions.insert(version.clone(), pointer.clone());
907        }
908        history.current = Some(pointer.clone());
909        if let Some(ref database) = self.database {
910            let _ = database.upsert_link_pointer(pointer);
911        }
912        drop(state);
913        Ok(())
914    }
915
916    /// Return the current pointer for a mutable name.
917    ///
918    /// # Errors
919    ///
920    /// Returns an error if the link is invalid or the resolver state lock is
921    /// poisoned.
922    pub fn current_pointer(&self, link: &NameLink) -> Result<Option<MutablePointer>> {
923        link.validate()?;
924        let state = self.lock_state()?;
925        Ok(state
926            .pointers
927            .get(&(link.publisher, link.alias.clone()))
928            .and_then(|history| history.current.clone()))
929    }
930
931    /// Revoke a private capability.
932    ///
933    /// # Errors
934    ///
935    /// Returns an error if the link is invalid or the resolver state lock is
936    /// poisoned.
937    pub fn revoke(&self, link: &PrivateLink) -> Result<()> {
938        link.validate()?;
939        self.lock_state()?.revoked.insert(link.revocation_key());
940        Ok(())
941    }
942
943    /// Return whether a private capability has been revoked.
944    ///
945    /// # Errors
946    ///
947    /// Returns an error if the link is invalid or the resolver state lock is
948    /// poisoned.
949    pub fn is_revoked(&self, link: &PrivateLink) -> Result<bool> {
950        link.validate()?;
951        Ok(self.lock_state()?.revoked.contains(&link.revocation_key()))
952    }
953
954    /// Resolve a link using the current time.
955    ///
956    /// # Errors
957    ///
958    /// Returns an error if the link is invalid, unavailable, expired, revoked,
959    /// or has an invalid provider.
960    pub fn resolve(&self, link: &Link) -> Result<LinkResolution> {
961        self.resolve_with_options(link, &ResolveOptions::default())
962    }
963
964    /// Resolve a link at an explicit timestamp.
965    ///
966    /// # Errors
967    ///
968    /// Returns an error if the link is invalid, unavailable, expired, revoked,
969    /// or has an invalid provider.
970    pub fn resolve_at(&self, link: &Link, now: u64) -> Result<LinkResolution> {
971        self.resolve_with_options(link, &ResolveOptions::at(now))
972    }
973
974    /// Resolve a mutable link to an exact semantic version.
975    ///
976    /// # Errors
977    ///
978    /// Returns an error if the link or requested version is unavailable.
979    pub fn resolve_version(&self, link: &NameLink, version: &str) -> Result<LinkResolution> {
980        self.resolve_with_options(&Link::Name(link.clone()), &ResolveOptions::version(version))
981    }
982
983    /// Resolve a link using explicit version and expiry options.
984    ///
985    /// # Errors
986    ///
987    /// Returns an error if the link is invalid, unavailable, expired, revoked,
988    /// or has an invalid provider.
989    pub fn resolve_with_options(&self, link: &Link, options: &ResolveOptions) -> Result<LinkResolution> {
990        let now = options.now.unwrap_or_else(crate::parsing::current_unix_secs);
991        let state = self.lock_state()?;
992        let resolution = match link {
993            Link::Content(content) => {
994                content.validate()?;
995                resolution_for_hash(content.hash, None, None, &state)
996            }
997            Link::Private(private) => {
998                private.validate()?;
999                if private.is_expired_at(now) {
1000                    return Err(SyncwebError::InvalidConfig("private link has expired".to_owned()));
1001                }
1002                if state.revoked.contains(&private.revocation_key()) {
1003                    return Err(SyncwebError::InvalidConfig("private link has been revoked".to_owned()));
1004                }
1005                resolution_for_hash(private.manifest, None, None, &state)
1006            }
1007            Link::Name(name) => {
1008                name.validate()?;
1009                let history = state
1010                    .pointers
1011                    .get(&(name.publisher, name.alias.clone()))
1012                    .ok_or_else(|| SyncwebError::InvalidConfig("mutable link has no published pointer".to_owned()))?;
1013                let pointer = if let Some(version) = &options.version {
1014                    history.versions.get(version).ok_or_else(|| {
1015                        SyncwebError::InvalidConfig(format!("mutable link version {version:?} not found"))
1016                    })?
1017                } else {
1018                    history
1019                        .current
1020                        .as_ref()
1021                        .ok_or_else(|| SyncwebError::InvalidConfig("mutable link has no current pointer".to_owned()))?
1022                };
1023                resolution_for_hash(
1024                    pointer.manifest,
1025                    pointer.version.clone(),
1026                    Some(pointer.sequence),
1027                    &state,
1028                )
1029            }
1030        };
1031        drop(state);
1032        Ok(resolution)
1033    }
1034
1035    /// Parse and resolve a link URI.
1036    ///
1037    /// # Errors
1038    ///
1039    /// Returns an error if the URI cannot be parsed or the link cannot be
1040    /// resolved.
1041    pub fn resolve_uri(&self, uri: &str) -> Result<LinkResolution> {
1042        let link = uri.parse::<Link>()?;
1043        self.resolve(&link)
1044    }
1045
1046    /// Resolve and fetch a link, trying each provider and mirror in order.
1047    ///
1048    /// # Errors
1049    ///
1050    /// Returns the first successful fetch result, or an error if resolution
1051    /// fails or every provider fails.
1052    pub fn fetch_with_mirrors<T, Fetch>(&self, link: &Link, fetch: Fetch) -> Result<T>
1053    where
1054        Fetch: FnMut(&BlobTicket) -> Result<T>,
1055    {
1056        self.resolve(link)?.fetch_with(fetch)
1057    }
1058
1059    fn lock_state(&self) -> Result<std::sync::MutexGuard<'_, ResolverState>> {
1060        self.state
1061            .lock()
1062            .map_err(|error| SyncwebError::operation("link resolver state lock poisoned", error))
1063    }
1064}
1065
1066/// A persisted link pointer, mirror, and revocation store.
1067///
1068/// Owns the stable-link rows of an [`IndexingDatabase`] and keeps a hydrated
1069/// [`LinkResolver`] in sync so CLI handlers only render results.
1070#[derive(Clone)]
1071pub struct LinkStore {
1072    database: IndexingDatabase,
1073    resolver: LinkResolver,
1074}
1075
1076impl LinkStore {
1077    /// Open a link store backed by an existing indexing database.
1078    ///
1079    /// # Errors
1080    ///
1081    /// Returns an error if the persisted links cannot be read.
1082    pub fn with_database(database: IndexingDatabase) -> Result<Self> {
1083        let resolver = LinkResolver::with_database(database.clone())?;
1084        Ok(Self { database, resolver })
1085    }
1086
1087    /// Open a link store backed by the database at `path`.
1088    ///
1089    /// # Errors
1090    ///
1091    /// Returns an error if the database cannot be opened or its links read.
1092    pub fn open(path: impl AsRef<Path>) -> Result<Self> {
1093        Self::with_database(IndexingDatabase::open(path)?)
1094    }
1095
1096    /// Return the underlying indexing database.
1097    #[must_use]
1098    pub const fn database(&self) -> &IndexingDatabase {
1099        &self.database
1100    }
1101
1102    /// Return the hydrated resolver.
1103    #[must_use]
1104    pub const fn resolver(&self) -> &LinkResolver {
1105        &self.resolver
1106    }
1107
1108    /// Create an immutable content link.
1109    #[must_use]
1110    pub const fn create_content_link(&self, hash: Hash) -> Link {
1111        Link::Content(ContentLink::new(hash))
1112    }
1113
1114    /// Create a private capability link and clear any stale revocation.
1115    ///
1116    /// # Errors
1117    ///
1118    /// Returns an error if the capability cannot be generated or persisted.
1119    pub fn create_private_link(&self, hash: Hash, expires: Option<u64>) -> Result<Link> {
1120        let expires_at = expires
1121            .unwrap_or_else(|| crate::parsing::current_unix_secs().saturating_add(DEFAULT_PRIVATE_LINK_TTL));
1122        let link = PrivateLink::generate(hash, expires_at)?;
1123        let (pointers, mirrors, revoked) = self.database.load_links()?;
1124        let retained = revoked
1125            .iter()
1126            .filter(|existing| **existing != link)
1127            .cloned()
1128            .collect::<Vec<_>>();
1129        if retained.len() != revoked.len() {
1130            self.database.save_links(&pointers, &mirrors, &retained)?;
1131        }
1132        Ok(Link::Private(link))
1133    }
1134
1135    /// Create and sign a mutable name link, advancing the pointer sequence.
1136    ///
1137    /// With `sequence == 0` the next sequence is derived from the persisted
1138    /// pointer history for the same publisher and alias.
1139    ///
1140    /// # Errors
1141    ///
1142    /// Returns an error if the pointer is invalid, cannot be signed, or is not
1143    /// monotonically newer than the current pointer.
1144    pub fn create_named_link(
1145        &self,
1146        identity: &IdentityManager,
1147        alias_input: impl Into<String>,
1148        hash: Hash,
1149        version: Option<String>,
1150        sequence: u64,
1151    ) -> Result<Link> {
1152        let alias = alias_input.into();
1153        let (pointers, _, _) = self.database.load_links()?;
1154        let pointer_sequence = if sequence == 0 {
1155            pointers
1156                .iter()
1157                .filter(|pointer| pointer.publisher == identity.node_id() && pointer.alias == alias)
1158                .map(|pointer| pointer.sequence)
1159                .max()
1160                .unwrap_or(0)
1161                .saturating_add(1)
1162        } else {
1163            sequence
1164        };
1165        let signing_key = SigningKey::from_bytes(&identity.secret_key().to_bytes());
1166        let mut pointer = MutablePointer::signed_with_secret_key(
1167            identity.node_id(),
1168            alias,
1169            hash,
1170            pointer_sequence,
1171            identity.secret_key(),
1172        )?;
1173        if let Some(version_value) = version {
1174            pointer = pointer.with_version(version_value);
1175            pointer.sign(&signing_key)?;
1176        }
1177        let link = pointer.link()?;
1178        self.resolver.publish(&pointer)?;
1179        Ok(Link::Name(link))
1180    }
1181
1182    /// Revoke a private capability and persist the revocation.
1183    ///
1184    /// # Errors
1185    ///
1186    /// Returns an error if the link is invalid or cannot be persisted.
1187    pub fn revoke(&self, private: &PrivateLink) -> Result<()> {
1188        self.resolver.revoke(private)?;
1189        let (pointers, mirrors, revoked) = self.database.load_links()?;
1190        if !revoked.contains(private) {
1191            let mut updated = revoked;
1192            updated.push(private.clone());
1193            self.database.save_links(&pointers, &mirrors, &updated)?;
1194        }
1195        Ok(())
1196    }
1197
1198    /// Register a provider ticket as a mirror for its content hash.
1199    ///
1200    /// # Errors
1201    ///
1202    /// Returns an error if the ticket is malformed or cannot be persisted.
1203    pub fn register_mirror(&self, ticket: BlobTicket) -> Result<()> {
1204        self.resolver.register_mirror(ticket)
1205    }
1206
1207    /// Publish a link into a managed folder document under `sys/links/`.
1208    ///
1209    /// # Errors
1210    ///
1211    /// Returns an error if the folder cannot be opened or the link payload
1212    /// cannot be serialized or written.
1213    pub async fn publish_link(&self, node: &IrohNode, namespace: &str, link: &Link) -> Result<()> {
1214        let namespace_id = namespace
1215            .parse::<NamespaceId>()
1216            .map_err(|error| SyncwebError::operation("invalid publish namespace", error))?;
1217        let folder = FolderManager::new(node).get(namespace_id).await?;
1218        match link {
1219            Link::Name(name_link) => {
1220                let (pointers, _, _) = self.database.load_links()?;
1221                if let Some(pointer) = pointers
1222                    .iter()
1223                    .find(|p| p.publisher == name_link.publisher && p.alias == name_link.alias)
1224                {
1225                    let payload = serde_json::to_vec(pointer)
1226                        .map_err(|error| SyncwebError::operation("failed to serialize mutable pointer", error))?;
1227                    folder
1228                        .set_blob(format!("sys/links/mutable/{}", pointer.alias), payload)
1229                        .await?;
1230                    tracing::info!(alias = %pointer.alias, namespace = %namespace_id, "published mutable pointer to folder");
1231                }
1232            }
1233            Link::Private(private_link) => {
1234                let payload = serde_json::to_vec(private_link)
1235                    .map_err(|error| SyncwebError::operation("failed to serialize private link", error))?;
1236                folder
1237                    .set_blob(
1238                        format!("sys/links/private/{}", hex::encode(private_link.capability)),
1239                        payload,
1240                    )
1241                    .await?;
1242                tracing::info!(namespace = %namespace_id, "published private link to folder");
1243            }
1244            Link::Content(_) => {
1245                tracing::warn!("--publish has no effect on immutable content links");
1246            }
1247        }
1248        Ok(())
1249    }
1250
1251    /// Resolve a collection selector to its content hash.
1252    ///
1253    /// Accepts a raw hash, a stable link, or a local file/directory path.
1254    ///
1255    /// # Errors
1256    ///
1257    /// Returns an error if the selector is a link that cannot be resolved.
1258    pub fn collection_hash(&self, collection: &str) -> Result<Option<Hash>> {
1259        if let Ok(hash) = collection.parse() {
1260            return Ok(Some(hash));
1261        }
1262        if let Ok(parsed_link) = collection.parse::<Link>() {
1263            return Ok(Some(match parsed_link {
1264                Link::Content(content_link) => content_link.hash,
1265                Link::Private(private_link) => private_link.manifest,
1266                Link::Name(_) => self.resolver.resolve(&parsed_link)?.manifest,
1267            }));
1268        }
1269        if Path::new(collection).exists() {
1270            return Ok(Some(hash_source(Path::new(collection))?));
1271        }
1272        Ok(None)
1273    }
1274}
1275
1276/// Hash a file or directory tree with a stable, content-addressed scheme.
1277///
1278/// # Errors
1279///
1280/// Returns an error if the source does not exist, is neither a file nor a
1281/// directory, or cannot be read.
1282pub fn hash_source(source: &Path) -> Result<Hash> {
1283    if source.is_file() {
1284        let bytes = std::fs::read(source)?;
1285        return Ok(Hash::from_bytes(*blake3::hash(&bytes).as_bytes()));
1286    }
1287    if let Ok(hash) = source.to_string_lossy().parse() {
1288        return Ok(hash);
1289    }
1290    if !source.is_dir() {
1291        return Err(SyncwebError::InvalidConfig(format!(
1292            "link source does not exist or is not a file/directory: {}",
1293            source.display()
1294        )));
1295    }
1296    let mut files = Vec::new();
1297    collect_files(source, source, &mut files)?;
1298    files.sort();
1299    let mut hasher = blake3::Hasher::new();
1300    for relative in files {
1301        let bytes = std::fs::read(source.join(&relative))?;
1302        hasher.update(relative.to_string_lossy().as_bytes());
1303        hasher.update(&[0]);
1304        hasher.update(&bytes);
1305        hasher.update(&[0]);
1306    }
1307    Ok(Hash::from_bytes(*hasher.finalize().as_bytes()))
1308}
1309
1310/// Collect the relative paths of every regular file under `current`.
1311///
1312/// # Errors
1313///
1314/// Returns an error if a directory cannot be read.
1315pub fn collect_files(root: &Path, current: &Path, output: &mut Vec<PathBuf>) -> Result<()> {
1316    for directory_entry_result in std::fs::read_dir(current)? {
1317        let directory_entry = directory_entry_result?;
1318        let path = directory_entry.path();
1319        if path.is_dir() {
1320            collect_files(root, &path, output)?;
1321        } else if path.is_file() {
1322            output.push(
1323                path.strip_prefix(root)
1324                    .map_err(|error| SyncwebError::operation("failed to relativize source path", error))?
1325                    .to_path_buf(),
1326            );
1327        } else {
1328            // ignore symlinks and other non-file entries
1329        }
1330    }
1331    Ok(())
1332}
1333
1334/// Try provider tickets in order until one succeeds.
1335///
1336/// # Errors
1337///
1338/// Returns an error when no ticket is supplied or every fetch attempt fails.
1339pub fn fetch_from_mirrors<T, Fetch>(tickets: &[BlobTicket], fetch: &mut Fetch) -> Result<T>
1340where
1341    Fetch: FnMut(&BlobTicket) -> Result<T>,
1342{
1343    if tickets.is_empty() {
1344        return Err(SyncwebError::InvalidConfig("link has no provider tickets".to_owned()));
1345    }
1346    let mut failures = Vec::new();
1347    for ticket in tickets {
1348        match fetch(ticket) {
1349            Ok(value) => return Ok(value),
1350            Err(error) => failures.push(error.to_string()),
1351        }
1352    }
1353    Err(SyncwebError::InvalidConfig(format!(
1354        "all mirror providers failed: {}",
1355        failures.join("; ")
1356    )))
1357}
1358
1359fn resolution_for_hash(
1360    manifest: Hash,
1361    version: Option<String>,
1362    sequence: Option<u64>,
1363    state: &ResolverState,
1364) -> LinkResolution {
1365    let mut tickets = Vec::new();
1366    if let Some(mirrors) = state.mirrors.get(&manifest) {
1367        for mirror in mirrors {
1368            if !tickets.contains(&mirror.ticket) {
1369                tickets.push(mirror.ticket.clone());
1370            }
1371        }
1372    }
1373    LinkResolution {
1374        manifest,
1375        version,
1376        sequence,
1377        tickets,
1378    }
1379}
1380
1381fn validate_path_segment(value: &str, field: &str) -> Result<()> {
1382    if value.is_empty() || value == "." || value == ".." {
1383        return Err(SyncwebError::InvalidConfig(format!(
1384            "{field} cannot be empty or dot-like"
1385        )));
1386    }
1387    if value
1388        .chars()
1389        .any(|character| character.is_control() || matches!(character, '/' | '?' | '#' | '%'))
1390    {
1391        return Err(SyncwebError::InvalidConfig(format!("{field} contains URI delimiters")));
1392    }
1393    Ok(())
1394}
1395
1396fn next_part<'a>(parts: &mut Split<'a, char>, field: &str) -> Result<&'a str> {
1397    let part = parts
1398        .next()
1399        .ok_or_else(|| SyncwebError::InvalidConfig(format!("{field} is missing")))?;
1400    if part.is_empty() {
1401        return Err(SyncwebError::InvalidConfig(format!("{field} is empty")));
1402    }
1403    Ok(part)
1404}
1405
1406#[cfg(test)]
1407mod tests {
1408    use super::*;
1409    use iroh::{EndpointAddr, SecretKey};
1410
1411    fn ticket(seed: u8, hash: Hash) -> BlobTicket {
1412        let secret = SecretKey::from_bytes(&[seed; 32]);
1413        crate::node::blob_store::raw_blob_ticket(EndpointAddr::new(secret.public()), hash)
1414    }
1415
1416    fn pointer(seed: u8, alias: &str, hash: Hash, sequence: u64, version: &str) -> Result<MutablePointer> {
1417        let secret = SecretKey::from_bytes(&[seed; 32]);
1418        let mut pointer = MutablePointer::new(secret.public(), alias, hash, sequence)?.with_version(version);
1419        pointer.sign(&SigningKey::from_bytes(&secret.to_bytes()))?;
1420        Ok(pointer)
1421    }
1422
1423    #[test]
1424    fn immutable_links_round_trip() {
1425        let hash = Hash::from_bytes([7; 32]);
1426        let link = ContentLink::new(hash);
1427        assert_eq!(link.to_string(), format!("syncweb://content/{hash}"));
1428        assert_eq!(
1429            link.to_string()
1430                .parse::<ContentLink>()
1431                .expect("content link should parse"),
1432            link
1433        );
1434        assert_eq!(
1435            link.to_string().parse::<Link>().expect("link should parse"),
1436            Link::Content(link)
1437        );
1438    }
1439
1440    #[test]
1441    fn signed_names_are_monotonic_and_version_pinnable() {
1442        let hash_one = Hash::from_bytes([1; 32]);
1443        let hash_two = Hash::from_bytes([2; 32]);
1444        let resolver = LinkResolver::new();
1445        let first = pointer(1, "dataset", hash_one, 1, "1.0.0").expect("pointer should build");
1446        let name = first.link().expect("pointer should have a link");
1447        resolver.publish(&first).expect("first pointer should publish");
1448        assert_eq!(
1449            resolver
1450                .resolve(&Link::Name(name.clone()))
1451                .expect("name should resolve")
1452                .manifest,
1453            hash_one
1454        );
1455        let pinned = resolver
1456            .resolve_version(&name, "1.0.0")
1457            .expect("version should resolve");
1458        assert_eq!(pinned.manifest, hash_one);
1459        let second = pointer(1, "dataset", hash_two, 2, "2.0.0").expect("pointer should build");
1460        resolver.publish(&second).expect("second pointer should publish");
1461        assert_eq!(
1462            resolver
1463                .resolve(&Link::Name(name.clone()))
1464                .expect("name should resolve")
1465                .manifest,
1466            hash_two
1467        );
1468        assert!(resolver.publish(&first).is_err());
1469        assert_eq!(
1470            resolver
1471                .resolve_version(&name, "1.0.0")
1472                .expect("version should resolve")
1473                .manifest,
1474            hash_one
1475        );
1476    }
1477
1478    #[test]
1479    fn private_links_expire_and_revoke() {
1480        let hash = Hash::from_bytes([3; 32]);
1481        let cap: [u8; 32] = [1_u8; 32];
1482        let link = PrivateLink::new(hash, cap, 20).expect("private link should build");
1483        let resolver = LinkResolver::new();
1484        assert_eq!(
1485            resolver
1486                .resolve_at(&Link::Private(link.clone()), 10)
1487                .expect("private link should resolve")
1488                .manifest,
1489            hash
1490        );
1491        resolver.revoke(&link).expect("private link should revoke");
1492        assert!(resolver.resolve_at(&Link::Private(link.clone()), 10).is_err());
1493        assert!(resolver.resolve_at(&Link::Private(link), 20).is_err());
1494    }
1495
1496    #[test]
1497    fn private_link_generates_random_capability() {
1498        let hash = Hash::from_bytes([4; 32]);
1499        let link = PrivateLink::generate(hash, 100).expect("should generate");
1500        assert_ne!(link.capability, [0_u8; 32], "capability should not be zeroed");
1501        assert_eq!(link.manifest, hash);
1502        assert_eq!(link.expires_at, 100);
1503        // Two generated links should differ
1504        let link2 = PrivateLink::generate(hash, 100).expect("should generate");
1505        assert_ne!(link.capability, link2.capability, "capabilities should be random");
1506    }
1507
1508    #[test]
1509    fn private_link_uri_round_trip() {
1510        let hash = Hash::from_bytes([5; 32]);
1511        let cap: [u8; 32] = [
1512            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12,
1513            0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
1514        ];
1515        let link = PrivateLink::new(hash, cap, 200).expect("should build");
1516        let uri = link.to_string();
1517        assert!(uri.starts_with("syncweb://private/"));
1518        assert!(uri.contains("?expires=200"));
1519        let parsed: PrivateLink = uri.parse().expect("should parse");
1520        assert_eq!(parsed.manifest, hash);
1521        assert_eq!(parsed.capability, cap);
1522        assert_eq!(parsed.expires_at, 200);
1523    }
1524
1525    #[test]
1526    fn mirror_fallback_tries_alternates() {
1527        let hash = Hash::from_bytes([4; 32]);
1528        let resolver = LinkResolver::new();
1529        let first = ticket(4, hash);
1530        let second = ticket(5, hash);
1531        resolver
1532            .register_mirror(first.clone())
1533            .expect("first mirror should register");
1534        resolver.register_mirror(second).expect("second mirror should register");
1535        let link = Link::Content(ContentLink::new(hash));
1536        let value = resolver
1537            .fetch_with_mirrors(&link, |candidate| {
1538                if candidate == &first {
1539                    Err(SyncwebError::InvalidConfig("first mirror unavailable".to_owned()))
1540                } else {
1541                    Ok("second mirror")
1542                }
1543            })
1544            .expect("fallback should find a mirror");
1545        assert_eq!(value, "second mirror");
1546    }
1547}