1pub mod update;
2
3mod id;
4
5use std::collections::{BTreeMap, BTreeSet};
6use std::fmt;
7use std::num::{NonZeroU32, NonZeroUsize};
8use std::ops::{Deref, Not};
9use std::path::Path;
10use std::str::FromStr;
11use std::sync::LazyLock;
12
13use nonempty::NonEmpty;
14use radicle_cob::type_name::{TypeName, TypeNameParse};
15use radicle_git_ext::Oid;
16use serde::{de, Deserialize, Serialize};
17use thiserror::Error;
18
19use crate::canonical::formatter::CanonicalFormatter;
20use crate::cob::identity;
21use crate::crypto;
22use crate::crypto::Signature;
23use crate::git;
24use crate::git::canonical::rules;
25use crate::identity::{project::Project, Did};
26use crate::node::device::Device;
27use crate::storage;
28use crate::storage::{ReadRepository, RepositoryError};
29
30pub use crypto::PublicKey;
31pub use id::*;
32
33use super::crefs::{self, RawCanonicalRefs};
34use super::CanonicalRefs;
35
36pub static PATH: LazyLock<&Path> = LazyLock::new(|| Path::new("radicle.json"));
38pub const MAX_STRING_LENGTH: usize = 255;
40pub const MAX_DELEGATES: usize = 255;
42pub const IDENTITY_VERSION: Version = Version(unsafe { NonZeroU32::new_unchecked(1) });
45
46#[derive(Error, Debug)]
47pub enum DocError {
48 #[error("json: {0}")]
49 Json(#[from] serde_json::Error),
50 #[error(transparent)]
51 Delegates(#[from] DelegatesError),
52 #[error(transparent)]
53 Threshold(#[from] ThresholdError),
54 #[error("git: {0}")]
55 GitExt(#[from] git::Error),
56 #[error("git: {0}")]
57 Git(#[from] git2::Error),
58 #[error("missing identity document")]
59 Missing,
60}
61
62#[derive(Debug, Error)]
63#[error("invalid delegates: {0}")]
64pub struct DelegatesError(&'static str);
65
66#[derive(Debug, Error)]
67#[error("invalid threshold `{0}`: {1}")]
68pub struct ThresholdError(usize, &'static str);
69
70impl DocError {
71 pub fn is_not_found(&self) -> bool {
73 match self {
74 Self::GitExt(git::Error::NotFound(_)) => true,
75 Self::GitExt(git::Error::Git(e)) if git::is_not_found_err(e) => true,
76 Self::Git(err) if git::is_not_found_err(err) => true,
77 _ => false,
78 }
79 }
80}
81
82#[derive(Debug, Error)]
83pub enum DefaultBranchRuleError {
84 #[error("could not create rule due to the reference name being invalid: {0}")]
85 Pattern(#[from] rules::PatternError),
86 #[error("could not load `xyz.radicle.project` to get default branch name: {0}")]
87 Payload(#[from] PayloadError),
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
97pub struct Version(NonZeroU32);
98
99impl Version {
100 pub fn new(n: u32) -> Result<Version, VersionError> {
108 match NonZeroU32::new(n) {
109 None => Err(VersionError::ZeroVersion),
110 Some(n) if n > IDENTITY_VERSION.into() => Err(VersionError::UnkownVersion(n)),
111 Some(n) => Ok(Version(n)),
112 }
113 }
114
115 pub fn number(&self) -> NonZeroU32 {
117 self.0
118 }
119
120 pub fn is_valid_version(v: &u32) -> bool {
122 0 < *v && *v <= IDENTITY_VERSION.into()
123 }
124
125 fn skip_serializing(&self) -> bool {
130 u32::from(*self) <= 1
131 }
132}
133
134impl From<Version> for NonZeroU32 {
135 fn from(Version(n): Version) -> Self {
136 n
137 }
138}
139
140impl From<Version> for u32 {
141 fn from(Version(n): Version) -> Self {
142 n.into()
143 }
144}
145
146#[derive(Debug, Error)]
147#[non_exhaustive]
148pub enum VersionError {
149 #[error("the version 0 is not supported")]
150 ZeroVersion,
151 #[error("unknown identity document version {0}, only version {IDENTITY_VERSION} is supported")]
152 UnkownVersion(NonZeroU32),
153}
154
155impl VersionError {
156 pub fn verbose(&self) -> String {
161 const UNKOWN_VERSION_ERROR: &str = r#"
162Perhaps a new version of the identity document is released which is not supported by the current client.
163See https://radicle.xyz for the latest versions of Radicle.
164The CLI command `rad id migrate` will help to migrate to an up-to-date versions."#;
165
166 match self {
167 err @ Self::ZeroVersion => err.to_string(),
168 err @ Self::UnkownVersion(_) => format!("{err}{UNKOWN_VERSION_ERROR}"),
169 }
170 }
171}
172
173impl TryFrom<u32> for Version {
174 type Error = VersionError;
175
176 fn try_from(n: u32) -> Result<Self, Self::Error> {
177 Version::new(n)
178 }
179}
180
181impl fmt::Display for Version {
182 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183 write!(f, "{}", self.0)
184 }
185}
186
187impl<'de> Deserialize<'de> for Version {
188 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
189 where
190 D: serde::Deserializer<'de>,
191 {
192 u32::deserialize(deserializer)
193 .and_then(|v| Version::new(v).map_err(|e| de::Error::custom(e.to_string())))
194 }
195}
196
197fn missing_version() -> Version {
200 unsafe { Version(NonZeroU32::new_unchecked(1)) }
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
206#[serde(transparent)]
207pub struct PayloadId(TypeName);
208
209impl fmt::Display for PayloadId {
210 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211 self.0.fmt(f)
212 }
213}
214
215impl FromStr for PayloadId {
216 type Err = TypeNameParse;
217
218 fn from_str(s: &str) -> Result<Self, Self::Err> {
219 TypeName::from_str(s).map(Self)
220 }
221}
222
223impl PayloadId {
224 pub fn project() -> Self {
226 Self(
227 TypeName::from_str("xyz.radicle.project")
229 .expect("PayloadId::project: type name is valid"),
230 )
231 }
232
233 pub fn canonical_refs() -> Self {
234 Self(
235 TypeName::from_str("xyz.radicle.crefs")
237 .expect("PayloadId::canonical_refs: type name is valid"),
238 )
239 }
240}
241
242#[derive(Debug, Error)]
243pub enum PayloadError {
244 #[error("json: {0}")]
245 Json(#[from] serde_json::Error),
246 #[error("payload '{0}' not found in identity document")]
247 NotFound(PayloadId),
248}
249
250#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
254#[serde(transparent)]
255pub struct Payload {
256 value: serde_json::Value,
257}
258
259impl Payload {
260 pub fn as_object_mut(
262 &mut self,
263 ) -> Option<&mut serde_json::value::Map<String, serde_json::Value>> {
264 self.value.as_object_mut()
265 }
266}
267
268impl From<serde_json::Value> for Payload {
269 fn from(value: serde_json::Value) -> Self {
270 Self { value }
271 }
272}
273
274impl Deref for Payload {
275 type Target = serde_json::Value;
276
277 fn deref(&self) -> &Self::Target {
278 &self.value
279 }
280}
281
282#[derive(Debug, Clone, PartialEq, Eq)]
284pub struct DocAt {
285 pub commit: Oid,
287 pub blob: Oid,
289 pub doc: Doc,
291}
292
293impl Deref for DocAt {
294 type Target = Doc;
295
296 fn deref(&self) -> &Self::Target {
297 &self.doc
298 }
299}
300
301impl From<DocAt> for Doc {
302 fn from(value: DocAt) -> Self {
303 value.doc
304 }
305}
306
307impl AsRef<Doc> for DocAt {
308 fn as_ref(&self) -> &Doc {
309 &self.doc
310 }
311}
312
313#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
315#[serde(rename_all = "camelCase", tag = "type")]
316pub enum Visibility {
317 #[default]
319 Public,
320 Private {
322 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
323 allow: BTreeSet<Did>,
324 },
325}
326
327#[derive(Error, Debug)]
328#[error("'{0}' is not a valid visibility type")]
329pub struct VisibilityParseError(String);
330
331impl FromStr for Visibility {
332 type Err = VisibilityParseError;
333
334 fn from_str(s: &str) -> Result<Self, Self::Err> {
335 match s {
336 "public" => Ok(Visibility::Public),
337 "private" => Ok(Visibility::private([])),
338 _ => Err(VisibilityParseError(s.to_owned())),
339 }
340 }
341}
342
343impl Visibility {
344 pub fn is_public(&self) -> bool {
346 matches!(self, Self::Public)
347 }
348
349 pub fn is_private(&self) -> bool {
351 matches!(self, Self::Private { .. })
352 }
353
354 pub fn private(allow: impl IntoIterator<Item = Did>) -> Self {
356 Self::Private {
357 allow: BTreeSet::from_iter(allow),
358 }
359 }
360}
361
362#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
372#[serde(rename_all = "camelCase")]
373pub struct RawDoc {
374 #[serde(default = "missing_version")]
376 version: Version,
377 pub payload: BTreeMap<PayloadId, Payload>,
379 pub delegates: Vec<Did>,
381 pub threshold: usize,
383 #[serde(default)]
385 pub visibility: Visibility,
386}
387
388impl TryFrom<RawDoc> for Doc {
389 type Error = DocError;
390
391 fn try_from(doc: RawDoc) -> Result<Self, Self::Error> {
392 doc.verified()
393 }
394}
395
396impl RawDoc {
397 pub fn new(
401 project: Project,
402 delegates: Vec<Did>,
403 threshold: usize,
404 visibility: Visibility,
405 ) -> Self {
406 let project =
407 serde_json::to_value(project).expect("Doc::initial: payload must be serializable");
408
409 Self {
410 version: IDENTITY_VERSION,
411 payload: BTreeMap::from_iter([(PayloadId::project(), Payload::from(project))]),
412 delegates,
413 threshold,
414 visibility,
415 }
416 }
417
418 pub fn version(&self) -> &Version {
420 &self.version
421 }
422
423 pub fn project(&self) -> Result<Project, PayloadError> {
425 let value = self
426 .payload
427 .get(&PayloadId::project())
428 .ok_or_else(|| PayloadError::NotFound(PayloadId::project()))?;
429 let proj: Project = serde_json::from_value((**value).clone())?;
430
431 Ok(proj)
432 }
433
434 pub fn is_delegate(&self, did: &Did) -> bool {
436 self.delegates.contains(did)
437 }
438
439 pub fn delegate(&mut self, did: Did) {
444 self.delegates.push(did)
445 }
446
447 pub fn rescind(&mut self, did: &Did) -> Result<bool, DocError> {
450 let (matches, delegates) = self.delegates.iter().partition(|d| *d == did);
451 self.delegates = delegates;
452 Ok(matches.is_empty().not())
453 }
454
455 pub fn from_json(bytes: &[u8]) -> Result<Self, DocError> {
458 serde_json::from_slice(bytes).map_err(DocError::from)
459 }
460
461 pub fn verified(self) -> Result<Doc, DocError> {
470 let RawDoc {
471 version,
472 payload,
473 delegates,
474 threshold,
475 visibility,
476 } = self;
477 let delegates = Delegates::new(delegates)?;
478 let threshold = Threshold::new(threshold, &delegates)?;
479 Ok(Doc {
480 version,
481 payload,
482 delegates,
483 threshold,
484 visibility,
485 })
486 }
487}
488
489#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
493#[serde(try_from = "Vec<Did>")]
494pub struct Delegates(NonEmpty<Did>);
495
496impl AsRef<NonEmpty<Did>> for Delegates {
497 fn as_ref(&self) -> &NonEmpty<Did> {
498 &self.0
499 }
500}
501
502impl From<Did> for Delegates {
503 fn from(did: Did) -> Self {
504 Self(NonEmpty::new(did))
505 }
506}
507
508impl TryFrom<Vec<Did>> for Delegates {
509 type Error = DelegatesError;
510
511 fn try_from(dids: Vec<Did>) -> Result<Self, Self::Error> {
512 Delegates::new(dids)
513 }
514}
515
516impl IntoIterator for Delegates {
517 type Item = <NonEmpty<Did> as IntoIterator>::Item;
518 type IntoIter = <NonEmpty<Did> as IntoIterator>::IntoIter;
519
520 fn into_iter(self) -> Self::IntoIter {
521 self.0.into_iter()
522 }
523}
524
525impl Delegates {
526 pub fn new(delegates: impl IntoIterator<Item = Did>) -> Result<Self, DelegatesError> {
530 let delegates = delegates
531 .into_iter()
532 .try_fold(Vec::<Did>::new(), |mut dids, did| {
533 if !dids.contains(&did) {
534 if dids.len() >= MAX_DELEGATES {
535 return Err(DelegatesError("number of delegates cannot exceed 255"));
536 }
537 dids.push(did);
538 }
539 Ok(dids)
540 })?;
541 NonEmpty::from_vec(delegates)
542 .map(Self)
543 .ok_or(DelegatesError("delegate list cannot be empty"))
544 }
545
546 pub fn first(&self) -> &Did {
548 self.0.first()
549 }
550
551 pub fn iter(&self) -> impl Iterator<Item = &Did> {
553 self.0.iter()
554 }
555
556 pub fn contains(&self, did: &Did) -> bool {
558 self.0.contains(did)
559 }
560
561 pub fn is_only(&self, did: &Did) -> bool {
563 self.0.tail.is_empty() && &self.0.head == did
564 }
565
566 pub fn len(&self) -> usize {
568 self.0.len()
569 }
570
571 pub fn is_empty(&self) -> bool {
573 false
574 }
575}
576
577impl<'a> From<&'a Delegates> for &'a NonEmpty<Did> {
578 fn from(ds: &'a Delegates) -> Self {
579 &ds.0
580 }
581}
582
583impl From<Delegates> for NonEmpty<Did> {
584 fn from(ds: Delegates) -> Self {
585 ds.0
586 }
587}
588
589impl From<Delegates> for Vec<Did> {
590 fn from(Delegates(ds): Delegates) -> Self {
591 ds.into()
592 }
593}
594
595#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
599#[serde(transparent)]
600pub struct Threshold(NonZeroUsize);
601
602impl From<Threshold> for usize {
603 fn from(Threshold(t): Threshold) -> Self {
604 t.get()
605 }
606}
607
608impl AsRef<NonZeroUsize> for Threshold {
609 fn as_ref(&self) -> &NonZeroUsize {
610 &self.0
611 }
612}
613
614impl fmt::Display for Threshold {
615 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
616 write!(f, "{}", self.0)
617 }
618}
619
620impl Threshold {
621 pub const MIN: Threshold = Threshold(NonZeroUsize::MIN);
623
624 pub fn new(t: usize, delegates: &Delegates) -> Result<Self, ThresholdError> {
628 if t > MAX_DELEGATES {
629 Err(ThresholdError(t, "threshold cannot exceed 255"))
630 } else if t > delegates.len() {
631 Err(ThresholdError(
632 t,
633 "threshold cannot exceed number of delegates",
634 ))
635 } else {
636 NonZeroUsize::new(t)
637 .map(Self)
638 .ok_or(ThresholdError(t, "threshold cannot be zero"))
639 }
640 }
641}
642
643#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
658#[serde(rename_all = "camelCase")]
659#[serde(try_from = "RawDoc")]
660pub struct Doc {
661 #[serde(skip_serializing_if = "Version::skip_serializing")]
662 version: Version,
663 payload: BTreeMap<PayloadId, Payload>,
664 delegates: Delegates,
665 threshold: Threshold,
666 #[serde(default, skip_serializing_if = "Visibility::is_public")]
667 visibility: Visibility,
668}
669
670impl Doc {
671 pub fn initial(project: Project, delegate: Did, visibility: Visibility) -> Self {
677 let project =
678 serde_json::to_value(project).expect("Doc::initial: payload must be serializable");
679
680 Self {
681 version: IDENTITY_VERSION,
682 payload: BTreeMap::from_iter([(PayloadId::project(), Payload::from(project))]),
683 delegates: Delegates(NonEmpty::new(delegate)),
684 threshold: Threshold(NonZeroUsize::MIN),
685 visibility,
686 }
687 }
688
689 pub fn from_blob(blob: &git2::Blob) -> Result<Self, DocError> {
691 RawDoc::from_json(blob.content())?.verified()
692 }
693
694 pub fn edit(self) -> RawDoc {
697 let Doc {
698 version,
699 payload,
700 delegates,
701 threshold,
702 visibility,
703 } = self;
704 RawDoc {
705 version,
706 payload,
707 delegates: delegates.into(),
708 threshold: threshold.into(),
709 visibility,
710 }
711 }
712
713 pub fn with_edits<F>(self, f: F) -> Result<Self, DocError>
716 where
717 F: FnOnce(&mut RawDoc),
718 {
719 let mut raw = self.edit();
720 f(&mut raw);
721 raw.verified()
722 }
723
724 pub fn version(&self) -> &Version {
726 &self.version
727 }
728
729 pub fn payload(&self) -> &BTreeMap<PayloadId, Payload> {
731 &self.payload
732 }
733
734 pub fn project(&self) -> Result<Project, PayloadError> {
736 let value = self
737 .payload
738 .get(&PayloadId::project())
739 .ok_or_else(|| PayloadError::NotFound(PayloadId::project()))?;
740 let proj: Project = serde_json::from_value((**value).clone())?;
741
742 Ok(proj)
743 }
744
745 pub fn default_branch_rule(
746 &self,
747 ) -> Result<(rules::Pattern, rules::ValidRule), DefaultBranchRuleError> {
748 let proj = self.project()?;
749 let refname = proj.default_branch();
750 let pattern = rules::Pattern::try_from(git::refs::branch(refname).to_owned())?;
751 let rule = rules::Rule::new(
752 rules::ResolvedDelegates::Delegates(self.delegates.clone()),
753 self.threshold,
754 );
755 Ok((pattern, rule))
756 }
757
758 pub fn visibility(&self) -> &Visibility {
760 &self.visibility
761 }
762
763 pub fn is_public(&self) -> bool {
765 self.visibility.is_public()
766 }
767
768 pub fn is_private(&self) -> bool {
770 self.visibility.is_private()
771 }
772
773 pub fn threshold(&self) -> usize {
775 self.threshold.into()
776 }
777
778 pub fn threshold_nonzero(&self) -> &NonZeroUsize {
780 &self.threshold.0
781 }
782
783 pub fn delegates(&self) -> &Delegates {
785 &self.delegates
786 }
787
788 pub fn is_delegate(&self, did: &Did) -> bool {
790 self.delegates.contains(did)
791 }
792
793 pub fn is_visible_to(&self, did: &Did) -> bool {
796 match &self.visibility {
797 Visibility::Public => true,
798 Visibility::Private { allow } => allow.contains(did) || self.is_delegate(did),
799 }
800 }
801
802 pub fn verify_signature(
805 &self,
806 key: &PublicKey,
807 signature: &Signature,
808 blob: Oid,
809 ) -> Result<(), PublicKey> {
810 if !self.is_delegate(&key.into()) {
811 return Err(*key);
812 }
813 if key.verify(blob.as_bytes(), signature).is_err() {
814 return Err(*key);
815 }
816 Ok(())
817 }
818
819 pub fn is_majority(&self, votes: usize) -> bool {
821 votes >= self.majority()
822 }
823
824 pub fn majority(&self) -> usize {
826 self.delegates.len() / 2 + 1
827 }
828
829 pub(crate) fn blob_at<R: ReadRepository>(
831 commit: Oid,
832 repo: &R,
833 ) -> Result<git2::Blob, DocError> {
834 let path = Path::new("embeds").join(*PATH);
835 repo.blob_at(commit, path.as_path()).map_err(DocError::from)
836 }
837
838 pub fn encode(&self) -> Result<(git::Oid, Vec<u8>), DocError> {
841 let mut buf = Vec::new();
842 let mut serializer =
843 serde_json::Serializer::with_formatter(&mut buf, CanonicalFormatter::new());
844
845 self.serialize(&mut serializer)?;
846 let oid = git2::Oid::hash_object(git2::ObjectType::Blob, &buf)?;
847
848 Ok((oid.into(), buf))
849 }
850
851 pub fn sign<G>(&self, signer: &G) -> Result<(git::Oid, Vec<u8>, Signature), DocError>
854 where
855 G: crypto::signature::Signer<crypto::Signature>,
856 {
857 let (oid, bytes) = self.encode()?;
858 let sig = signer.sign(oid.as_bytes());
859
860 Ok((oid, bytes, sig))
861 }
862
863 pub fn signature_of<G>(&self, signer: &G) -> Result<Signature, DocError>
865 where
866 G: crypto::signature::Signer<crypto::Signature>,
867 {
868 let (_, _, sig) = self.sign(signer)?;
869
870 Ok(sig)
871 }
872
873 pub fn load_at<R: ReadRepository>(commit: Oid, repo: &R) -> Result<DocAt, DocError> {
876 let blob = Self::blob_at(commit, repo)?;
877 let doc = Self::from_blob(&blob)?;
878
879 Ok(DocAt {
880 commit,
881 doc,
882 blob: blob.id().into(),
883 })
884 }
885
886 pub fn init<G>(
889 &self,
890 repo: &storage::git::Repository,
891 signer: &Device<G>,
892 ) -> Result<git::Oid, RepositoryError>
893 where
894 G: crypto::signature::Signer<crypto::Signature>,
895 {
896 let cob = identity::Identity::initialize(self, repo, signer)?;
897 let id_ref = git::refs::storage::id(signer.public_key());
898 let cob_ref = git::refs::storage::cob(
899 signer.public_key(),
900 &crate::cob::identity::TYPENAME,
901 &cob.id,
902 );
903 repo.backend.reference_symbolic(
905 id_ref.as_str(),
906 cob_ref.as_str(),
907 false,
908 "Create `rad/id` reference to point to new identity COB",
909 )?;
910
911 Ok(*cob.id)
912 }
913}
914
915#[derive(Debug, Error)]
916pub enum CanonicalRefsError {
917 #[error(transparent)]
918 Json(#[from] serde_json::Error),
919 #[error(transparent)]
920 CanonicalRefs(#[from] rules::ValidationError),
921 #[error(transparent)]
922 DefaultBranch(#[from] DefaultBranchRuleError),
923}
924
925impl crefs::GetCanonicalRefs for Doc {
926 type Error = CanonicalRefsError;
927
928 fn canonical_refs(&self) -> Result<Option<CanonicalRefs>, Self::Error> {
929 self.raw_canonical_refs().and_then(|raw| {
930 raw.map(|raw| {
931 raw.try_into_canonical_refs(&mut || self.delegates.clone())
932 .map_err(CanonicalRefsError::from)
933 .and_then(|mut crefs| {
934 self.default_branch_rule()
935 .map_err(CanonicalRefsError::from)
936 .map(|rule| {
937 crefs.extend([rule]);
938 crefs
939 })
940 })
941 })
942 .transpose()
943 })
944 }
945
946 fn raw_canonical_refs(&self) -> Result<Option<RawCanonicalRefs>, Self::Error> {
947 let value = self.payload.get(&PayloadId::canonical_refs());
948 let crefs = value
949 .map(|value| {
950 serde_json::from_value((**value).clone()).map_err(CanonicalRefsError::from)
951 })
952 .transpose()?;
953 Ok(crefs)
954 }
955}
956
957impl crefs::GetCanonicalRefs for RawDoc {
958 type Error = CanonicalRefsError;
959
960 fn canonical_refs(&self) -> Result<Option<CanonicalRefs>, Self::Error> {
961 Ok(None)
962 }
963
964 fn raw_canonical_refs(&self) -> Result<Option<RawCanonicalRefs>, Self::Error> {
965 let value = self.payload.get(&PayloadId::canonical_refs());
966 let crefs = value
967 .map(|value| {
968 serde_json::from_value((**value).clone()).map_err(CanonicalRefsError::from)
969 })
970 .transpose()?;
971 Ok(crefs)
972 }
973}
974
975#[cfg(test)]
976#[allow(clippy::unwrap_used)]
977mod test {
978 use serde_json::json;
979
980 use crate::assert_matches;
981 use crate::rad;
982 use crate::storage::git::transport;
983 use crate::storage::git::Storage;
984 use crate::storage::{ReadStorage as _, RemoteId, WriteStorage as _};
985 use crate::test::arbitrary;
986 use crate::test::arbitrary::gen;
987 use crate::test::fixtures;
988
989 use super::*;
990 use qcheck_macros::quickcheck;
991
992 #[test]
993 fn test_duplicate_dids() {
994 let delegate = Device::mock_from_seed([0xff; 32]);
995 let did = Did::from(delegate.public_key());
996 let mut doc = RawDoc::new(gen::<Project>(1), vec![did], 1, Visibility::Public);
997 doc.delegate(did);
998 let doc = doc.verified().unwrap();
999 assert!(doc.delegates().len() == 1, "Duplicate DID was not removed");
1000 assert!(doc.delegates().first() == &did)
1001 }
1002
1003 #[test]
1004 fn test_max_delegates() {
1005 let delegates = (0..MAX_DELEGATES + 1).map(gen).collect::<Vec<Did>>();
1007
1008 let doc = RawDoc::new(
1010 gen::<Project>(1),
1011 delegates[0..MAX_DELEGATES].into(),
1012 1,
1013 Visibility::Public,
1014 );
1015 assert_matches!(doc.verified(), Ok(_));
1016
1017 let doc = RawDoc::new(gen::<Project>(1), delegates, 1, Visibility::Public);
1019 assert_matches!(doc.verified(), Err(DocError::Delegates(DelegatesError(_))));
1020 }
1021
1022 #[test]
1023 fn test_is_valid_version() {
1024 assert!(!Version::is_valid_version(&0));
1026
1027 let current = IDENTITY_VERSION.number();
1029 assert!(Version::is_valid_version(¤t.into()));
1030
1031 let next = current.checked_add(1).unwrap();
1034 assert!(!Version::is_valid_version(&next.into()));
1035 }
1036
1037 #[test]
1038 fn test_future_version_error() {
1039 let v = Version(NonZeroU32::MAX).to_string();
1040 assert_eq!(
1041 serde_json::from_str::<Version>(&v)
1042 .expect_err("should fail to deserialize")
1043 .to_string(),
1044 VersionError::UnkownVersion(NonZeroU32::MAX).to_string(),
1045 )
1046 }
1047
1048 #[test]
1049 fn test_parse_version() {
1050 let v1 = json!(
1052 {
1053 "payload": {
1054 "xyz.radicle.project": {
1055 "defaultBranch": "master",
1056 "description": "Radicle Heartwood Protocol & Stack",
1057 "name": "heartwood"
1058 }
1059 },
1060 "delegates": [
1061 "did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
1062 "did:key:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW",
1063 "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM"
1064 ],
1065 "threshold": 1
1066 }
1067 );
1068
1069 let doc = serde_json::from_str::<RawDoc>(&v1.to_string()).unwrap();
1072 let payload = [(
1073 PayloadId::project(),
1074 Payload {
1075 value: json!({
1076 "name": "heartwood",
1077 "description": "Radicle Heartwood Protocol & Stack",
1078 "defaultBranch": "master",
1079 }),
1080 },
1081 )]
1082 .into_iter()
1083 .collect::<BTreeMap<_, _>>();
1084 let delegates = vec![
1085 "did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT"
1086 .parse::<Did>()
1087 .unwrap(),
1088 "did:key:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW"
1089 .parse::<Did>()
1090 .unwrap(),
1091 "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM"
1092 .parse::<Did>()
1093 .unwrap(),
1094 ];
1095 assert_eq!(
1097 doc,
1098 RawDoc {
1099 version: IDENTITY_VERSION,
1100 payload: payload.clone(),
1101 delegates: delegates.clone(),
1102 threshold: 1,
1103 visibility: Visibility::Public,
1104 }
1105 );
1106
1107 let verified = serde_json::from_str::<Doc>(&v1.to_string()).unwrap();
1109 let delegates = Delegates(NonEmpty::from_vec(delegates).unwrap());
1110 assert_eq!(
1111 verified,
1112 Doc {
1113 version: IDENTITY_VERSION,
1114 threshold: Threshold::new(1, &delegates).unwrap(),
1115 payload: payload.clone(),
1116 delegates,
1117 visibility: Visibility::Public,
1118 }
1119 );
1120 }
1121
1122 #[test]
1123 fn test_canonical_example() {
1124 let tempdir = tempfile::tempdir().unwrap();
1125 let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();
1126
1127 transport::local::register(storage.clone());
1128
1129 let delegate = Device::mock_from_seed([0xff; 32]);
1130 let (repo, _) = fixtures::repository(tempdir.path().join("working"));
1131 let (id, _, _) = rad::init(
1132 &repo,
1133 "heartwood".try_into().unwrap(),
1134 "Radicle Heartwood Protocol & Stack",
1135 git::refname!("master"),
1136 Visibility::default(),
1137 &delegate,
1138 &storage,
1139 )
1140 .unwrap();
1141
1142 assert_eq!(
1143 delegate.public_key().to_human(),
1144 String::from("z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi")
1145 );
1146 assert_eq!(
1147 (*id).to_string(),
1148 "d96f425412c9f8ad5d9a9a05c9831d0728e2338d"
1149 );
1150 assert_eq!(id.urn(), String::from("rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji"));
1151 }
1152
1153 #[test]
1154 fn test_not_found() {
1155 let tempdir = tempfile::tempdir().unwrap();
1156 let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();
1157 let remote = arbitrary::gen::<RemoteId>(1);
1158 let proj = arbitrary::gen::<RepoId>(1);
1159 let repo = storage.create(proj).unwrap();
1160 let oid = git2::Oid::from_str("2d52a53ce5e4f141148a5f770cfd3ead2d6a45b8").unwrap();
1161
1162 let err = repo.identity_head_of(&remote).unwrap_err();
1163 matches!(err, git::ext::Error::NotFound(_));
1164
1165 let err = Doc::load_at(oid.into(), &repo).unwrap_err();
1166 assert!(err.is_not_found());
1167 }
1168
1169 #[test]
1170 fn test_canonical_doc() {
1171 let tempdir = tempfile::tempdir().unwrap();
1172 let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();
1173 transport::local::register(storage.clone());
1174
1175 let (working, _) = fixtures::repository(tempdir.path().join("working"));
1176
1177 let delegate = Device::mock_from_seed([0xff; 32]);
1178 let (rid, doc, _) = rad::init(
1179 &working,
1180 "heartwood".try_into().unwrap(),
1181 "Radicle Heartwood Protocol & Stack",
1182 git::refname!("master"),
1183 Visibility::default(),
1184 &delegate,
1185 &storage,
1186 )
1187 .unwrap();
1188 let repo = storage.repository(rid).unwrap();
1189
1190 assert_eq!(doc, repo.identity_doc().unwrap().doc);
1191 }
1192
1193 #[quickcheck]
1194 fn prop_encode_decode(doc: Doc) {
1195 let (_, bytes) = doc.encode().unwrap();
1196 assert_eq!(RawDoc::from_json(&bytes).unwrap().verified().unwrap(), doc);
1197 }
1198
1199 #[test]
1200 fn test_visibility_json() {
1201 use std::str::FromStr;
1202
1203 assert_eq!(
1204 serde_json::to_value(Visibility::Public).unwrap(),
1205 serde_json::json!({ "type": "public" })
1206 );
1207 assert_eq!(
1208 serde_json::to_value(Visibility::private([])).unwrap(),
1209 serde_json::json!({ "type": "private" })
1210 );
1211 assert_eq!(
1212 serde_json::to_value(Visibility::private([Did::from_str(
1213 "did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT"
1214 )
1215 .unwrap()]))
1216 .unwrap(),
1217 serde_json::json!({ "type": "private", "allow": ["did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT"] })
1218 );
1219 }
1220}