1use std::{
10 collections::{BTreeMap, HashMap, HashSet},
11 fmt,
12 str::{FromStr, Split},
13 sync::{Arc, Mutex},
14 time::{SystemTime, UNIX_EPOCH},
15};
16
17use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
18use iroh::{PublicKey, SecretKey};
19use iroh_blobs::{Hash, ticket::BlobTicket};
20use serde::{Deserialize, Serialize};
21use uuid::Uuid;
22
23use crate::{
24 error::{Result, SyncwebError},
25 indexing::ProviderLease,
26};
27
28const LINK_SIGNATURE_CONTEXT: &[u8] = b"syncweb/name-pointer/v1\0";
29const LINK_SCHEME: &str = "syncweb://";
30
31#[must_use]
33pub fn current_epoch_seconds() -> u64 {
34 SystemTime::now()
35 .duration_since(UNIX_EPOCH)
36 .map_or(0, |duration| duration.as_secs())
37}
38
39#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
41#[non_exhaustive]
42pub struct ContentLink {
43 pub hash: Hash,
45}
46
47impl ContentLink {
48 #[must_use]
50 pub const fn new(hash: Hash) -> Self {
51 Self { hash }
52 }
53
54 #[must_use]
56 pub const fn hash(self) -> Hash {
57 self.hash
58 }
59
60 #[must_use]
62 pub const fn create(hash: Hash) -> Self {
63 Self::new(hash)
64 }
65
66 pub fn parse(value: &str) -> Result<Self> {
72 value.parse()
73 }
74
75 #[must_use]
77 pub fn uri(self) -> String {
78 self.to_string()
79 }
80
81 pub const fn validate(self) -> Result<()> {
88 Ok(())
89 }
90}
91
92impl fmt::Display for ContentLink {
93 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
94 write!(formatter, "{LINK_SCHEME}content/{}", self.hash)
95 }
96}
97
98impl FromStr for ContentLink {
99 type Err = SyncwebError;
100
101 fn from_str(value: &str) -> Result<Self> {
102 let hash_text = value
103 .strip_prefix("syncweb://content/")
104 .ok_or_else(|| SyncwebError::InvalidConfig("content link must use syncweb://content/".to_owned()))?;
105 if hash_text.is_empty() || hash_text.contains('/') || hash_text.contains('?') || hash_text.contains('#') {
106 return Err(SyncwebError::InvalidConfig(
107 "content link has an invalid hash".to_owned(),
108 ));
109 }
110 let hash = hash_text
111 .parse::<Hash>()
112 .map_err(|error| SyncwebError::InvalidConfig(format!("invalid content link hash: {error}")))?;
113 Ok(Self::new(hash))
114 }
115}
116
117pub type ImmutableLink = ContentLink;
119
120#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
122#[non_exhaustive]
123pub struct NameLink {
124 pub publisher: PublicKey,
126 pub alias: String,
128}
129
130impl NameLink {
131 pub fn new(publisher: PublicKey, alias: impl Into<String>) -> Result<Self> {
138 let link = Self {
139 publisher,
140 alias: alias.into(),
141 };
142 link.validate()?;
143 Ok(link)
144 }
145
146 pub fn create(publisher: PublicKey, alias: impl Into<String>) -> Result<Self> {
152 Self::new(publisher, alias)
153 }
154
155 pub fn parse(value: &str) -> Result<Self> {
161 value.parse()
162 }
163
164 #[must_use]
166 pub fn uri(&self) -> String {
167 self.to_string()
168 }
169
170 pub fn validate(&self) -> Result<()> {
176 validate_path_segment(&self.alias, "mutable link alias")
177 }
178}
179
180impl fmt::Display for NameLink {
181 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
182 write!(formatter, "{LINK_SCHEME}name/{}/{}", self.publisher, self.alias)
183 }
184}
185
186impl FromStr for NameLink {
187 type Err = SyncwebError;
188
189 fn from_str(value: &str) -> Result<Self> {
190 let remainder = value
191 .strip_prefix("syncweb://name/")
192 .ok_or_else(|| SyncwebError::InvalidConfig("name link must use syncweb://name/".to_owned()))?;
193 let mut parts = remainder.split('/');
194 let publisher_text = next_part(&mut parts, "name link publisher")?;
195 let alias = next_part(&mut parts, "name link alias")?;
196 if parts.next().is_some() {
197 return Err(SyncwebError::InvalidConfig(
198 "name link has too many path segments".to_owned(),
199 ));
200 }
201 let publisher = publisher_text
202 .parse::<PublicKey>()
203 .map_err(|error| SyncwebError::InvalidIdentity(format!("invalid name link publisher: {error}")))?;
204 Self::new(publisher, alias)
205 }
206}
207
208pub type MutableLink = NameLink;
210
211#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
217#[non_exhaustive]
218pub struct MutablePointer {
219 pub publisher: PublicKey,
221 pub alias: String,
223 pub sequence: u64,
225 pub manifest: Hash,
227 #[serde(default)]
229 pub version: Option<String>,
230 #[serde(default)]
232 pub providers: Vec<ProviderLease>,
233 #[serde(default)]
235 pub signature: Option<String>,
236}
237
238impl MutablePointer {
239 pub fn new(publisher: PublicKey, alias: impl Into<String>, manifest: Hash, sequence: u64) -> Result<Self> {
247 Self::new_with_providers(publisher, alias, manifest, sequence, Vec::new())
248 }
249
250 pub fn new_with_providers(
256 publisher: PublicKey,
257 alias: impl Into<String>,
258 manifest: Hash,
259 sequence: u64,
260 providers: Vec<ProviderLease>,
261 ) -> Result<Self> {
262 let pointer = Self {
263 publisher,
264 alias: alias.into(),
265 sequence,
266 manifest,
267 version: None,
268 providers,
269 signature: None,
270 };
271 pointer.validate()?;
272 Ok(pointer)
273 }
274
275 pub fn signed(
282 publisher: PublicKey,
283 alias: impl Into<String>,
284 manifest: Hash,
285 sequence: u64,
286 signing_key: &SigningKey,
287 ) -> Result<Self> {
288 let mut pointer = Self::new(publisher, alias, manifest, sequence)?;
289 pointer.sign(signing_key)?;
290 Ok(pointer)
291 }
292
293 pub fn signed_with_secret_key(
300 publisher: PublicKey,
301 alias: impl Into<String>,
302 manifest: Hash,
303 sequence: u64,
304 secret_key: &SecretKey,
305 ) -> Result<Self> {
306 let signing_key = SigningKey::from_bytes(&secret_key.to_bytes());
307 Self::signed(publisher, alias, manifest, sequence, &signing_key)
308 }
309
310 #[must_use]
312 pub fn with_version(mut self, version: impl Into<String>) -> Self {
313 self.version = Some(version.into());
314 self.signature = None;
315 self
316 }
317
318 #[must_use]
320 pub fn with_provider(mut self, provider: ProviderLease) -> Self {
321 self.providers.push(provider);
322 self.signature = None;
323 self
324 }
325
326 #[must_use]
328 pub fn with_providers(mut self, providers: impl IntoIterator<Item = ProviderLease>) -> Self {
329 self.providers.extend(providers);
330 self.signature = None;
331 self
332 }
333
334 pub fn link(&self) -> Result<NameLink> {
340 NameLink::new(self.publisher, self.alias.clone())
341 }
342
343 #[must_use]
345 pub const fn manifest_hash(&self) -> Hash {
346 self.manifest
347 }
348
349 pub fn unsigned_bytes(&self) -> Result<Vec<u8>> {
355 let mut unsigned = self.clone();
356 unsigned.signature = None;
357 unsigned.validate()?;
358 let mut bytes = LINK_SIGNATURE_CONTEXT.to_vec();
359 let encoded = serde_json::to_vec(&unsigned)
360 .map_err(|error| SyncwebError::operation("failed to serialize mutable link pointer", error))?;
361 bytes.extend_from_slice(&encoded);
362 Ok(bytes)
363 }
364
365 pub fn sign(&mut self, signing_key: &SigningKey) -> Result<()> {
372 if signing_key.verifying_key().to_bytes() != *self.publisher.as_bytes() {
373 return Err(SyncwebError::InvalidIdentity(
374 "mutable link signer does not match publisher".to_owned(),
375 ));
376 }
377 self.validate()?;
378 self.signature = Some(hex::encode(signing_key.sign(&self.unsigned_bytes()?).to_bytes()));
379 Ok(())
380 }
381
382 pub fn verify_signature(&self) -> Result<()> {
389 self.validate()?;
390 let signature_text = self
391 .signature
392 .as_deref()
393 .ok_or_else(|| SyncwebError::InvalidConfig("mutable link pointer must be signed".to_owned()))?;
394 let signature_bytes = hex::decode(signature_text)
395 .map_err(|error| SyncwebError::InvalidConfig(format!("invalid mutable link signature: {error}")))?;
396 let signature = Signature::from_slice(&signature_bytes)
397 .map_err(|error| SyncwebError::InvalidConfig(format!("invalid mutable link signature: {error}")))?;
398 let key = VerifyingKey::from_bytes(self.publisher.as_bytes())
399 .map_err(|error| SyncwebError::InvalidIdentity(format!("invalid mutable link publisher: {error}")))?;
400 key.verify(&self.unsigned_bytes()?, &signature)
401 .map_err(|error| SyncwebError::InvalidConfig(format!("mutable link signature is invalid: {error}")))?;
402 for provider in &self.providers {
403 provider.verify_signature()?;
404 }
405 Ok(())
406 }
407
408 pub fn validate(&self) -> Result<()> {
414 validate_path_segment(&self.alias, "mutable link alias")?;
415 if self.sequence == 0 {
416 return Err(SyncwebError::InvalidConfig(
417 "mutable link sequence must be greater than zero".to_owned(),
418 ));
419 }
420 if let Some(version) = &self.version
421 && version.trim().is_empty()
422 {
423 return Err(SyncwebError::InvalidConfig(
424 "mutable link version cannot be empty".to_owned(),
425 ));
426 }
427 for provider in &self.providers {
428 provider.validate()?;
429 if provider.hash != self.manifest {
430 return Err(SyncwebError::InvalidTicket(
431 "mutable link provider lease does not match its manifest".to_owned(),
432 ));
433 }
434 }
435 Ok(())
436 }
437
438 pub fn to_bytes(&self) -> Result<Vec<u8>> {
445 self.verify_signature()?;
446 serde_json::to_vec(self)
447 .map_err(|error| SyncwebError::operation("failed to serialize mutable link pointer", error))
448 }
449
450 pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self> {
456 let pointer: Self = serde_json::from_slice(bytes.as_ref())
457 .map_err(|error| SyncwebError::operation("failed to deserialize mutable link pointer", error))?;
458 pointer.verify_signature()?;
459 Ok(pointer)
460 }
461}
462
463pub type SignedMutablePointer = MutablePointer;
465
466#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
468#[non_exhaustive]
469pub struct PrivateLink {
470 pub manifest: Hash,
472 pub capability: String,
474 pub expires_at: u64,
476}
477
478impl PrivateLink {
479 pub fn generate(manifest: Hash, expires_at: u64) -> Result<Self> {
486 let capability = format!("{}{}", Uuid::new_v4().simple(), Uuid::new_v4().simple());
487 Self::new(manifest, capability, expires_at)
488 }
489
490 pub fn create(manifest: Hash, expires_at: u64) -> Result<Self> {
496 Self::generate(manifest, expires_at)
497 }
498
499 pub fn parse(value: &str) -> Result<Self> {
506 value.parse()
507 }
508
509 pub fn new(manifest: Hash, capability: impl Into<String>, expires_at: u64) -> Result<Self> {
515 let link = Self {
516 manifest,
517 capability: capability.into(),
518 expires_at,
519 };
520 link.validate()?;
521 Ok(link)
522 }
523
524 #[must_use]
526 pub fn uri(&self) -> String {
527 self.to_string()
528 }
529
530 pub fn validate(&self) -> Result<()> {
537 if self.expires_at == 0 {
538 return Err(SyncwebError::InvalidConfig(
539 "private link expiration must be greater than zero".to_owned(),
540 ));
541 }
542 validate_path_segment(&self.capability, "private link capability")
543 }
544
545 #[must_use]
547 pub const fn is_expired_at(&self, now: u64) -> bool {
548 self.expires_at <= now
549 }
550
551 #[must_use]
553 pub fn revocation_key(&self) -> (Hash, String) {
554 (self.manifest, self.capability.clone())
555 }
556}
557
558impl fmt::Display for PrivateLink {
559 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
560 write!(
561 formatter,
562 "{LINK_SCHEME}private/{}/{}?expires={}",
563 self.manifest, self.capability, self.expires_at
564 )
565 }
566}
567
568impl FromStr for PrivateLink {
569 type Err = SyncwebError;
570
571 fn from_str(value: &str) -> Result<Self> {
572 let (path, query) = value
573 .strip_prefix("syncweb://private/")
574 .ok_or_else(|| SyncwebError::InvalidConfig("private link must use syncweb://private/".to_owned()))?
575 .split_once('?')
576 .ok_or_else(|| SyncwebError::InvalidConfig("private link must contain an expiration".to_owned()))?;
577 let mut parts = path.split('/');
578 let manifest = next_part(&mut parts, "private link manifest")?;
579 let capability = next_part(&mut parts, "private link capability")?;
580 if parts.next().is_some() {
581 return Err(SyncwebError::InvalidConfig(
582 "private link has too many path segments".to_owned(),
583 ));
584 }
585 let manifest_hash = manifest
586 .parse::<Hash>()
587 .map_err(|error| SyncwebError::InvalidConfig(format!("invalid private link manifest: {error}")))?;
588 let expires_text = query
589 .strip_prefix("expires=")
590 .ok_or_else(|| SyncwebError::InvalidConfig("private link query must contain expires".to_owned()))?;
591 let expires_at = expires_text
592 .parse::<u64>()
593 .map_err(|error| SyncwebError::InvalidConfig(format!("invalid private link expiration: {error}")))?;
594 Self::new(manifest_hash, capability, expires_at)
595 }
596}
597
598pub type CapabilityLink = PrivateLink;
600
601#[derive(Clone, Debug, Eq, PartialEq)]
603#[non_exhaustive]
604pub struct Mirror {
605 pub hash: Hash,
607 pub ticket: BlobTicket,
609}
610
611impl Mirror {
612 pub fn new(ticket: BlobTicket) -> Result<Self> {
618 let mirror = Self {
619 hash: ticket.hash(),
620 ticket,
621 };
622 mirror.validate()?;
623 Ok(mirror)
624 }
625
626 pub fn validate(&self) -> Result<()> {
632 if self.ticket.hash() != self.hash {
633 return Err(SyncwebError::InvalidTicket(
634 "mirror ticket does not match its content hash".to_owned(),
635 ));
636 }
637 Ok(())
638 }
639}
640
641#[derive(Clone, Debug, Eq, PartialEq)]
643#[non_exhaustive]
644pub struct LinkResolution {
645 pub manifest: Hash,
647 pub version: Option<String>,
649 pub sequence: Option<u64>,
651 pub providers: Vec<ProviderLease>,
653 pub tickets: Vec<BlobTicket>,
655}
656
657impl LinkResolution {
658 #[must_use]
660 pub const fn hash(&self) -> Hash {
661 self.manifest
662 }
663
664 #[must_use]
666 pub const fn manifest_hash(&self) -> Hash {
667 self.manifest
668 }
669
670 #[must_use]
672 pub fn provider_tickets(&self) -> &[BlobTicket] {
673 &self.tickets
674 }
675
676 pub fn fetch_with<T, Fetch>(&self, mut fetch: Fetch) -> Result<T>
682 where
683 Fetch: FnMut(&BlobTicket) -> Result<T>,
684 {
685 fetch_from_mirrors(&self.tickets, &mut fetch)
686 }
687}
688
689pub type ResolvedLink = LinkResolution;
691
692#[derive(Clone, Debug, Eq, PartialEq, Hash)]
694#[non_exhaustive]
695pub enum Link {
696 Content(ContentLink),
698 Name(NameLink),
700 Private(PrivateLink),
702}
703
704pub type SyncwebLink = Link;
706
707impl Link {
708 pub fn parse(value: &str) -> Result<Self> {
714 value.parse()
715 }
716
717 #[must_use]
719 pub fn uri(&self) -> String {
720 self.to_string()
721 }
722}
723
724impl fmt::Display for Link {
725 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
726 match self {
727 Self::Content(link) => link.fmt(formatter),
728 Self::Name(link) => link.fmt(formatter),
729 Self::Private(link) => link.fmt(formatter),
730 }
731 }
732}
733
734impl FromStr for Link {
735 type Err = SyncwebError;
736
737 fn from_str(value: &str) -> Result<Self> {
738 if value.starts_with("syncweb://content/") {
739 return Ok(Self::Content(value.parse()?));
740 }
741 if value.starts_with("syncweb://name/") {
742 return Ok(Self::Name(value.parse()?));
743 }
744 if value.starts_with("syncweb://private/") {
745 return Ok(Self::Private(value.parse()?));
746 }
747 Err(SyncwebError::InvalidConfig(
748 "unsupported syncweb link; expected content, name, or private".to_owned(),
749 ))
750 }
751}
752
753#[derive(Clone, Debug, Default, Eq, PartialEq)]
755#[non_exhaustive]
756pub struct ResolveOptions {
757 pub version: Option<String>,
759 pub now: Option<u64>,
761}
762
763impl ResolveOptions {
764 #[must_use]
766 pub fn version(version: impl Into<String>) -> Self {
767 Self {
768 version: Some(version.into()),
769 now: None,
770 }
771 }
772
773 #[must_use]
775 pub const fn at(now: u64) -> Self {
776 Self {
777 version: None,
778 now: Some(now),
779 }
780 }
781
782 #[must_use]
784 pub fn with_version(mut self, version: impl Into<String>) -> Self {
785 self.version = Some(version.into());
786 self
787 }
788}
789
790#[derive(Clone, Debug, Default)]
791struct ResolverState {
792 mirrors: HashMap<Hash, Vec<Mirror>>,
793 pointers: HashMap<(PublicKey, String), PointerHistory>,
794 revoked: HashSet<(Hash, String)>,
795}
796
797#[derive(Clone, Debug, Default)]
798struct PointerHistory {
799 current: Option<MutablePointer>,
800 versions: BTreeMap<String, MutablePointer>,
801}
802
803#[derive(Clone, Debug, Default)]
805pub struct LinkResolver {
806 state: Arc<Mutex<ResolverState>>,
807}
808
809impl LinkResolver {
810 #[must_use]
812 pub fn new() -> Self {
813 Self::default()
814 }
815
816 pub fn register_mirror(&self, ticket: BlobTicket) -> Result<()> {
822 let mirror = Mirror::new(ticket)?;
823 let mut state = self.lock_state()?;
824 let mirrors = state.mirrors.entry(mirror.hash).or_default();
825 if !mirrors.iter().any(|existing| existing.ticket == mirror.ticket) {
826 mirrors.push(mirror);
827 }
828 drop(state);
829 Ok(())
830 }
831
832 pub fn add_mirror(&self, ticket: BlobTicket) -> Result<()> {
838 self.register_mirror(ticket)
839 }
840
841 pub fn register_provider_ticket(&self, ticket: BlobTicket) -> Result<()> {
847 self.register_mirror(ticket)
848 }
849
850 pub fn register_provider_lease(&self, lease: ProviderLease) -> Result<()> {
856 let now = current_epoch_seconds();
857 let validated_lease = checked_lease(lease, now)?;
858 let ticket = lease_ticket(&validated_lease)?;
859 let mut state = self.lock_state()?;
860 let mirrors = state.mirrors.entry(validated_lease.hash).or_default();
861 if !mirrors.iter().any(|existing| existing.ticket == ticket) {
862 mirrors.push(Mirror {
863 hash: validated_lease.hash,
864 ticket,
865 });
866 }
867 drop(state);
868 Ok(())
869 }
870
871 pub fn mirrors(&self, hash: Hash) -> Result<Vec<BlobTicket>> {
877 let state = self.lock_state()?;
878 Ok(state
879 .mirrors
880 .get(&hash)
881 .map(|mirrors| mirrors.iter().map(|mirror| mirror.ticket.clone()).collect())
882 .unwrap_or_default())
883 }
884
885 pub fn publish(&self, pointer: MutablePointer) -> Result<()> {
892 pointer.verify_signature()?;
893 let key = (pointer.publisher, pointer.alias.clone());
894 let mut state = self.lock_state()?;
895 let history = state.pointers.entry(key).or_default();
896 if let Some(current) = &history.current
897 && pointer.sequence <= current.sequence
898 {
899 return Err(SyncwebError::InvalidConfig(format!(
900 "mutable link sequence {} must be greater than {}",
901 pointer.sequence, current.sequence
902 )));
903 }
904 if let Some(version) = &pointer.version {
905 if history.versions.contains_key(version) {
906 return Err(SyncwebError::InvalidConfig(format!(
907 "mutable link version {version:?} is already published"
908 )));
909 }
910 history.versions.insert(version.clone(), pointer.clone());
911 }
912 history.current = Some(pointer);
913 drop(state);
914 Ok(())
915 }
916
917 pub fn register_pointer(&self, pointer: MutablePointer) -> Result<()> {
923 self.publish(pointer)
924 }
925
926 pub fn update(&self, pointer: MutablePointer) -> Result<()> {
932 self.publish(pointer)
933 }
934
935 pub fn current_pointer(&self, link: &NameLink) -> Result<Option<MutablePointer>> {
942 link.validate()?;
943 let state = self.lock_state()?;
944 Ok(state
945 .pointers
946 .get(&(link.publisher, link.alias.clone()))
947 .and_then(|history| history.current.clone()))
948 }
949
950 pub fn revoke(&self, link: &PrivateLink) -> Result<()> {
957 link.validate()?;
958 self.lock_state()?.revoked.insert(link.revocation_key());
959 Ok(())
960 }
961
962 pub fn revoke_private(&self, link: &PrivateLink) -> Result<()> {
969 self.revoke(link)
970 }
971
972 pub fn is_revoked(&self, link: &PrivateLink) -> Result<bool> {
979 link.validate()?;
980 Ok(self.lock_state()?.revoked.contains(&link.revocation_key()))
981 }
982
983 pub fn resolve(&self, link: &Link) -> Result<LinkResolution> {
990 self.resolve_with_options(link, &ResolveOptions::default())
991 }
992
993 pub fn resolve_at(&self, link: &Link, now: u64) -> Result<LinkResolution> {
1000 self.resolve_with_options(link, &ResolveOptions::at(now))
1001 }
1002
1003 pub fn resolve_version(&self, link: &NameLink, version: &str) -> Result<LinkResolution> {
1009 self.resolve_with_options(&Link::Name(link.clone()), &ResolveOptions::version(version))
1010 }
1011
1012 pub fn resolve_with_options(&self, link: &Link, options: &ResolveOptions) -> Result<LinkResolution> {
1019 let now = options.now.unwrap_or_else(current_epoch_seconds);
1020 let state = self.lock_state()?;
1021 let resolution = match link {
1022 Link::Content(content) => {
1023 content.validate()?;
1024 resolution_for_hash(content.hash, None, None, Vec::new(), &state, now)
1025 }
1026 Link::Private(private) => {
1027 private.validate()?;
1028 if private.is_expired_at(now) {
1029 return Err(SyncwebError::InvalidConfig("private link has expired".to_owned()));
1030 }
1031 if state.revoked.contains(&private.revocation_key()) {
1032 return Err(SyncwebError::InvalidConfig("private link has been revoked".to_owned()));
1033 }
1034 resolution_for_hash(private.manifest, None, None, Vec::new(), &state, now)
1035 }
1036 Link::Name(name) => {
1037 name.validate()?;
1038 let history = state
1039 .pointers
1040 .get(&(name.publisher, name.alias.clone()))
1041 .ok_or_else(|| SyncwebError::InvalidConfig("mutable link has no published pointer".to_owned()))?;
1042 let pointer = if let Some(version) = &options.version {
1043 history.versions.get(version).ok_or_else(|| {
1044 SyncwebError::InvalidConfig(format!("mutable link version {version:?} not found"))
1045 })?
1046 } else {
1047 history
1048 .current
1049 .as_ref()
1050 .ok_or_else(|| SyncwebError::InvalidConfig("mutable link has no current pointer".to_owned()))?
1051 };
1052 resolution_for_hash(
1053 pointer.manifest,
1054 pointer.version.clone(),
1055 Some(pointer.sequence),
1056 pointer.providers.clone(),
1057 &state,
1058 now,
1059 )
1060 }
1061 };
1062 drop(state);
1063 resolution
1064 }
1065
1066 pub fn resolve_uri(&self, uri: &str) -> Result<LinkResolution> {
1073 let link = uri.parse::<Link>()?;
1074 self.resolve(&link)
1075 }
1076
1077 pub fn fetch_with_mirrors<T, Fetch>(&self, link: &Link, fetch: Fetch) -> Result<T>
1084 where
1085 Fetch: FnMut(&BlobTicket) -> Result<T>,
1086 {
1087 self.resolve(link)?.fetch_with(fetch)
1088 }
1089
1090 fn lock_state(&self) -> Result<std::sync::MutexGuard<'_, ResolverState>> {
1091 self.state
1092 .lock()
1093 .map_err(|error| SyncwebError::operation("link resolver state lock poisoned", error))
1094 }
1095}
1096
1097pub fn fetch_from_mirrors<T, Fetch>(tickets: &[BlobTicket], fetch: &mut Fetch) -> Result<T>
1103where
1104 Fetch: FnMut(&BlobTicket) -> Result<T>,
1105{
1106 if tickets.is_empty() {
1107 return Err(SyncwebError::InvalidConfig("link has no provider tickets".to_owned()));
1108 }
1109 let mut failures = Vec::new();
1110 for ticket in tickets {
1111 match fetch(ticket) {
1112 Ok(value) => return Ok(value),
1113 Err(error) => failures.push(error.to_string()),
1114 }
1115 }
1116 Err(SyncwebError::InvalidConfig(format!(
1117 "all mirror providers failed: {}",
1118 failures.join("; ")
1119 )))
1120}
1121
1122fn resolution_for_hash(
1123 manifest: Hash,
1124 version: Option<String>,
1125 sequence: Option<u64>,
1126 providers: Vec<ProviderLease>,
1127 state: &ResolverState,
1128 now: u64,
1129) -> Result<LinkResolution> {
1130 let mut tickets = Vec::new();
1131 let mut active_providers = Vec::new();
1132 for provider in providers {
1133 if provider.expires_at <= now {
1134 continue;
1135 }
1136 provider.verify_at(now)?;
1137 let ticket = lease_ticket(&provider)?;
1138 if !tickets.contains(&ticket) {
1139 tickets.push(ticket);
1140 }
1141 active_providers.push(provider);
1142 }
1143 if let Some(mirrors) = state.mirrors.get(&manifest) {
1144 for mirror in mirrors {
1145 if !tickets.contains(&mirror.ticket) {
1146 tickets.push(mirror.ticket.clone());
1147 }
1148 }
1149 }
1150 Ok(LinkResolution {
1151 manifest,
1152 version,
1153 sequence,
1154 providers: active_providers,
1155 tickets,
1156 })
1157}
1158
1159fn lease_ticket(lease: &ProviderLease) -> Result<BlobTicket> {
1160 lease
1161 .ticket
1162 .parse::<BlobTicket>()
1163 .map_err(|error| SyncwebError::InvalidTicket(format!("invalid provider ticket: {error}")))
1164}
1165
1166fn checked_lease(lease: ProviderLease, now: u64) -> Result<ProviderLease> {
1167 lease.verify_at(now)?;
1168 Ok(lease)
1169}
1170
1171fn validate_path_segment(value: &str, field: &str) -> Result<()> {
1172 if value.is_empty() || value == "." || value == ".." {
1173 return Err(SyncwebError::InvalidConfig(format!(
1174 "{field} cannot be empty or dot-like"
1175 )));
1176 }
1177 if value
1178 .chars()
1179 .any(|character| character.is_control() || matches!(character, '/' | '?' | '#' | '%'))
1180 {
1181 return Err(SyncwebError::InvalidConfig(format!("{field} contains URI delimiters")));
1182 }
1183 Ok(())
1184}
1185
1186fn next_part<'a>(parts: &mut Split<'a, char>, field: &str) -> Result<&'a str> {
1187 let part = parts
1188 .next()
1189 .ok_or_else(|| SyncwebError::InvalidConfig(format!("{field} is missing")))?;
1190 if part.is_empty() {
1191 return Err(SyncwebError::InvalidConfig(format!("{field} is empty")));
1192 }
1193 Ok(part)
1194}
1195
1196#[cfg(test)]
1197mod tests {
1198 use super::*;
1199 use iroh::{EndpointAddr, SecretKey};
1200 use iroh_blobs::BlobFormat;
1201
1202 fn ticket(seed: u8, hash: Hash) -> BlobTicket {
1203 let secret = SecretKey::from_bytes(&[seed; 32]);
1204 BlobTicket::new(EndpointAddr::new(secret.public()), hash, BlobFormat::Raw)
1205 }
1206
1207 fn pointer(seed: u8, alias: &str, hash: Hash, sequence: u64, version: &str) -> Result<MutablePointer> {
1208 let secret = SecretKey::from_bytes(&[seed; 32]);
1209 let mut pointer = MutablePointer::new(secret.public(), alias, hash, sequence)?.with_version(version);
1210 pointer.sign(&SigningKey::from_bytes(&secret.to_bytes()))?;
1211 Ok(pointer)
1212 }
1213
1214 #[test]
1215 fn immutable_links_round_trip() {
1216 let hash = Hash::from_bytes([7; 32]);
1217 let link = ContentLink::new(hash);
1218 assert_eq!(link.to_string(), format!("syncweb://content/{hash}"));
1219 assert_eq!(
1220 link.to_string()
1221 .parse::<ContentLink>()
1222 .expect("content link should parse"),
1223 link
1224 );
1225 assert_eq!(
1226 link.to_string().parse::<Link>().expect("link should parse"),
1227 Link::Content(link)
1228 );
1229 }
1230
1231 #[test]
1232 fn signed_names_are_monotonic_and_version_pinnable() {
1233 let hash_one = Hash::from_bytes([1; 32]);
1234 let hash_two = Hash::from_bytes([2; 32]);
1235 let resolver = LinkResolver::new();
1236 let first = pointer(1, "dataset", hash_one, 1, "1.0.0").expect("pointer should build");
1237 let name = first.link().expect("pointer should have a link");
1238 resolver.publish(first.clone()).expect("first pointer should publish");
1239 assert_eq!(
1240 resolver
1241 .resolve(&Link::Name(name.clone()))
1242 .expect("name should resolve")
1243 .manifest,
1244 hash_one
1245 );
1246 let pinned = resolver
1247 .resolve_version(&name, "1.0.0")
1248 .expect("version should resolve");
1249 assert_eq!(pinned.manifest, hash_one);
1250 let second = pointer(1, "dataset", hash_two, 2, "2.0.0").expect("pointer should build");
1251 resolver.publish(second).expect("second pointer should publish");
1252 assert_eq!(
1253 resolver
1254 .resolve(&Link::Name(name.clone()))
1255 .expect("name should resolve")
1256 .manifest,
1257 hash_two
1258 );
1259 assert!(resolver.publish(first).is_err());
1260 assert_eq!(
1261 resolver
1262 .resolve_version(&name, "1.0.0")
1263 .expect("version should resolve")
1264 .manifest,
1265 hash_one
1266 );
1267 }
1268
1269 #[test]
1270 fn mutable_resolution_includes_verified_provider_tickets() {
1271 let hash = Hash::from_bytes([8; 32]);
1272 let provider_secret = SecretKey::from_bytes(&[8; 32]);
1273 let provider_ticket = ticket(8, hash);
1274 let mut lease = ProviderLease::new_with_times(hash, provider_ticket.to_string(), 1, 0, u64::MAX)
1275 .expect("provider lease should build");
1276 lease
1277 .sign_with_secret_key(&provider_secret)
1278 .expect("provider lease should sign");
1279
1280 let publisher_secret = SecretKey::from_bytes(&[9; 32]);
1281 let mut pointer =
1282 MutablePointer::new_with_providers(publisher_secret.public(), "dataset", hash, 1, vec![lease])
1283 .expect("pointer should build")
1284 .with_version("1.0.0");
1285 pointer
1286 .sign(&SigningKey::from_bytes(&publisher_secret.to_bytes()))
1287 .expect("pointer should sign");
1288
1289 let resolver = LinkResolver::new();
1290 let name = pointer.link().expect("pointer should have a link");
1291 resolver.publish(pointer).expect("pointer should publish");
1292 let resolution = resolver.resolve(&Link::Name(name)).expect("name should resolve");
1293 assert_eq!(resolution.manifest_hash(), hash);
1294 assert_eq!(resolution.providers.len(), 1);
1295 assert_eq!(resolution.provider_tickets(), &[provider_ticket]);
1296 }
1297
1298 #[test]
1299 fn private_links_expire_and_revoke() {
1300 let hash = Hash::from_bytes([3; 32]);
1301 let link = PrivateLink::new(hash, "capability-token", 20).expect("private link should build");
1302 let resolver = LinkResolver::new();
1303 assert_eq!(
1304 resolver
1305 .resolve_at(&Link::Private(link.clone()), 10)
1306 .expect("private link should resolve")
1307 .manifest,
1308 hash
1309 );
1310 resolver.revoke(&link).expect("private link should revoke");
1311 assert!(resolver.resolve_at(&Link::Private(link.clone()), 10).is_err());
1312 assert!(resolver.resolve_at(&Link::Private(link), 20).is_err());
1313 }
1314
1315 #[test]
1316 fn mirror_fallback_tries_alternates() {
1317 let hash = Hash::from_bytes([4; 32]);
1318 let resolver = LinkResolver::new();
1319 let first = ticket(4, hash);
1320 let second = ticket(5, hash);
1321 resolver
1322 .register_mirror(first.clone())
1323 .expect("first mirror should register");
1324 resolver.register_mirror(second).expect("second mirror should register");
1325 let link = Link::Content(ContentLink::new(hash));
1326 let value = resolver
1327 .fetch_with_mirrors(&link, |candidate| {
1328 if candidate == &first {
1329 Err(SyncwebError::InvalidConfig("first mirror unavailable".to_owned()))
1330 } else {
1331 Ok("second mirror")
1332 }
1333 })
1334 .expect("fallback should find a mirror");
1335 assert_eq!(value, "second mirror");
1336 }
1337}