1use 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#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
37#[non_exhaustive]
38pub struct ContentLink {
39 pub hash: Hash,
41}
42
43impl ContentLink {
44 #[must_use]
46 pub const fn new(hash: Hash) -> Self {
47 Self { hash }
48 }
49
50 #[must_use]
52 pub const fn hash(self) -> Hash {
53 self.hash
54 }
55
56 pub fn parse(value: &str) -> Result<Self> {
62 value.parse()
63 }
64
65 #[must_use]
67 pub fn uri(self) -> String {
68 self.to_string()
69 }
70
71 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
107pub type ImmutableLink = ContentLink;
109
110#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
112#[non_exhaustive]
113pub struct NameLink {
114 pub publisher: PublicKey,
116 pub alias: String,
118}
119
120impl NameLink {
121 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 pub fn parse(value: &str) -> Result<Self> {
142 value.parse()
143 }
144
145 #[must_use]
147 pub fn uri(&self) -> String {
148 self.to_string()
149 }
150
151 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
189pub type MutableLink = NameLink;
191
192#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
198#[non_exhaustive]
199pub struct MutablePointer {
200 pub publisher: PublicKey,
202 pub alias: String,
204 pub sequence: u64,
206 pub manifest: Hash,
208 #[serde(default)]
210 pub version: Option<String>,
211 #[serde(default)]
213 pub signature: Option<String>,
214}
215
216impl MutablePointer {
217 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 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 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 #[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 pub fn link(&self) -> Result<NameLink> {
286 NameLink::new(self.publisher, self.alias.clone())
287 }
288
289 #[must_use]
291 pub const fn manifest_hash(&self) -> Hash {
292 self.manifest
293 }
294
295 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 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 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 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 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 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
394pub type SignedMutablePointer = MutablePointer;
396
397#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
399#[non_exhaustive]
400pub struct PrivateLink {
401 pub manifest: Hash,
403 pub capability: [u8; 32],
405 pub expires_at: u64,
407}
408
409impl PrivateLink {
410 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 pub fn parse(value: &str) -> Result<Self> {
428 value.parse()
429 }
430
431 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 #[must_use]
448 pub fn uri(&self) -> String {
449 self.to_string()
450 }
451
452 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 #[must_use]
468 pub const fn is_expired_at(&self, now: u64) -> bool {
469 self.expires_at <= now
470 }
471
472 #[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
526pub type CapabilityLink = PrivateLink;
528
529#[derive(Clone, Debug, Eq, PartialEq)]
531#[non_exhaustive]
532pub struct Mirror {
533 pub hash: Hash,
535 pub ticket: BlobTicket,
537}
538
539impl Mirror {
540 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 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#[derive(Clone, Debug, Eq, PartialEq)]
571#[non_exhaustive]
572pub struct LinkResolution {
573 pub manifest: Hash,
575 pub version: Option<String>,
577 pub sequence: Option<u64>,
579 pub tickets: Vec<BlobTicket>,
581}
582
583impl LinkResolution {
584 #[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 #[must_use]
597 pub const fn hash(&self) -> Hash {
598 self.manifest
599 }
600
601 #[must_use]
603 pub fn provider_tickets(&self) -> &[BlobTicket] {
604 &self.tickets
605 }
606
607 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
620pub type ResolvedLink = LinkResolution;
622
623#[derive(Clone, Debug, Eq, PartialEq, Hash)]
625#[non_exhaustive]
626pub enum Link {
627 Content(ContentLink),
629 Name(NameLink),
631 Private(PrivateLink),
633}
634
635impl Link {
636 pub fn parse(value: &str) -> Result<Self> {
642 value.parse()
643 }
644
645 #[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#[derive(Clone, Debug, Default, Eq, PartialEq)]
683#[non_exhaustive]
684pub struct ResolveOptions {
685 pub version: Option<String>,
687 pub now: Option<u64>,
689}
690
691impl ResolveOptions {
692 #[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 #[must_use]
703 pub const fn at(now: u64) -> Self {
704 Self {
705 version: None,
706 now: Some(now),
707 }
708 }
709
710 #[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
731pub type ProviderFetch =
733 Arc<dyn Fn(NameLink) -> Pin<Box<dyn Future<Output = Result<LinkResolution>> + Send>> + Send + Sync>;
734
735#[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 #[must_use]
765 pub fn new() -> Self {
766 Self::default()
767 }
768
769 #[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 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 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 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 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 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 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 pub fn revoke(&self, link: &PrivateLink) -> Result<()> {
938 link.validate()?;
939 self.lock_state()?.revoked.insert(link.revocation_key());
940 Ok(())
941 }
942
943 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 pub fn resolve(&self, link: &Link) -> Result<LinkResolution> {
961 self.resolve_with_options(link, &ResolveOptions::default())
962 }
963
964 pub fn resolve_at(&self, link: &Link, now: u64) -> Result<LinkResolution> {
971 self.resolve_with_options(link, &ResolveOptions::at(now))
972 }
973
974 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 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 pub fn resolve_uri(&self, uri: &str) -> Result<LinkResolution> {
1042 let link = uri.parse::<Link>()?;
1043 self.resolve(&link)
1044 }
1045
1046 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#[derive(Clone)]
1071pub struct LinkStore {
1072 database: IndexingDatabase,
1073 resolver: LinkResolver,
1074}
1075
1076impl LinkStore {
1077 pub fn with_database(database: IndexingDatabase) -> Result<Self> {
1083 let resolver = LinkResolver::with_database(database.clone())?;
1084 Ok(Self { database, resolver })
1085 }
1086
1087 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
1093 Self::with_database(IndexingDatabase::open(path)?)
1094 }
1095
1096 #[must_use]
1098 pub const fn database(&self) -> &IndexingDatabase {
1099 &self.database
1100 }
1101
1102 #[must_use]
1104 pub const fn resolver(&self) -> &LinkResolver {
1105 &self.resolver
1106 }
1107
1108 #[must_use]
1110 pub const fn create_content_link(&self, hash: Hash) -> Link {
1111 Link::Content(ContentLink::new(hash))
1112 }
1113
1114 pub fn create_private_link(&self, hash: Hash, expires: Option<u64>) -> Result<Link> {
1120 let expires_at =
1121 expires.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 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 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 pub fn register_mirror(&self, ticket: BlobTicket) -> Result<()> {
1204 self.resolver.register_mirror(ticket)
1205 }
1206
1207 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 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
1276pub 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
1310pub 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 }
1330 }
1331 Ok(())
1332}
1333
1334pub 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 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}