This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-migrate / src / mapping.rs
23 kB 671 lines
1use std::collections::{BTreeMap, BTreeSet}; 2use std::fmt; 3 4use knot_types::{AccountDid, OwnerDid, ParseError, RepoDid, RepoName, RepoRkey, UnixSeconds}; 5use zeroize::{Zeroize, ZeroizeOnDrop}; 6 7use crate::casbin::AclRoster; 8use crate::source::{ 9 CollabRow, MemberRow, RepoRow, SourceDid, SourceKeyType, SourceRepoDid, SourceRepoName, 10 SourceRkey, SourceTimestamp, 11}; 12 13#[derive(Debug, thiserror::Error)] 14pub enum MappingError { 15 #[error("repo DID {value} doesn't parse: {source}")] 16 BadRepoDid { 17 value: SourceRepoDid, 18 source: ParseError, 19 }, 20 #[error("owner {value} on repo {repo} doesn't parse: {source}")] 21 BadOwnerDid { 22 repo: SourceRepoDid, 23 value: SourceDid, 24 source: ParseError, 25 }, 26 #[error("member DID {value} doesn't parse: {source}")] 27 BadMemberDid { 28 value: SourceDid, 29 source: ParseError, 30 }, 31 #[error("collaborator DID {value} on repo {repo} doesn't parse: {source}")] 32 BadCollaboratorDid { 33 repo: SourceRepoDid, 34 value: SourceDid, 35 source: ParseError, 36 }, 37 #[error("{context} timestamp {value} isn't RFC 3339")] 38 BadTimestamp { 39 context: &'static str, 40 value: SourceTimestamp, 41 }, 42 #[error("repo {repo} has a {key_type} signing key of {bytes} bytes instead of 32-byte k256")] 43 BadSigningKey { 44 repo: SourceRepoDid, 45 key_type: SourceKeyType, 46 bytes: usize, 47 }, 48 #[error("acl names no server owner")] 49 MissingServerOwner, 50 #[error("acl marks {marker} as owner of {repo} while repo_keys names {owner}")] 51 ConflictingOwnerMarker { 52 repo: SourceRepoDid, 53 marker: SourceDid, 54 owner: SourceDid, 55 }, 56 #[error("record key {owner}/{rkey} has no single alias-backed holder")] 57 AmbiguousRkey { owner: OwnerDid, rkey: RepoRkey }, 58} 59 60#[derive(Debug, Clone, PartialEq, Eq)] 61pub struct MappedGrant { 62 pub subject: AccountDid, 63 pub added_by: AccountDid, 64 pub created_at: UnixSeconds, 65 pub unioned: bool, 66} 67 68#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)] 69pub struct SigningKey([u8; 32]); 70 71impl fmt::Debug for SigningKey { 72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 73 f.debug_tuple("SigningKey").finish_non_exhaustive() 74 } 75} 76 77impl SigningKey { 78 pub fn to_hex(&self) -> zeroize::Zeroizing<String> { 79 zeroize::Zeroizing::new(knot_types::lowercase_hex(&self.0)) 80 } 81} 82 83#[derive(Debug, Clone, PartialEq, Eq)] 84pub struct AdoptRepo { 85 pub source_did: SourceRepoDid, 86 pub did: RepoDid, 87 pub owner: OwnerDid, 88 pub name: RepoName, 89 pub rkey: RepoRkey, 90 pub created_at: UnixSeconds, 91 pub signing_key: SigningKey, 92 pub collaborators: Vec<MappedGrant>, 93} 94 95#[derive(Debug, Clone, PartialEq, Eq)] 96pub enum SkipReason { 97 Name { value: SourceRepoName }, 98 Rkey { value: SourceRkey }, 99 RkeyCollision { rkey: RepoRkey, winner: RepoDid }, 100 NoSourceRepo, 101} 102 103#[derive(Debug, Clone, PartialEq, Eq)] 104pub struct SkippedRepo { 105 pub repo_did: RepoDid, 106 pub reason: SkipReason, 107 pub lost_collaborators: Vec<AccountDid>, 108} 109 110#[derive(Debug, Default, PartialEq, Eq)] 111pub struct Drift { 112 pub acl_only_collaborators: Vec<(SourceRepoDid, SourceDid)>, 113 pub table_only_collaborators: Vec<(SourceRepoDid, SourceDid)>, 114 pub slash_resolved_collaborators: Vec<(SourceRepoDid, SourceDid)>, 115 pub orphan_collaborator_pairs: Vec<(SourceRepoDid, SourceDid)>, 116 pub markerless_owner_repos: Vec<SourceRepoDid>, 117 pub orphan_owner_markers: Vec<SourceRepoDid>, 118 pub extra_owner_markers: Vec<(SourceRepoDid, SourceDid)>, 119 pub acl_only_members: Vec<SourceDid>, 120 pub table_only_members: Vec<SourceDid>, 121 pub slash_owner_markers: u64, 122 pub slash_collab_rows: u64, 123 pub unresolved_slash_forms: Vec<String>, 124} 125 126#[derive(Debug)] 127pub struct Mapping { 128 pub knot_owner: AccountDid, 129 pub members: Vec<MappedGrant>, 130 pub repos: Vec<AdoptRepo>, 131 pub skipped: Vec<SkippedRepo>, 132 pub drift: Drift, 133} 134 135pub fn map_tables( 136 repos: &[RepoRow], 137 rkeys: &BTreeMap<SourceRepoDid, SourceRkey>, 138 members: &[MemberRow], 139 collabs: &[CollabRow], 140 acl: &AclRoster, 141 exists: impl Fn(&SourceRepoDid) -> bool, 142) -> Result<Mapping, MappingError> { 143 let knot_owner = server_owner(acl)?; 144 let repo_index: BTreeMap<&SourceRepoDid, &RepoRow> = 145 repos.iter().map(|repo| (&repo.repo_did, repo)).collect(); 146 147 let table_members: Vec<MappedGrant> = members 148 .iter() 149 .filter(|row| row.subject.as_str() != knot_owner.as_str()) 150 .map(|row| { 151 Ok(MappedGrant { 152 subject: account(&row.subject).map_err(|source| MappingError::BadMemberDid { 153 value: row.subject.clone(), 154 source, 155 })?, 156 added_by: account(&row.did).map_err(|source| MappingError::BadMemberDid { 157 value: row.did.clone(), 158 source, 159 })?, 160 created_at: unix("knot_members.created", &row.created)?, 161 unioned: false, 162 }) 163 }) 164 .collect::<Result<_, MappingError>>()?; 165 166 let table_member_dids: BTreeSet<&SourceDid> = members.iter().map(|row| &row.subject).collect(); 167 let acl_only_members: Vec<SourceDid> = acl 168 .members 169 .iter() 170 .filter(|did| !table_member_dids.contains(*did) && did.as_str() != knot_owner.as_str()) 171 .cloned() 172 .collect(); 173 let table_only_members: Vec<SourceDid> = members 174 .iter() 175 .filter(|row| !acl.members.contains(&row.subject)) 176 .map(|row| row.subject.clone()) 177 .collect(); 178 let unioned_members: Vec<MappedGrant> = acl_only_members 179 .iter() 180 .map(|did| { 181 Ok(MappedGrant { 182 subject: account(did).map_err(|source| MappingError::BadMemberDid { 183 value: did.clone(), 184 source, 185 })?, 186 added_by: knot_owner.clone(), 187 // every casbin-only owner is at epoch 188 created_at: UnixSeconds::new(0), 189 unioned: true, 190 }) 191 }) 192 .collect::<Result<_, MappingError>>()?; 193 194 let (live_collabs, orphan_collabs): (Vec<&CollabRow>, Vec<&CollabRow>) = collabs 195 .iter() 196 .partition(|row| repo_index.contains_key(&row.repo_did)); 197 let table_pairs: BTreeSet<(&SourceRepoDid, &SourceDid)> = collabs 198 .iter() 199 .map(|row| (&row.repo_did, &row.subject_did)) 200 .collect(); 201 let acl_extra: Vec<(&SourceRepoDid, &SourceDid)> = acl 202 .collaborators 203 .iter() 204 .flat_map(|(repo, dids)| dids.iter().map(move |did| (repo, did))) 205 .filter(|(repo, did)| !table_pairs.contains(&(*repo, *did))) 206 .collect(); 207 let (acl_only, acl_orphans): (Vec<_>, Vec<_>) = acl_extra 208 .into_iter() 209 .partition(|(repo, _)| repo_index.contains_key(*repo)); 210 let orphan_pairs: BTreeSet<(SourceRepoDid, SourceDid)> = acl_orphans 211 .into_iter() 212 .map(|(repo, did)| (repo.clone(), did.clone())) 213 .chain( 214 orphan_collabs 215 .iter() 216 .map(|row| (row.repo_did.clone(), row.subject_did.clone())), 217 ) 218 .collect(); 219 let table_only_collaborators: Vec<(SourceRepoDid, SourceDid)> = live_collabs 220 .iter() 221 .filter(|row| { 222 [&acl.collaborators, &acl.slash_collaborators] 223 .into_iter() 224 .all(|grants| { 225 grants 226 .get(&row.repo_did) 227 .is_none_or(|dids| !dids.contains(&row.subject_did)) 228 }) 229 }) 230 .map(|row| (row.repo_did.clone(), row.subject_did.clone())) 231 .collect(); 232 233 let table_grants = live_collabs.iter().copied().try_fold( 234 BTreeMap::<SourceRepoDid, Vec<MappedGrant>>::new(), 235 |mut grants, row| { 236 let grant = MappedGrant { 237 subject: account(&row.subject_did).map_err(|source| { 238 MappingError::BadCollaboratorDid { 239 repo: row.repo_did.clone(), 240 value: row.subject_did.clone(), 241 source, 242 } 243 })?, 244 added_by: account(&row.added_by_did).map_err(|source| { 245 MappingError::BadCollaboratorDid { 246 repo: row.repo_did.clone(), 247 value: row.added_by_did.clone(), 248 source, 249 } 250 })?, 251 created_at: unix("collaborators.created", &row.created)?, 252 unioned: false, 253 }; 254 let entry = grants.entry(row.repo_did.clone()).or_default(); 255 if !entry.iter().any(|held| held.subject == grant.subject) { 256 entry.push(grant); 257 } 258 Ok::<_, MappingError>(grants) 259 }, 260 )?; 261 let collab_grants = owner_attributed_grants(&acl_only, &repo_index, true, table_grants)?; 262 263 let owners = owner_drift(repos, &repo_index, acl)?; 264 let (adopted, skipped) = classify(repos, rkeys, &collab_grants, exists)?; 265 266 Ok(Mapping { 267 knot_owner, 268 members: table_members.into_iter().chain(unioned_members).collect(), 269 repos: adopted, 270 skipped, 271 drift: Drift { 272 acl_only_collaborators: acl_only 273 .into_iter() 274 .map(|(repo, did)| (repo.clone(), did.clone())) 275 .collect(), 276 table_only_collaborators, 277 slash_resolved_collaborators: acl 278 .slash_collaborators 279 .iter() 280 .flat_map(|(repo, dids)| dids.iter().map(move |did| (repo, did))) 281 .filter(|(repo, did)| !table_pairs.contains(&(*repo, *did))) 282 .map(|(repo, did)| (repo.clone(), did.clone())) 283 .collect(), 284 orphan_collaborator_pairs: orphan_pairs.into_iter().collect(), 285 markerless_owner_repos: owners.markerless, 286 orphan_owner_markers: owners.orphans, 287 extra_owner_markers: owners.extras, 288 acl_only_members, 289 table_only_members, 290 slash_owner_markers: acl.slash_owner_markers, 291 slash_collab_rows: acl.slash_collab_rows, 292 unresolved_slash_forms: acl.unresolved_slash_forms.clone(), 293 }, 294 }) 295} 296 297pub fn map_preflip( 298 repos: &[RepoRow], 299 rkeys: &BTreeMap<SourceRepoDid, SourceRkey>, 300 members: &[MemberRow], 301 acl: &AclRoster, 302 exists: impl Fn(&SourceRepoDid) -> bool, 303) -> Result<Mapping, MappingError> { 304 let knot_owner = server_owner(acl)?; 305 let repo_index: BTreeMap<&SourceRepoDid, &RepoRow> = 306 repos.iter().map(|repo| (&repo.repo_did, repo)).collect(); 307 let enrich: BTreeMap<&SourceDid, &MemberRow> = 308 members.iter().map(|row| (&row.subject, row)).collect(); 309 let table_only_members: Vec<SourceDid> = members 310 .iter() 311 .filter(|row| !acl.members.contains(&row.subject)) 312 .map(|row| row.subject.clone()) 313 .collect(); 314 315 let mapped_members: Vec<MappedGrant> = acl 316 .members 317 .iter() 318 .filter(|did| did.as_str() != knot_owner.as_str()) 319 .map(|did| { 320 let subject = account(did).map_err(|source| MappingError::BadMemberDid { 321 value: did.clone(), 322 source, 323 })?; 324 match enrich.get(did) { 325 Some(row) => Ok(MappedGrant { 326 subject, 327 added_by: account(&row.did).map_err(|source| MappingError::BadMemberDid { 328 value: row.did.clone(), 329 source, 330 })?, 331 created_at: unix("knot_members.created", &row.created)?, 332 unioned: false, 333 }), 334 None => Ok(MappedGrant { 335 subject, 336 added_by: knot_owner.clone(), 337 created_at: UnixSeconds::new(0), 338 unioned: false, 339 }), 340 } 341 }) 342 .collect::<Result<_, MappingError>>()?; 343 344 let combined: BTreeMap<&SourceRepoDid, BTreeSet<&SourceDid>> = acl 345 .collaborators 346 .iter() 347 .chain(acl.slash_collaborators.iter()) 348 .flat_map(|(repo, dids)| dids.iter().map(move |did| (repo, did))) 349 .fold(BTreeMap::new(), |mut pairs, (repo, did)| { 350 pairs.entry(repo).or_default().insert(did); 351 pairs 352 }); 353 let live = combined 354 .iter() 355 .flat_map(|(repo, dids)| dids.iter().map(move |did| (*repo, *did))); 356 let (resolvable, orphan_pairs): (Vec<_>, Vec<_>) = 357 live.partition(|(repo, _)| repo_index.contains_key(*repo)); 358 let collab_grants = owner_attributed_grants(&resolvable, &repo_index, false, BTreeMap::new())?; 359 360 let owners = owner_drift(repos, &repo_index, acl)?; 361 let (adopted, skipped) = classify(repos, rkeys, &collab_grants, exists)?; 362 363 Ok(Mapping { 364 knot_owner, 365 members: mapped_members, 366 repos: adopted, 367 skipped, 368 drift: Drift { 369 orphan_collaborator_pairs: orphan_pairs 370 .into_iter() 371 .map(|(repo, did)| (repo.clone(), did.clone())) 372 .collect(), 373 markerless_owner_repos: owners.markerless, 374 orphan_owner_markers: owners.orphans, 375 extra_owner_markers: owners.extras, 376 table_only_members, 377 slash_owner_markers: acl.slash_owner_markers, 378 slash_collab_rows: acl.slash_collab_rows, 379 unresolved_slash_forms: acl.unresolved_slash_forms.clone(), 380 ..Drift::default() 381 }, 382 }) 383} 384 385fn owner_attributed_grants( 386 pairs: &[(&SourceRepoDid, &SourceDid)], 387 repo_index: &BTreeMap<&SourceRepoDid, &RepoRow>, 388 unioned: bool, 389 base: BTreeMap<SourceRepoDid, Vec<MappedGrant>>, 390) -> Result<BTreeMap<SourceRepoDid, Vec<MappedGrant>>, MappingError> { 391 pairs 392 .iter() 393 .copied() 394 .try_fold(base, |mut grants, (repo, did)| { 395 let row = repo_index[repo]; 396 let grant = MappedGrant { 397 subject: account(did).map_err(|source| MappingError::BadCollaboratorDid { 398 repo: repo.clone(), 399 value: did.clone(), 400 source, 401 })?, 402 added_by: account(&row.owner_did).map_err(|source| MappingError::BadOwnerDid { 403 repo: repo.clone(), 404 value: row.owner_did.clone(), 405 source, 406 })?, 407 created_at: unix("repo_keys.created_at", &row.created_at)?, 408 unioned, 409 }; 410 grants.entry(repo.clone()).or_default().push(grant); 411 Ok(grants) 412 }) 413} 414 415fn server_owner(acl: &AclRoster) -> Result<AccountDid, MappingError> { 416 let did = acl 417 .server_owner 418 .as_ref() 419 .ok_or(MappingError::MissingServerOwner)?; 420 account(did).map_err(|source| MappingError::BadMemberDid { 421 value: did.clone(), 422 source, 423 }) 424} 425 426struct OwnerDrift { 427 markerless: Vec<SourceRepoDid>, 428 orphans: Vec<SourceRepoDid>, 429 extras: Vec<(SourceRepoDid, SourceDid)>, 430} 431 432fn owner_drift( 433 repos: &[RepoRow], 434 repo_index: &BTreeMap<&SourceRepoDid, &RepoRow>, 435 acl: &AclRoster, 436) -> Result<OwnerDrift, MappingError> { 437 repos.iter().try_for_each(|repo| { 438 let conflicting = acl.owner_markers.get(&repo.repo_did).and_then(|markers| { 439 (!markers.contains(&repo.owner_did)) 440 .then(|| markers.iter().next().cloned()) 441 .flatten() 442 }); 443 match conflicting { 444 Some(marker) => Err(MappingError::ConflictingOwnerMarker { 445 repo: repo.repo_did.clone(), 446 marker, 447 owner: repo.owner_did.clone(), 448 }), 449 None => Ok(()), 450 } 451 })?; 452 let markerless = repos 453 .iter() 454 .filter(|repo| !acl.owner_markers.contains_key(&repo.repo_did)) 455 .map(|repo| repo.repo_did.clone()) 456 .collect(); 457 let orphans = acl 458 .owner_markers 459 .keys() 460 .filter(|repo| !repo_index.contains_key(*repo)) 461 .cloned() 462 .collect(); 463 let extras = repos 464 .iter() 465 .filter_map(|repo| { 466 acl.owner_markers 467 .get(&repo.repo_did) 468 .map(|markers| (repo, markers)) 469 }) 470 .flat_map(|(repo, markers)| { 471 markers 472 .iter() 473 .filter(move |marker| *marker != &repo.owner_did) 474 .map(move |marker| (repo.repo_did.clone(), marker.clone())) 475 }) 476 .collect(); 477 Ok(OwnerDrift { 478 markerless, 479 orphans, 480 extras, 481 }) 482} 483 484fn classify( 485 repos: &[RepoRow], 486 rkeys: &BTreeMap<SourceRepoDid, SourceRkey>, 487 collab_grants: &BTreeMap<SourceRepoDid, Vec<MappedGrant>>, 488 exists: impl Fn(&SourceRepoDid) -> bool, 489) -> Result<(Vec<AdoptRepo>, Vec<SkippedRepo>), MappingError> { 490 let (adopted, skipped) = repos.iter().try_fold( 491 (Vec::new(), Vec::new()), 492 |(mut adopted, mut skipped), row| { 493 match classify_one(row, rkeys, collab_grants, &exists)? { 494 Ok(repo) => adopted.push(repo), 495 Err(skip) => skipped.push(skip), 496 } 497 Ok::<_, MappingError>((adopted, skipped)) 498 }, 499 )?; 500 let alias_backed: BTreeSet<RepoDid> = rkeys 501 .keys() 502 .filter_map(|did| RepoDid::new(did.as_str()).ok()) 503 .collect(); 504 resolve_rkey_collisions(adopted, skipped, &alias_backed) 505} 506 507fn resolve_rkey_collisions( 508 adopted: Vec<AdoptRepo>, 509 skipped: Vec<SkippedRepo>, 510 alias_backed: &BTreeSet<RepoDid>, 511) -> Result<(Vec<AdoptRepo>, Vec<SkippedRepo>), MappingError> { 512 let groups: BTreeMap<(&OwnerDid, &RepoRkey), Vec<usize>> = 513 adopted 514 .iter() 515 .enumerate() 516 .fold(BTreeMap::new(), |mut groups, (index, repo)| { 517 groups 518 .entry((&repo.owner, &repo.rkey)) 519 .or_default() 520 .push(index); 521 groups 522 }); 523 let losers: BTreeMap<usize, SkippedRepo> = groups 524 .into_iter() 525 .filter(|(_, indices)| indices.len() > 1) 526 .map(|((owner, rkey), indices)| { 527 let backed: Vec<usize> = indices 528 .iter() 529 .copied() 530 .filter(|index| alias_backed.contains(&adopted[*index].did)) 531 .collect(); 532 match backed.as_slice() { 533 [winner] => Ok(indices 534 .into_iter() 535 .filter(|index| index != &*winner) 536 .map(|index| { 537 ( 538 index, 539 SkippedRepo { 540 repo_did: adopted[index].did.clone(), 541 reason: SkipReason::RkeyCollision { 542 rkey: rkey.clone(), 543 winner: adopted[*winner].did.clone(), 544 }, 545 lost_collaborators: adopted[index] 546 .collaborators 547 .iter() 548 .map(|grant| grant.subject.clone()) 549 .collect(), 550 }, 551 ) 552 }) 553 .collect::<Vec<_>>()), 554 _ => Err(MappingError::AmbiguousRkey { 555 owner: owner.clone(), 556 rkey: rkey.clone(), 557 }), 558 } 559 }) 560 .collect::<Result<Vec<_>, MappingError>>()? 561 .into_iter() 562 .flatten() 563 .collect(); 564 let (kept, demoted) = adopted.into_iter().enumerate().fold( 565 (Vec::new(), losers), 566 |(mut kept, demoted), (index, repo)| { 567 if !demoted.contains_key(&index) { 568 kept.push(repo); 569 } 570 (kept, demoted) 571 }, 572 ); 573 Ok(( 574 kept, 575 skipped.into_iter().chain(demoted.into_values()).collect(), 576 )) 577} 578 579fn classify_one( 580 row: &RepoRow, 581 rkeys: &BTreeMap<SourceRepoDid, SourceRkey>, 582 collab_grants: &BTreeMap<SourceRepoDid, Vec<MappedGrant>>, 583 exists: &impl Fn(&SourceRepoDid) -> bool, 584) -> Result<Result<AdoptRepo, SkippedRepo>, MappingError> { 585 let did = RepoDid::new(row.repo_did.as_str()).map_err(|source| MappingError::BadRepoDid { 586 value: row.repo_did.clone(), 587 source, 588 })?; 589 let owner = 590 OwnerDid::new(row.owner_did.as_str()).map_err(|source| MappingError::BadOwnerDid { 591 repo: row.repo_did.clone(), 592 value: row.owner_did.clone(), 593 source, 594 })?; 595 let created_at = unix("repo_keys.created_at", &row.created_at)?; 596 let key_bytes: [u8; 32] = 597 row.signing_key 598 .as_bytes() 599 .try_into() 600 .map_err(|_| MappingError::BadSigningKey { 601 repo: row.repo_did.clone(), 602 key_type: row.key_type.clone(), 603 bytes: row.signing_key.as_bytes().len(), 604 })?; 605 if !row.key_type.is_k256() { 606 return Err(MappingError::BadSigningKey { 607 repo: row.repo_did.clone(), 608 key_type: row.key_type.clone(), 609 bytes: row.signing_key.as_bytes().len(), 610 }); 611 } 612 613 let skip = |reason: SkipReason| SkippedRepo { 614 repo_did: did.clone(), 615 reason, 616 lost_collaborators: collab_grants 617 .get(&row.repo_did) 618 .map(|grants| grants.iter().map(|grant| grant.subject.clone()).collect()) 619 .unwrap_or_default(), 620 }; 621 let name = match RepoName::new(row.repo_name.as_str()) { 622 Ok(name) => name, 623 Err(_) => { 624 return Ok(Err(skip(SkipReason::Name { 625 value: row.repo_name.clone(), 626 }))); 627 } 628 }; 629 let raw_rkey = rkeys 630 .get(&row.repo_did) 631 .map(SourceRkey::as_str) 632 .unwrap_or(row.repo_name.as_str()); 633 let rkey = match RepoRkey::new(raw_rkey) { 634 Ok(rkey) => rkey, 635 Err(_) => { 636 return Ok(Err(skip(SkipReason::Rkey { 637 value: SourceRkey::from_column(raw_rkey), 638 }))); 639 } 640 }; 641 if !exists(&row.repo_did) { 642 return Ok(Err(skip(SkipReason::NoSourceRepo))); 643 } 644 645 Ok(Ok(AdoptRepo { 646 source_did: row.repo_did.clone(), 647 did, 648 owner, 649 name, 650 rkey, 651 created_at, 652 signing_key: SigningKey(key_bytes), 653 collaborators: collab_grants 654 .get(&row.repo_did) 655 .cloned() 656 .unwrap_or_default(), 657 })) 658} 659 660fn account(value: &SourceDid) -> Result<AccountDid, ParseError> { 661 AccountDid::new(value.as_str()) 662} 663 664fn unix(context: &'static str, value: &SourceTimestamp) -> Result<UnixSeconds, MappingError> { 665 chrono::DateTime::parse_from_rfc3339(value.as_str()) 666 .map(|parsed| UnixSeconds::new(parsed.timestamp())) 667 .map_err(|_| MappingError::BadTimestamp { 668 context, 669 value: value.clone(), 670 }) 671}