5b9c6ad4 Andy McCall 1 month ago ↶
1
2
750251b2 Andy McCall 1 month ago ↶
3
4
5
6
7
5b9c6ad4 Andy McCall 1 month ago ↶
8
9
use crate :: attest:: Attest;
750251b2 Andy McCall 1 month ago ↶
10
use crate :: cbor:: { self , Value} ;
5b9c6ad4 Andy McCall 1 month ago ↶
11
use crate :: hash:: { ChangeHash, ObjectRef, ObjectType} ;
12
use crate :: identity:: Signature;
750251b2 Andy McCall 1 month ago ↶
13
use crate :: objects:: { self as o, Change, ObjectError} ;
83e0677b Andy McCall 1 month ago ↶
14
use crate :: policy:: Policy;
5b9c6ad4 Andy McCall 1 month ago ↶
15
use crate :: repo:: { Repo, RepoError, Result } ;
2b5ee371 Andy McCall 1 month ago ↶
16
use std:: path:: { Path, PathBuf} ;
9f9e72b9 Andy McCall 1 month ago ↶
17
use std:: sync:: { Arc, Mutex} ;
5b9c6ad4 Andy McCall 1 month ago ↶
18
750251b2 Andy McCall 1 month ago ↶
19
pub const CONTENT_TYPE : & str = " application/oxy-cbor" ;
20
pub const PROTOCOL_VERSION : u64 = 1 ;
21
5b9c6ad4 Andy McCall 1 month ago ↶
22
23
# [ derive ( Clone, Debug, PartialEq, Eq ) ]
24
pub struct SignedAttest {
25
pub attest : Attest,
26
pub sigs : Vec< Signature> ,
27
}
28
83e0677b Andy McCall 1 month ago ↶
29
30
# [ derive ( Clone, Debug, PartialEq, Eq ) ]
31
pub struct SignedPolicy {
32
pub policy : Policy,
33
pub sigs : Vec< Signature> ,
34
}
35
28fc23d7 Andy McCall 1 month ago ↶
36
impl SignedPolicy {
37
38
pub fn encode ( & self ) -> Vec< u8 > {
39
crate :: cbor:: encode( & wire:: policy_value( self ) )
40
}
41
pub fn decode ( b : & [u8 ]) -> Result< SignedPolicy> {
75167672 Andy McCall 1 month ago
42
wire:: policy_from(
43
& crate :: cbor:: decode( b) .map_err ( | e | RepoError:: Object( ObjectError:: from( e) ) ) ? ,
44
)
28fc23d7 Andy McCall 1 month ago ↶
45
}
46
}
47
132b884b Andy McCall 1 month ago ↶
48
83e0677b Andy McCall 1 month ago ↶
49
# [ derive ( Clone, Debug, Default ) ]
50
pub struct RefsResponse {
51
pub refs : Vec< RemoteRef> ,
52
pub policy : Option< SignedPolicy> ,
132b884b Andy McCall 1 month ago ↶
53
pub tags : Vec< TagRef> ,
28fc23d7 Andy McCall 1 month ago ↶
54
55
56
pub history : Vec< SignedPolicy> ,
14edbaaf Andy McCall 1 month ago ↶
57
58
pub default_bookmark : Option< String> ,
83e0677b Andy McCall 1 month ago ↶
59
}
60
132b884b Andy McCall 1 month ago ↶
61
62
# [ derive ( Clone, Debug, PartialEq, Eq ) ]
63
pub struct TagRef {
64
pub name : String,
65
pub state : ObjectRef,
66
pub heads : Vec< ChangeHash> ,
67
pub attestation : Option< SignedAttest> ,
68
}
69
5b9c6ad4 Andy McCall 1 month ago ↶
70
71
# [ derive ( Clone, Debug ) ]
72
pub struct RemoteRef {
73
pub name : String,
74
pub state : ObjectRef,
75
pub heads : Vec< ChangeHash> ,
4161262e Andy McCall 1 month ago ↶
76
77
5b9c6ad4 Andy McCall 1 month ago ↶
78
pub attestation : Option< SignedAttest> ,
4161262e Andy McCall 1 month ago ↶
79
80
81
pub last_serial : Option< u64 > ,
f53cbe7a Andy McCall 18 days ago ↶
82
83
84
pub accepted : Option< SignedAttest> ,
5b9c6ad4 Andy McCall 1 month ago ↶
85
}
86
87
88
# [ derive ( Clone, Debug, Default ) ]
89
pub struct Bundle {
90
91
pub changes : Vec< ( Change, Vec< Signature> ) > ,
92
93
pub attests : Vec< SignedAttest> ,
94
95
pub objects : Vec< ( ObjectType, Vec< u8 > ) > ,
28fc23d7 Andy McCall 1 month ago ↶
96
97
98
pub policies : Vec< SignedPolicy> ,
598009a7 Andy McCall 1 month ago ↶
99
100
101
102
pub hashes : Vec< ChangeHash> ,
2c75672f Andy McCall 1 month ago ↶
103
104
105
106
pub raw_changes : Vec< ( Vec< u8 > , Vec< Signature> ) > ,
f0a517d2 Andy McCall 18 days ago ↶
107
108
109
pub certificates : Vec< crate:: cert:: SignedCertificate> ,
5b9c6ad4 Andy McCall 1 month ago ↶
110
}
111
112
impl Bundle {
113
pub fn is_empty ( & self ) -> bool {
114
self .changes.is_empty ( ) & & self .attests.is_empty ( ) & & self .objects.is_empty ( )
115
}
116
}
117
118
119
# [ derive ( Clone, Debug ) ]
120
pub struct RefUpdate {
121
pub name : String,
122
pub old_state : ObjectRef,
123
pub new_state : ObjectRef,
124
pub force : bool ,
125
pub attestation : Option< SignedAttest> ,
126
}
127
128
# [ derive ( Clone, Debug, PartialEq, Eq ) ]
129
pub struct RefResult {
130
pub name : String,
131
pub ok : bool ,
132
pub reason : String,
133
}
134
135
pub trait Remote {
136
fn repo_id ( & self ) -> Result< [ u8 ; 16 ] > ;
83e0677b Andy McCall 1 month ago ↶
137
fn refs ( & self ) -> Result< RefsResponse> ;
5b9c6ad4 Andy McCall 1 month ago ↶
138
139
fn negotiate ( & self , have : & [ChangeHash], want : & [ChangeHash]) -> Result< Vec< ChangeHash> > ;
b3c5a59a Andy McCall 1 month ago ↶
140
141
142
fn fetch ( & self , changes : & [ChangeHash], max_blob : Option< u64 > ) -> Result< Bundle> ;
143
144
145
fn objects ( & self , refs : & [ObjectRef]) -> Result< Vec< ( ObjectType, Vec< u8 > ) > > ;
2863456a Andy McCall 1 month ago ↶
146
147
148
fn checkpoint ( & self , heads : & [ChangeHash]) -> Result< Bundle> ;
149
150
fn shallow ( & self , heads : & [ChangeHash], depth : u32 ) -> Result< Vec< ChangeHash> > ;
132b884b Andy McCall 1 month ago ↶
151
152
153
75167672 Andy McCall 1 month ago
154
fn push (
155
& self ,
156
bundle : Bundle,
157
updates : Vec< RefUpdate> ,
158
policy : Option< SignedPolicy> ,
159
tags : Vec< TagRef> ,
160
) -> Result< Vec< RefResult> > ;
5b9c6ad4 Andy McCall 1 month ago ↶
161
}
162
750251b2 Andy McCall 1 month ago ↶
163
164
5b9c6ad4 Andy McCall 1 month ago ↶
165
166
pub struct LocalRemote {
167
pub repo : Repo,
84b9e9ac Andy McCall 1 month ago ↶
168
169
170
pub claimed_id : Option< [ u8 ; 16 ] > ,
5b9c6ad4 Andy McCall 1 month ago ↶
171
}
172
173
impl LocalRemote {
174
pub fn open ( path : & Path) -> Result< LocalRemote> {
75167672 Andy McCall 1 month ago
175
Ok ( LocalRemote {
176
repo: Repo:: open( path) ? ,
84b9e9ac Andy McCall 1 month ago ↶
177
claimed_id: None ,
75167672 Andy McCall 1 month ago
178
} )
5b9c6ad4 Andy McCall 1 month ago ↶
179
}
180
}
181
182
impl Remote for LocalRemote {
183
fn repo_id ( & self ) -> Result< [ u8 ; 16 ] > {
184
self .repo.id ( )
185
}
83e0677b Andy McCall 1 month ago ↶
186
fn refs ( & self ) -> Result< RefsResponse> {
5b9c6ad4 Andy McCall 1 month ago ↶
187
self .repo.remote_refs ( )
188
}
189
fn negotiate ( & self , have : & [ChangeHash], want : & [ChangeHash]) -> Result< Vec< ChangeHash> > {
190
self .repo.negotiate ( have, want)
191
}
b3c5a59a Andy McCall 1 month ago ↶
192
fn fetch ( & self , changes : & [ChangeHash], max_blob : Option< u64 > ) -> Result< Bundle> {
2c75672f Andy McCall 1 month ago ↶
193
self .repo.bundle_raw_for ( changes, max_blob)
b3c5a59a Andy McCall 1 month ago ↶
194
}
195
fn objects ( & self , refs : & [ObjectRef]) -> Result< Vec< ( ObjectType, Vec< u8 > ) > > {
196
self .repo.objects_for ( refs)
5b9c6ad4 Andy McCall 1 month ago ↶
197
}
2863456a Andy McCall 1 month ago ↶
198
fn checkpoint ( & self , heads : & [ChangeHash]) -> Result< Bundle> {
199
self .repo.checkpoint_bundle_for ( heads)
200
}
201
fn shallow ( & self , heads : & [ChangeHash], depth : u32 ) -> Result< Vec< ChangeHash> > {
202
self .repo.shallow_changes ( heads, depth)
203
}
75167672 Andy McCall 1 month ago
204
fn push (
205
& self ,
206
bundle : Bundle,
207
updates : Vec< RefUpdate> ,
208
policy : Option< SignedPolicy> ,
209
tags : Vec< TagRef> ,
210
) -> Result< Vec< RefResult> > {
84b9e9ac Andy McCall 1 month ago ↶
211
if self .repo.is_unbound ( ) {
212
match self .claimed_id {
213
Some ( id) => self .repo.bind_id ( id) ? ,
eae59556 Andy McCall 1 month ago ↶
214
None => {
215
return Err ( RepoError:: Msg(
216
" the remote is unbound and this client did not say which repository it is"
217
.into ( ) ,
218
) )
219
}
84b9e9ac Andy McCall 1 month ago ↶
220
}
221
}
132b884b Andy McCall 1 month ago ↶
222
self .repo.receive_push ( bundle, updates, policy, tags)
5b9c6ad4 Andy McCall 1 month ago ↶
223
}
224
}
225
750251b2 Andy McCall 1 month ago ↶
226
227
2b5ee371 Andy McCall 1 month ago ↶
228
229
# [ derive ( Clone, Debug, Default ) ]
230
pub struct RemoteOptions {
231
232
pub token : Option< String> ,
233
234
235
pub ca : Option< PathBuf> ,
75167672 Andy McCall 1 month ago
236
237
238
pub repo_id : Option< [ u8 ; 16 ] > ,
9f9e72b9 Andy McCall 1 month ago ↶
239
240
241
242
243
pub cache_dir : Option< PathBuf> ,
68a92484 Andy McCall 21 days ago ↶
244
245
246
247
248
pub fallback_tokens : Vec< String> ,
2b5ee371 Andy McCall 1 month ago ↶
249
}
250
9f9e72b9 Andy McCall 1 month ago ↶
251
252
253
254
pub type StoreOpener =
255
fn ( & str , & RemoteOptions) -> Option< crate:: strata:: Result< Box< dyn crate:: strata:: Store> > > ;
256
257
static STORE_OPENERS : Mutex< Vec< StoreOpener> > = Mutex:: new( Vec :: new( ) ) ;
258
259
pub fn register_store_opener ( opener : StoreOpener) {
260
STORE_OPENERS
261
.lock ( )
262
.unwrap_or_else ( | p | p.into_inner ( ) )
263
.push ( opener) ;
264
}
265
266
267
pub fn open_store (
268
url : & str ,
269
options : & RemoteOptions,
270
) -> Option< crate:: strata:: Result< Box< dyn crate:: strata:: Store> > > {
271
if let Some ( path) = url.strip_prefix ( " dir://" ) {
272
return Some (
273
crate :: strata:: LocalStore:: open( path)
274
.map ( | s | Box :: new( s) as Box< dyn crate:: strata:: Store> ) ,
275
) ;
276
}
277
let openers = STORE_OPENERS .lock ( ) .unwrap_or_else ( | p | p.into_inner ( ) ) ;
278
openers.iter ( ) .find_map ( | open | open ( url, options) )
279
}
280
75167672 Andy McCall 1 month ago
281
282
pub const REPO_ID_HEADER : & str = " Oxy-Repo-Id" ;
283
284
285
pub const UNBOUND_ID : [ u8 ; 16 ] = [ 0 ; 16 ] ;
286
750251b2 Andy McCall 1 month ago ↶
287
288
pub struct HttpRemote {
289
base : String,
68a92484 Andy McCall 21 days ago ↶
290
291
token : Mutex< Option< String> > ,
292
fallbacks : Mutex< Vec< String> > ,
75167672 Andy McCall 1 month ago
293
repo_id : Option< [ u8 ; 16 ] > ,
750251b2 Andy McCall 1 month ago ↶
294
agent : ureq:: Agent,
295
}
296
11c95802 Andy McCall 1 month ago ↶
297
298
299
pub fn tls_client_config ( ca : Option< & Path> ) -> Result< Arc< rustls:: ClientConfig> > {
2b5ee371 Andy McCall 1 month ago ↶
300
let mut roots = rustls:: RootCertStore:: empty( ) ;
301
roots.extend ( webpki_roots:: TLS_SERVER_ROOTS .iter ( ) .cloned ( ) ) ;
302
if let Some ( path) = ca {
75167672 Andy McCall 1 month ago
303
let pem = std:: fs:: read( path)
304
.map_err ( | e | RepoError:: Msg( format! ( " reading CA file {} : {e} " , path.display ( ) ) ) ) ? ;
2b5ee371 Andy McCall 1 month ago ↶
305
let certs = rustls_pemfile:: certs( & mut pem.as_slice ( ) )
306
.collect:: < std:: result:: Result< Vec< _ > , _ > > ( )
307
.map_err ( | e | RepoError:: Msg( format! ( " parsing CA file {} : {e} " , path.display ( ) ) ) ) ? ;
308
if certs.is_empty ( ) {
75167672 Andy McCall 1 month ago
309
return Err ( RepoError:: Msg( format! (
310
" CA file {} holds no certificates" ,
311
path.display ( )
312
) ) ) ;
2b5ee371 Andy McCall 1 month ago ↶
313
}
314
for c in certs {
75167672 Andy McCall 1 month ago
315
roots
316
.add ( c)
317
.map_err ( | e | RepoError:: Msg( format! ( " CA file {} : {e} " , path.display ( ) ) ) ) ? ;
2b5ee371 Andy McCall 1 month ago ↶
318
}
319
}
75167672 Andy McCall 1 month ago
320
let cfg = rustls:: ClientConfig:: builder_with_provider( Arc:: new(
321
rustls:: crypto:: ring:: default_provider( ) ,
322
) )
323
.with_safe_default_protocol_versions ( )
324
.map_err ( | e | RepoError:: Msg( e.to_string ( ) ) ) ?
325
.with_root_certificates ( roots)
326
.with_no_client_auth ( ) ;
2b5ee371 Andy McCall 1 month ago ↶
327
Ok ( Arc:: new( cfg) )
328
}
329
750251b2 Andy McCall 1 month ago ↶
330
impl HttpRemote {
2b5ee371 Andy McCall 1 month ago ↶
331
pub fn new ( base : & str , options : & RemoteOptions) -> Result< HttpRemote> {
ff1f62fe Andy McCall 1 month ago ↶
332
refuse_plain_token ( base, options) ? ;
75167672 Andy McCall 1 month ago
333
let agent = ureq:: AgentBuilder:: new( )
334
.tls_config ( tls_client_config ( options.ca.as_deref ( ) ) ? )
335
.build ( ) ;
336
Ok ( HttpRemote {
337
base: base.trim_end_matches ( ' /' ) .to_string ( ) ,
68a92484 Andy McCall 21 days ago ↶
338
token: Mutex:: new( options.token.clone ( ) ) ,
339
fallbacks: Mutex:: new( options.fallback_tokens.iter ( ) .rev ( ) .cloned ( ) .collect ( ) ) ,
75167672 Andy McCall 1 month ago
340
repo_id: options.repo_id,
341
agent,
342
} )
750251b2 Andy McCall 1 month ago ↶
343
}
344
68a92484 Andy McCall 21 days ago ↶
345
346
750251b2 Andy McCall 1 month ago ↶
347
fn call ( & self , post : bool , endpoint : & str , body : Option< & [ u8 ] > ) -> Result< Vec< u8 > > {
68a92484 Andy McCall 21 days ago ↶
348
loop {
349
let token = self .token.lock ( ) .unwrap_or_else ( | p | p.into_inner ( ) ) .clone ( ) ;
350
match self .call_once ( post, endpoint, body, token.as_deref ( ) ) {
351
Err ( RepoError:: Msg( m) ) if token.is_some ( ) & & refused ( & m) => {
352
let next = self
353
.fallbacks
354
.lock ( )
355
.unwrap_or_else ( | p | p.into_inner ( ) )
356
.pop ( ) ;
357
let Some ( next) = next else {
358
return Err ( RepoError:: Msg( m) ) ;
359
} ;
360
crate :: credentials:: warn_once( & format! (
361
" {} : the stored token was refused; using the one an enclosing checkout has for this hub" ,
362
self .base
363
) ) ;
364
* self .token.lock ( ) .unwrap_or_else ( | p | p.into_inner ( ) ) = Some ( next) ;
365
}
366
other => return other,
367
}
368
}
369
}
370
371
fn call_once (
372
& self ,
373
post : bool ,
374
endpoint : & str ,
375
body : Option< & [ u8 ] > ,
376
token : Option< & str > ,
377
) -> Result< Vec< u8 > > {
750251b2 Andy McCall 1 month ago ↶
378
let url = format! ( " {} /oxy/v1/{endpoint} " , self .base) ;
75167672 Andy McCall 1 month ago
379
let mut req = if post {
380
self .agent.post ( & url)
381
} else {
382
self .agent.get ( & url)
383
} ;
750251b2 Andy McCall 1 month ago ↶
384
req = req.set ( " Accept" , CONTENT_TYPE ) ;
68a92484 Andy McCall 21 days ago ↶
385
if let Some ( t) = token {
750251b2 Andy McCall 1 month ago ↶
386
req = req.set ( " Authorization" , & format! ( " Bearer {t} " ) ) ;
387
}
75167672 Andy McCall 1 month ago
388
if let Some ( id) = & self .repo_id {
389
req = req.set ( REPO_ID_HEADER , & hex:: encode( id) ) ;
390
}
750251b2 Andy McCall 1 month ago ↶
391
let resp = match body {
392
Some ( b) => req.set ( " Content-Type" , CONTENT_TYPE ) .send_bytes ( b) ,
393
None => req.call ( ) ,
394
} ;
395
match resp {
396
Ok ( r) => {
397
let mut out = Vec :: new( ) ;
75167672 Andy McCall 1 month ago
398
r.into_reader ( )
399
.read_to_end ( & mut out)
400
.map_err ( | e | RepoError:: Msg( format! ( " {url} : {e} " ) ) ) ? ;
750251b2 Andy McCall 1 month ago ↶
401
Ok ( out)
402
}
403
Err ( ureq:: Error:: Status( code, r) ) => {
404
let text = r.into_string ( ) .unwrap_or_default ( ) ;
3e5772cc Andy McCall 1 month ago ↶
405
let msg = format! ( " {url} : HTTP {code} : {} " , text.trim ( ) ) ;
406
Err ( RepoError:: Msg( with_hint ( msg) ) )
750251b2 Andy McCall 1 month ago ↶
407
}
3e5772cc Andy McCall 1 month ago ↶
408
409
Err ( e) => Err ( RepoError:: Msg( with_hint ( e.to_string ( ) ) ) ) ,
750251b2 Andy McCall 1 month ago ↶
410
}
411
}
68a92484 Andy McCall 21 days ago ↶
412
}
413
414
415
416
417
fn refused ( message : & str ) -> bool {
418
[ " : HTTP 401" , " : HTTP 403" , " : HTTP 404" ]
419
.iter ( )
420
.any ( | c | message.contains ( c) )
750251b2 Andy McCall 1 month ago ↶
421
}
422
ff1f62fe Andy McCall 1 month ago ↶
423
424
425
fn refuse_plain_token ( url : & str , options : & RemoteOptions) -> Result< ( ) > {
426
if options.token.is_some ( ) & & ! crate :: credentials:: token_allowed( url) {
427
return Err ( RepoError:: Msg( format! (
428
" {url} : refusing to send a token over plain http; use https"
429
) ) ) ;
430
}
431
Ok ( ( ) )
432
}
433
3e5772cc Andy McCall 1 month ago ↶
434
435
pub fn hint_for ( message : & str ) -> Option< & 'static str > {
436
let m = message.to_ascii_lowercase ( ) ;
437
let hint = if m.contains ( " http 401" ) {
ff1f62fe Andy McCall 1 month ago ↶
438
" the hub needs you signed in, or did not accept the token: run `oxy auth login HUB` (pushing needs repo:write)"
3e5772cc Andy McCall 1 month ago ↶
439
} else if m.contains ( " http 403" ) {
440
" the token or account lacks permission: pushing needs a token with repo:write and write access to the repository"
441
} else if m.contains ( " http 404" ) {
ff1f62fe Andy McCall 1 month ago ↶
442
" nothing at this URL that you can see: check the owner and repository name; a private repository needs you signed in (`oxy auth login HUB`)"
3e5772cc Andy McCall 1 month ago ↶
443
} else if m.contains ( " unknownissuer" )
444
| | m.contains ( " unknown issuer" )
445
| | m.contains ( " certificate" )
446
| | m.contains ( " invalidcertificate" )
447
{
448
" the server's certificate is not trusted: for a hub with a self-signed certificate, download it from /.well-known/oxyhub-ca.pem on the hub and run `oxy config remote.<remote>.ca FILE`"
449
} else if m.contains ( " connection refused" ) {
450
" nothing is listening there: check the URL and port, and that the server is running"
451
} else if m.contains ( " dns" )
452
| | m.contains ( " failed to lookup" )
453
| | m.contains ( " name or service not known" )
454
{
455
" the host name does not resolve: check the URL"
456
} else if m.contains ( " timed out" ) | | m.contains ( " timeout" ) {
457
" the server did not answer in time: check the network, or try again"
458
} else {
459
return None ;
460
} ;
461
Some ( hint)
462
}
463
464
465
pub fn with_hint ( message : String) -> String {
466
match hint_for ( & message) {
467
Some ( h) => format! ( " {message} \n hint: {h} " ) ,
468
None => message,
ff1f62fe Andy McCall 1 month ago ↶
469
}
470
}
471
472
473
474
475
pub fn http_json (
476
method : & str ,
477
url : & str ,
478
body : Option< & str > ,
479
options : & RemoteOptions,
480
) -> Result< ( u16 , String) > {
481
refuse_plain_token ( url, options) ? ;
482
let agent = ureq:: AgentBuilder:: new( )
483
.tls_config ( tls_client_config ( options.ca.as_deref ( ) ) ? )
484
.timeout ( std:: time:: Duration:: from_secs( 60 ) )
485
.build ( ) ;
486
let mut req = agent.request ( method, url) .set ( " Accept" , " application/json" ) ;
487
if let Some ( t) = & options.token {
488
req = req.set ( " Authorization" , & format! ( " Bearer {t} " ) ) ;
489
}
490
let resp = match body {
491
Some ( b) => req.set ( " Content-Type" , " application/json" ) .send_string ( b) ,
492
None => req.call ( ) ,
493
} ;
494
match resp {
495
Ok ( r) => {
496
let code = r.status ( ) ;
497
Ok ( ( code, r.into_string ( ) .unwrap_or_default ( ) ) )
498
}
499
Err ( ureq:: Error:: Status( code, r) ) => Ok ( ( code, r.into_string ( ) .unwrap_or_default ( ) ) ) ,
500
Err ( e) => Err ( RepoError:: Msg( with_hint ( e.to_string ( ) ) ) ) ,
3e5772cc Andy McCall 1 month ago ↶
501
}
502
}
503
d82e4409 Andy McCall 1 month ago ↶
504
505
506
pub fn http_get ( url : & str , options : & RemoteOptions) -> Result< Vec< u8 > > {
ff1f62fe Andy McCall 1 month ago ↶
507
refuse_plain_token ( url, options) ? ;
d82e4409 Andy McCall 1 month ago ↶
508
let agent = ureq:: AgentBuilder:: new( )
509
.tls_config ( tls_client_config ( options.ca.as_deref ( ) ) ? )
510
.build ( ) ;
511
let mut req = agent.get ( url) .set ( " Accept" , " application/json" ) ;
512
if let Some ( t) = & options.token {
513
req = req.set ( " Authorization" , & format! ( " Bearer {t} " ) ) ;
514
}
515
match req.call ( ) {
516
Ok ( r) => {
517
let mut out = Vec :: new( ) ;
518
r.into_reader ( )
519
.read_to_end ( & mut out)
520
.map_err ( | e | RepoError:: Msg( format! ( " {url} : {e} " ) ) ) ? ;
521
Ok ( out)
522
}
523
Err ( ureq:: Error:: Status( code, r) ) => {
524
let text = r.into_string ( ) .unwrap_or_default ( ) ;
3e5772cc Andy McCall 1 month ago ↶
525
Err ( RepoError:: Msg( with_hint ( format! (
4213c41d Andy McCall 1 month ago ↶
526
" {url} : HTTP {code} : {} " ,
527
text.trim ( )
3e5772cc Andy McCall 1 month ago ↶
528
) ) ) )
d82e4409 Andy McCall 1 month ago ↶
529
}
3e5772cc Andy McCall 1 month ago ↶
530
531
Err ( e) => Err ( RepoError:: Msg( with_hint ( e.to_string ( ) ) ) ) ,
d82e4409 Andy McCall 1 month ago ↶
532
}
533
}
534
750251b2 Andy McCall 1 month ago ↶
535
impl Remote for HttpRemote {
536
fn repo_id ( & self ) -> Result< [ u8 ; 16 ] > {
537
wire:: decode_capabilities( & self .call ( false , " capabilities" , None ) ? )
538
}
83e0677b Andy McCall 1 month ago ↶
539
fn refs ( & self ) -> Result< RefsResponse> {
750251b2 Andy McCall 1 month ago ↶
540
Ok ( wire:: decode_refs( & self .call ( false , " refs" , None ) ? ) ? .1 )
541
}
542
fn negotiate ( & self , have : & [ChangeHash], want : & [ChangeHash]) -> Result< Vec< ChangeHash> > {
75167672 Andy McCall 1 month ago
543
wire:: decode_hashes( & self .call (
544
true ,
545
" negotiate" ,
546
Some ( & wire:: encode_negotiate( have, want) ) ,
547
) ? )
750251b2 Andy McCall 1 month ago ↶
548
}
b3c5a59a Andy McCall 1 month ago ↶
549
fn fetch ( & self , changes : & [ChangeHash], max_blob : Option< u64 > ) -> Result< Bundle> {
75167672 Andy McCall 1 month ago
550
wire:: decode_bundle( & self .call (
551
true ,
552
" fetch" ,
553
Some ( & wire:: encode_fetch( changes, max_blob) ) ,
554
) ? )
750251b2 Andy McCall 1 month ago ↶
555
}
b3c5a59a Andy McCall 1 month ago ↶
556
fn objects ( & self , refs : & [ObjectRef]) -> Result< Vec< ( ObjectType, Vec< u8 > ) > > {
75167672 Andy McCall 1 month ago
557
Ok (
558
wire:: decode_bundle( & self .call ( true , " objects" , Some ( & wire:: encode_hashes( refs) ) ) ? ) ?
559
.objects,
560
)
b3c5a59a Andy McCall 1 month ago ↶
561
}
2863456a Andy McCall 1 month ago ↶
562
fn checkpoint ( & self , heads : & [ChangeHash]) -> Result< Bundle> {
563
wire:: decode_bundle( & self .call ( true , " checkpoint" , Some ( & wire:: encode_hashes( heads) ) ) ? )
564
}
565
fn shallow ( & self , heads : & [ChangeHash], depth : u32 ) -> Result< Vec< ChangeHash> > {
75167672 Andy McCall 1 month ago
566
wire:: decode_hashes( & self .call (
567
true ,
568
" shallow" ,
569
Some ( & wire:: encode_shallow( heads, depth) ) ,
570
) ? )
2863456a Andy McCall 1 month ago ↶
571
}
75167672 Andy McCall 1 month ago
572
fn push (
573
& self ,
574
bundle : Bundle,
575
updates : Vec< RefUpdate> ,
576
policy : Option< SignedPolicy> ,
577
tags : Vec< TagRef> ,
578
) -> Result< Vec< RefResult> > {
579
wire:: decode_push_response( & self .call (
580
true ,
581
" push" ,
582
Some ( & wire:: encode_push(
583
& bundle,
584
& updates,
585
policy.as_ref ( ) ,
586
& tags,
587
) ) ,
588
) ? )
750251b2 Andy McCall 1 month ago ↶
589
}
590
}
591
827281ee Andy McCall 1 month ago ↶
592
593
594
595
pub fn stored_url ( url : & str ) -> String {
8d803563 Andy McCall 1 month ago ↶
596
597
9f9e72b9 Andy McCall 1 month ago ↶
598
let ( prefix, path) = match ( url.strip_prefix ( " file://" ) , url.strip_prefix ( " dir://" ) ) {
599
( Some ( p) , _ ) => ( " file://" , p) ,
600
( _ , Some ( p) ) => ( " dir://" , p) ,
8d803563 Andy McCall 1 month ago ↶
601
_ if url.contains ( " ://" ) => return url.to_string ( ) ,
9f9e72b9 Andy McCall 1 month ago ↶
602
_ => ( " " , url) ,
827281ee Andy McCall 1 month ago ↶
603
} ;
604
match std:: path:: absolute( path) {
605
Ok ( abs) => format! ( " {prefix} {} " , abs.display ( ) ) ,
606
Err ( _ ) => url.to_string ( ) ,
607
}
608
}
609
9f9e72b9 Andy McCall 1 month ago ↶
610
611
2b5ee371 Andy McCall 1 month ago ↶
612
pub fn open_remote ( url : & str , options : & RemoteOptions) -> Result< Box< dyn Remote> > {
750251b2 Andy McCall 1 month ago ↶
613
if url.starts_with ( " http://" ) | | url.starts_with ( " https://" ) {
2b5ee371 Andy McCall 1 month ago ↶
614
return Ok ( Box :: new( HttpRemote:: new( url, options) ? ) ) ;
9f9e72b9 Andy McCall 1 month ago ↶
615
}
616
if let Some ( store) = open_store ( url, options) {
617
return crate :: repo:: strata:: open_store_remote( store? , options) ;
750251b2 Andy McCall 1 month ago ↶
618
}
5b9c6ad4 Andy McCall 1 month ago ↶
619
let path = url.strip_prefix ( " file://" ) .unwrap_or ( url) ;
620
if path.contains ( " ://" ) {
750251b2 Andy McCall 1 month ago ↶
621
return Err ( RepoError:: Msg( format! ( " unsupported remote URL {url:?} " ) ) ) ;
5b9c6ad4 Andy McCall 1 month ago ↶
622
}
84b9e9ac Andy McCall 1 month ago ↶
623
let mut local = LocalRemote:: open( Path:: new( path) ) ? ;
624
local.claimed_id = options.repo_id;
625
Ok ( Box :: new( local) )
5b9c6ad4 Andy McCall 1 month ago ↶
626
}
750251b2 Andy McCall 1 month ago ↶
627
628
629
630
631
pub mod wire {
632
use super :: * ;
633
634
fn schema ( msg : & str ) -> RepoError {
635
RepoError:: Object( ObjectError:: Schema( msg.into ( ) ) )
636
}
637
fn sigs_value ( sigs : & [Signature]) -> Value {
638
Value:: Array( sigs.iter ( ) .map ( | s | s.to_value ( ) ) .collect ( ) )
639
}
640
fn sigs_from ( v : & Value) -> Result< Vec< Signature> > {
75167672 Andy McCall 1 month ago
641
Ok ( o:: array( v, " signatures" ) ?
642
.iter ( )
643
.map ( Signature:: from_value)
644
.collect:: < std:: result:: Result< Vec< _ > , _ > > ( ) ? )
750251b2 Andy McCall 1 month ago ↶
645
}
646
fn hashes_value ( hs : & [ObjectRef]) -> Value {
647
Value:: Array( hs.iter ( ) .map ( | h | Value:: Bytes( h.0. to_vec ( ) ) ) .collect ( ) )
648
}
649
fn hashes_from ( v : & Value) -> Result< Vec< ObjectRef> > {
75167672 Andy McCall 1 month ago
650
Ok ( o:: array( v, " hashes" ) ?
651
.iter ( )
652
.map ( | h | o:: oref( h, " hash" ) )
653
.collect:: < std:: result:: Result< Vec< _ > , _ > > ( ) ? )
750251b2 Andy McCall 1 month ago ↶
654
}
655
fn attest_value ( sa : & SignedAttest) -> Value {
656
Value:: Array( vec! [ Value:: Bytes( sa.attest.encode ( ) ) , sigs_value ( & sa.sigs) ] )
657
}
658
fn attest_from ( v : & Value) -> Result< SignedAttest> {
659
let a = o:: array( v, " signed attest" ) ? ;
660
if a.len ( ) ! = 2 {
661
return Err ( schema ( " signed attest: expected [attest, sigs]" ) ) ;
662
}
75167672 Andy McCall 1 month ago
663
Ok ( SignedAttest {
664
attest: Attest:: decode( o:: bytes( & a[ 0 ] , " attest" ) ? ) ? ,
665
sigs: sigs_from ( & a[ 1 ] ) ? ,
666
} )
750251b2 Andy McCall 1 month ago ↶
667
}
668
fn opt_attest_value ( sa : & Option< SignedAttest> ) -> Value {
669
sa.as_ref ( ) .map ( attest_value) .unwrap_or ( Value:: Null)
670
}
671
fn opt_attest_from ( v : & Value) -> Result< Option< SignedAttest> > {
75167672 Andy McCall 1 month ago
672
if v.is_null ( ) {
673
Ok ( None )
674
} else {
675
attest_from ( v) .map ( Some )
676
}
750251b2 Andy McCall 1 month ago ↶
677
}
28fc23d7 Andy McCall 1 month ago ↶
678
679
680
pub fn policy_value ( sp : & SignedPolicy) -> Value {
681
Value:: Array( vec! [ Value:: Bytes( sp.policy.encode ( ) ) , sigs_value ( & sp.sigs) ] )
83e0677b Andy McCall 1 month ago ↶
682
}
28fc23d7 Andy McCall 1 month ago ↶
683
pub fn policy_from ( v : & Value) -> Result< SignedPolicy> {
83e0677b Andy McCall 1 month ago ↶
684
let a = o:: array( v, " signed policy" ) ? ;
685
if a.len ( ) ! = 2 {
686
return Err ( schema ( " signed policy: expected [policy, sigs]" ) ) ;
687
}
75167672 Andy McCall 1 month ago
688
Ok ( SignedPolicy {
689
policy: Policy:: decode( o:: bytes( & a[ 0 ] , " policy" ) ? ) ? ,
690
sigs: sigs_from ( & a[ 1 ] ) ? ,
691
} )
28fc23d7 Andy McCall 1 month ago ↶
692
}
693
fn opt_policy_value ( sp : Option< & SignedPolicy> ) -> Value {
694
sp.map ( policy_value) .unwrap_or ( Value:: Null)
695
}
696
fn opt_policy_from ( v : & Value) -> Result< Option< SignedPolicy> > {
75167672 Andy McCall 1 month ago
697
if v.is_null ( ) {
698
Ok ( None )
699
} else {
700
policy_from ( v) .map ( Some )
701
}
83e0677b Andy McCall 1 month ago ↶
702
}
750251b2 Andy McCall 1 month ago ↶
703
fn map < 'a > ( v : & 'a Value, what : & str ) -> Result< & 'a o:: Map> {
704
Ok ( o:: map( v, what) ? )
705
}
706
fn field < 'a > ( m : & 'a o:: Map, k : u64 , what : & str ) -> Result< & 'a Value> {
707
Ok ( o:: field( m, k, what) ? )
708
}
709
6f8aeb99 Andy McCall 1 month ago ↶
710
711
712
750251b2 Andy McCall 1 month ago ↶
713
pub fn bundle_value ( b : & Bundle) -> Value {
2c75672f Andy McCall 1 month ago ↶
714
let mut changes = Vec :: with_capacity( b.raw_changes.len ( ) + b.changes.len ( ) ) ;
715
716
717
for ( bytes, s) in & b.raw_changes {
718
changes.push ( Value:: Array( vec! [
719
Value:: Bytes( bytes.clone ( ) ) ,
720
sigs_value ( s) ,
721
] ) ) ;
722
}
6f8aeb99 Andy McCall 1 month ago ↶
723
let mut texts: Vec< ( ObjectType, Vec< u8 > ) > = Vec :: new( ) ;
724
let mut listed: Option< std:: collections:: HashSet< ObjectRef> > = None ;
725
for ( c, s) in & b.changes {
726
let ( bytes, stored) = c.encode_parts ( ) ;
727
for t in stored {
728
let listed = listed.get_or_insert_with ( | | {
729
b.objects
730
.iter ( )
731
.map ( | ( ty , body ) | crate :: hash:: hash_object( * ty, body) )
732
.collect ( )
733
} ) ;
734
if ! listed.insert ( t.blob.hash ( ) ) {
735
continue ;
736
}
737
for chunk in crate :: chunk:: chunks( & t.content) {
738
if listed.insert ( crate :: hash:: hash_object( ObjectType:: Chunk, chunk) ) {
739
texts.push ( ( ObjectType:: Chunk, chunk.to_vec ( ) ) ) ;
740
}
741
}
742
texts.push ( ( ObjectType:: Blob, t.blob.encode ( ) ) ) ;
743
}
744
changes.push ( Value:: Array( vec! [ Value:: Bytes( bytes) , sigs_value ( s) ] ) ) ;
745
}
f0a517d2 Andy McCall 18 days ago ↶
746
let mut v = Value:: map( [
6f8aeb99 Andy McCall 1 month ago ↶
747
( 0 , Value:: Array( changes) ) ,
750251b2 Andy McCall 1 month ago ↶
748
(
75167672 Andy McCall 1 month ago
749
1 ,
750
Value:: Array( b.attests.iter ( ) .map ( attest_value) .collect ( ) ) ,
751
) ,
752
(
750251b2 Andy McCall 1 month ago ↶
753
2 ,
754
Value:: Array(
75167672 Andy McCall 1 month ago
755
b.objects
756
.iter ( )
6f8aeb99 Andy McCall 1 month ago ↶
757
.chain ( & texts)
75167672 Andy McCall 1 month ago
758
.map ( | ( t , body ) | {
759
Value:: Array( vec! [
760
Value:: Text( t.name ( ) .into ( ) ) ,
761
Value:: Bytes( body.clone ( ) ) ,
762
] )
763
} )
764
.collect ( ) ,
750251b2 Andy McCall 1 month ago ↶
765
) ,
766
) ,
75167672 Andy McCall 1 month ago
767
(
768
3 ,
769
Value:: Array( b.policies.iter ( ) .map ( policy_value) .collect ( ) ) ,
770
) ,
f0a517d2 Andy McCall 18 days ago ↶
771
] ) ;
772
773
if ! b.certificates.is_empty ( ) {
774
if let Value:: Map( m) = & mut v {
775
m.insert (
776
4 ,
777
Value:: Array(
778
b.certificates
779
.iter ( )
780
.map ( | c | Value:: Bytes( c.encode ( ) ) )
781
.collect ( ) ,
782
) ,
783
) ;
784
}
785
}
786
v
750251b2 Andy McCall 1 month ago ↶
787
}
788
789
pub fn bundle_from_value ( v : & Value) -> Result< Bundle> {
790
let m = map ( v, " bundle" ) ? ;
791
let mut b = Bundle:: default( ) ;
792
for ob in o:: array( field ( m, 2 , " objects" ) ? , " objects" ) ? {
793
let pair = o:: array( ob, " object" ) ? ;
794
if pair.len ( ) ! = 2 {
795
return Err ( schema ( " object: expected [type, body]" ) ) ;
796
}
75167672 Andy McCall 1 month ago
797
let ty = ObjectType:: from_name( o:: text( & pair[ 0 ] , " object type" ) ? )
798
.ok_or_else ( | | schema ( " unknown object type" ) ) ? ;
799
b.objects
800
.push ( ( ty, o:: bytes( & pair[ 1 ] , " object body" ) ? .to_vec ( ) ) ) ;
6f8aeb99 Andy McCall 1 month ago ↶
801
}
802
803
let mut by_ref: Option< std:: collections:: HashMap< ObjectRef, usize > > = None ;
804
for c in o:: array( field ( m, 0 , " changes" ) ? , " changes" ) ? {
805
let pair = o:: array( c, " change entry" ) ? ;
806
if pair.len ( ) ! = 2 {
807
return Err ( schema ( " change entry: expected [change, sigs]" ) ) ;
808
}
598009a7 Andy McCall 1 month ago ↶
809
let encoded = o:: bytes( & pair[ 0 ] , " change" ) ? ;
810
b.hashes
811
.push ( crate :: hash:: hash_object( ObjectType:: Change, encoded) ) ;
0f93f49c Andy McCall 1 month ago ↶
812
let change = Change:: decode_with( encoded, & mut | text, count, form| {
6f8aeb99 Andy McCall 1 month ago ↶
813
let by_ref = by_ref.get_or_insert_with ( | | {
814
b.objects
815
.iter ( )
816
.enumerate ( )
817
.map ( | ( i , (ty , body ) ) | ( crate :: hash:: hash_object( * ty, body) , i) )
818
.collect ( )
819
} ) ;
820
let get =
821
| r : & ObjectRef, want : ObjectType| match by_ref.get ( r) .map ( | i | & b.objects[ * i] ) {
822
Some ( ( ty, body) ) if * ty = = want => Ok ( body) ,
823
_ => Err ( ObjectError:: Schema( format! (
824
" the bundle lacks {} {} " ,
825
want.name ( ) ,
826
r.short ( 12 )
827
) ) ) ,
828
} ;
829
let blob = crate :: objects:: Blob:: decode( get ( text, ObjectType:: Blob) ? ) ? ;
830
let mut content = Vec :: with_capacity( blob.length as usize ) ;
831
for chunk in & blob.chunks {
832
content.extend_from_slice ( get ( chunk, ObjectType:: Chunk) ? ) ;
833
}
0f93f49c Andy McCall 1 month ago ↶
834
crate :: objects:: text_lines( & content, count, form)
6f8aeb99 Andy McCall 1 month ago ↶
835
} ) ? ;
836
b.changes.push ( ( change, sigs_from ( & pair[ 1 ] ) ? ) ) ;
837
}
838
for a in o:: array( field ( m, 1 , " attests" ) ? , " attests" ) ? {
839
b.attests.push ( attest_from ( a) ? ) ;
750251b2 Andy McCall 1 month ago ↶
840
}
28fc23d7 Andy McCall 1 month ago ↶
841
if let Some ( ps) = m.get ( & 3 ) {
842
for p in o:: array( ps, " policies" ) ? {
843
b.policies.push ( policy_from ( p) ? ) ;
f0a517d2 Andy McCall 18 days ago ↶
844
}
845
}
846
if let Some ( cs) = m.get ( & 4 ) {
847
for c in o:: array( cs, " certificates" ) ? {
848
b.certificates.push (
849
crate :: cert:: SignedCertificate:: decode( o:: bytes( c, " certificate" ) ? )
850
.map_err ( | e | schema ( & format! ( " certificate: {e} " ) ) ) ? ,
851
) ;
28fc23d7 Andy McCall 1 month ago ↶
852
}
853
}
750251b2 Andy McCall 1 month ago ↶
854
Ok ( b)
855
}
856
857
pub fn encode_bundle ( b : & Bundle) -> Vec< u8 > {
858
cbor:: encode( & bundle_value ( b) )
859
}
860
pub fn decode_bundle ( bytes : & [u8 ]) -> Result< Bundle> {
861
bundle_from_value ( & cbor:: decode( bytes) .map_err ( ObjectError:: from) ? )
862
}
863
864
pub fn encode_capabilities ( id : [u8 ; 16]) -> Vec< u8 > {
865
cbor:: encode( & Value:: map( [
866
( 0 , Value:: Array( vec! [ Value:: Uint( PROTOCOL_VERSION ) ] ) ) ,
867
( 1 , Value:: Bytes( id.to_vec ( ) ) ) ,
868
( 2 , Value:: Array( vec! [ Value:: Text( " seal" .into ( ) ) ] ) ) ,
869
] ) )
870
}
871
pub fn decode_capabilities ( bytes : & [u8 ]) -> Result< [ u8 ; 16 ] > {
872
let v = cbor:: decode( bytes) .map_err ( ObjectError:: from) ? ;
873
let m = map ( & v, " capabilities" ) ? ;
874
let versions = o:: array( field ( m, 0 , " versions" ) ? , " versions" ) ? ;
75167672 Andy McCall 1 month ago
875
if ! versions
876
.iter ( )
877
.any ( | x | x.as_uint ( ) = = Some ( PROTOCOL_VERSION ) )
878
{
879
return Err ( RepoError:: Msg(
880
" remote does not speak protocol version 1" .into ( ) ,
881
) ) ;
750251b2 Andy McCall 1 month ago ↶
882
}
75167672 Andy McCall 1 month ago
883
o:: bytes( field ( m, 1 , " repo id" ) ? , " repo id" ) ?
884
.try_into ( )
885
.map_err ( | _| schema ( " repo id: expected 16 bytes" ) )
750251b2 Andy McCall 1 month ago ↶
886
}
887
132b884b Andy McCall 1 month ago ↶
888
fn tags_value ( tags : & [TagRef]) -> Value {
889
Value:: Array(
890
tags.iter ( )
891
.map ( | t | {
892
Value:: map( [
893
( 0 , Value:: Text( t.name.clone ( ) ) ) ,
894
( 1 , Value:: Bytes( t.state.0. to_vec ( ) ) ) ,
895
( 2 , hashes_value ( & t.heads) ) ,
896
( 3 , opt_attest_value ( & t.attestation) ) ,
897
] )
898
} )
899
.collect ( ) ,
900
)
901
}
902
fn tags_from ( v : & Value) -> Result< Vec< TagRef> > {
903
let mut out = Vec :: new( ) ;
904
for t in o:: array( v, " tags" ) ? {
905
let tm = map ( t, " tag" ) ? ;
906
out.push ( TagRef {
907
name: o:: text( field ( tm, 0 , " name" ) ? , " name" ) ? .to_owned ( ) ,
908
state: o:: oref( field ( tm, 1 , " state" ) ? , " state" ) ? ,
909
heads: hashes_from ( field ( tm, 2 , " heads" ) ? ) ? ,
910
attestation: opt_attest_from ( field ( tm, 3 , " attestation" ) ? ) ? ,
911
} ) ;
912
}
913
Ok ( out)
914
}
915
83e0677b Andy McCall 1 month ago ↶
916
pub fn encode_refs ( id : [u8 ; 16], resp : & RefsResponse) -> Vec< u8 > {
750251b2 Andy McCall 1 month ago ↶
917
cbor:: encode( & Value:: map( [
918
(
919
0 ,
920
Value:: Array(
83e0677b Andy McCall 1 month ago ↶
921
resp.refs
922
.iter ( )
750251b2 Andy McCall 1 month ago ↶
923
.map ( | r | {
f53cbe7a Andy McCall 18 days ago ↶
924
let mut v = Value:: map( [
750251b2 Andy McCall 1 month ago ↶
925
( 0 , Value:: Text( r.name.clone ( ) ) ) ,
926
( 1 , Value:: Bytes( r.state.0. to_vec ( ) ) ) ,
927
( 2 , hashes_value ( & r.heads) ) ,
928
( 3 , opt_attest_value ( & r.attestation) ) ,
929
( 4 , r.last_serial.map ( Value:: Uint) .unwrap_or ( Value:: Null) ) ,
f53cbe7a Andy McCall 18 days ago ↶
930
] ) ;
931
932
if r.accepted.is_some ( ) {
933
if let Value:: Map( m) = & mut v {
934
m.insert ( 5 , opt_attest_value ( & r.accepted) ) ;
935
}
936
}
937
v
750251b2 Andy McCall 1 month ago ↶
938
} )
939
.collect ( ) ,
940
) ,
941
) ,
942
( 1 , Value:: Bytes( id.to_vec ( ) ) ) ,
83e0677b Andy McCall 1 month ago ↶
943
( 2 , opt_policy_value ( resp.policy.as_ref ( ) ) ) ,
132b884b Andy McCall 1 month ago ↶
944
( 3 , tags_value ( & resp.tags) ) ,
75167672 Andy McCall 1 month ago
945
(
946
4 ,
947
Value:: Array( resp.history.iter ( ) .map ( policy_value) .collect ( ) ) ,
948
) ,
14edbaaf Andy McCall 1 month ago ↶
949
(
950
5 ,
951
resp.default_bookmark
952
.clone ( )
953
.map ( Value:: Text)
954
.unwrap_or ( Value:: Null) ,
955
) ,
750251b2 Andy McCall 1 month ago ↶
956
] ) )
957
}
83e0677b Andy McCall 1 month ago ↶
958
pub fn decode_refs ( bytes : & [u8 ]) -> Result< ( [u8 ; 16], RefsResponse) > {
750251b2 Andy McCall 1 month ago ↶
959
let v = cbor:: decode( bytes) .map_err ( ObjectError:: from) ? ;
960
let m = map ( & v, " refs" ) ? ;
75167672 Andy McCall 1 month ago
961
let id: [ u8 ; 16 ] = o:: bytes( field ( m, 1 , " repo id" ) ? , " repo id" ) ?
962
.try_into ( )
963
.map_err ( | _| schema ( " repo id: expected 16 bytes" ) ) ? ;
750251b2 Andy McCall 1 month ago ↶
964
let mut refs = Vec :: new( ) ;
965
for r in o:: array( field ( m, 0 , " refs" ) ? , " refs" ) ? {
966
let rm = map ( r, " ref" ) ? ;
967
let ls = field ( rm, 4 , " last serial" ) ? ;
968
refs.push ( RemoteRef {
969
name: o:: text( field ( rm, 0 , " name" ) ? , " name" ) ? .to_owned ( ) ,
970
state: o:: oref( field ( rm, 1 , " state" ) ? , " state" ) ? ,
971
heads: hashes_from ( field ( rm, 2 , " heads" ) ? ) ? ,
972
attestation: opt_attest_from ( field ( rm, 3 , " attestation" ) ? ) ? ,
75167672 Andy McCall 1 month ago
973
last_serial: if ls.is_null ( ) {
974
None
975
} else {
976
Some ( o:: uint( ls, " last serial" ) ? )
f53cbe7a Andy McCall 18 days ago ↶
977
} ,
978
accepted: match rm.get ( & 5 ) {
979
Some ( a) => opt_attest_from ( a) ? ,
980
None => None ,
75167672 Andy McCall 1 month ago
981
} ,
750251b2 Andy McCall 1 month ago ↶
982
} ) ;
983
}
83e0677b Andy McCall 1 month ago ↶
984
let policy = match m.get ( & 2 ) {
985
Some ( p) => opt_policy_from ( p) ? ,
986
None => None ,
987
} ;
132b884b Andy McCall 1 month ago ↶
988
let tags = match m.get ( & 3 ) {
989
Some ( t) => tags_from ( t) ? ,
990
None => Vec :: new( ) ,
991
} ;
28fc23d7 Andy McCall 1 month ago ↶
992
let mut history = Vec :: new( ) ;
993
if let Some ( h) = m.get ( & 4 ) {
994
for p in o:: array( h, " history" ) ? {
995
history.push ( policy_from ( p) ? ) ;
996
}
997
}
14edbaaf Andy McCall 1 month ago ↶
998
let default_bookmark = match m.get ( & 5 ) {
999
Some ( Value:: Text( t) ) => Some ( t.clone ( ) ) ,
1000
_ => None ,
1001
} ;
75167672 Andy McCall 1 month ago
1002
Ok ( (
1003
id,
1004
RefsResponse {
1005
refs,
1006
policy,
1007
tags,
1008
history,
14edbaaf Andy McCall 1 month ago ↶
1009
default_bookmark,
75167672 Andy McCall 1 month ago
1010
} ,
1011
) )
750251b2 Andy McCall 1 month ago ↶
1012
}
1013
1014
pub fn encode_negotiate ( have : & [ChangeHash], want : & [ChangeHash]) -> Vec< u8 > {
75167672 Andy McCall 1 month ago
1015
cbor:: encode( & Value:: map( [
1016
( 0 , hashes_value ( have) ) ,
1017
( 1 , hashes_value ( want) ) ,
1018
] ) )
750251b2 Andy McCall 1 month ago ↶
1019
}
1020
pub fn decode_negotiate ( bytes : & [u8 ]) -> Result< ( Vec< ChangeHash> , Vec< ChangeHash> ) > {
1021
let v = cbor:: decode( bytes) .map_err ( ObjectError:: from) ? ;
1022
let m = map ( & v, " negotiate" ) ? ;
75167672 Andy McCall 1 month ago
1023
Ok ( (
1024
hashes_from ( field ( m, 0 , " have" ) ? ) ? ,
1025
hashes_from ( field ( m, 1 , " want" ) ? ) ? ,
1026
) )
750251b2 Andy McCall 1 month ago ↶
1027
}
1028
b3c5a59a Andy McCall 1 month ago ↶
1029
1030
pub fn encode_fetch ( hs : & [ChangeHash], max_blob : Option< u64 > ) -> Vec< u8 > {
75167672 Andy McCall 1 month ago
1031
cbor:: encode( & Value:: map( [
1032
( 0 , hashes_value ( hs) ) ,
1033
( 1 , max_blob.map ( Value:: Uint) .unwrap_or ( Value:: Null) ) ,
1034
] ) )
b3c5a59a Andy McCall 1 month ago ↶
1035
}
1036
pub fn decode_fetch ( bytes : & [u8 ]) -> Result< ( Vec< ChangeHash> , Option< u64 > ) > {
1037
let v = cbor:: decode( bytes) .map_err ( ObjectError:: from) ? ;
1038
let m = map ( & v, " fetch" ) ? ;
1039
let max = match m.get ( & 1 ) {
1040
Some ( x) if ! x.is_null ( ) => Some ( o:: uint( x, " max blob" ) ? ) ,
1041
_ => None ,
1042
} ;
1043
Ok ( ( hashes_from ( field ( m, 0 , " changes" ) ? ) ? , max) )
1044
}
1045
2863456a Andy McCall 1 month ago ↶
1046
1047
pub fn encode_shallow ( hs : & [ChangeHash], depth : u32 ) -> Vec< u8 > {
75167672 Andy McCall 1 month ago
1048
cbor:: encode( & Value:: map( [
1049
( 0 , hashes_value ( hs) ) ,
1050
( 1 , Value:: Uint( depth as u64 ) ) ,
1051
] ) )
2863456a Andy McCall 1 month ago ↶
1052
}
1053
pub fn decode_shallow ( bytes : & [u8 ]) -> Result< ( Vec< ChangeHash> , u32 ) > {
1054
let v = cbor:: decode( bytes) .map_err ( ObjectError:: from) ? ;
1055
let m = map ( & v, " shallow" ) ? ;
75167672 Andy McCall 1 month ago
1056
Ok ( (
1057
hashes_from ( field ( m, 0 , " heads" ) ? ) ? ,
1058
o:: u32_( field ( m, 1 , " depth" ) ? , " depth" ) ? ,
1059
) )
2863456a Andy McCall 1 month ago ↶
1060
}
1061
750251b2 Andy McCall 1 month ago ↶
1062
pub fn encode_hashes ( hs : & [ChangeHash]) -> Vec< u8 > {
1063
cbor:: encode( & Value:: map( [ ( 0 , hashes_value ( hs) ) ] ) )
1064
}
1065
pub fn decode_hashes ( bytes : & [u8 ]) -> Result< Vec< ChangeHash> > {
1066
let v = cbor:: decode( bytes) .map_err ( ObjectError:: from) ? ;
1067
hashes_from ( field ( map ( & v, " hashes" ) ? , 0 , " hashes" ) ? )
1068
}
1069
75167672 Andy McCall 1 month ago
1070
pub fn encode_push (
1071
bundle : & Bundle,
1072
updates : & [RefUpdate],
1073
policy : Option< & SignedPolicy> ,
1074
tags : & [TagRef],
1075
) -> Vec< u8 > {
750251b2 Andy McCall 1 month ago ↶
1076
cbor:: encode( & Value:: map( [
1077
( 0 , bundle_value ( bundle) ) ,
83e0677b Andy McCall 1 month ago ↶
1078
( 2 , opt_policy_value ( policy) ) ,
132b884b Andy McCall 1 month ago ↶
1079
( 3 , tags_value ( tags) ) ,
750251b2 Andy McCall 1 month ago ↶
1080
(
1081
1 ,
1082
Value:: Array(
1083
updates
1084
.iter ( )
1085
.map ( | u | {
1086
Value:: map( [
1087
( 0 , Value:: Text( u.name.clone ( ) ) ) ,
1088
( 1 , Value:: Bytes( u.old_state.0. to_vec ( ) ) ) ,
1089
( 2 , Value:: Bytes( u.new_state.0. to_vec ( ) ) ) ,
1090
( 3 , Value:: Bool( u.force) ) ,
1091
( 4 , opt_attest_value ( & u.attestation) ) ,
1092
] )
1093
} )
1094
.collect ( ) ,
1095
) ,
1096
) ,
1097
] ) )
1098
}
132b884b Andy McCall 1 month ago ↶
1099
1100
pub type PushParts = ( Bundle, Vec< RefUpdate> , Option< SignedPolicy> , Vec< TagRef> ) ;
1101
1102
pub fn decode_push ( bytes : & [u8 ]) -> Result< PushParts> {
750251b2 Andy McCall 1 month ago ↶
1103
let v = cbor:: decode( bytes) .map_err ( ObjectError:: from) ? ;
1104
let m = map ( & v, " push" ) ? ;
1105
let bundle = bundle_from_value ( field ( m, 0 , " bundle" ) ? ) ? ;
1106
let mut updates = Vec :: new( ) ;
1107
for u in o:: array( field ( m, 1 , " updates" ) ? , " updates" ) ? {
1108
let um = map ( u, " update" ) ? ;
1109
updates.push ( RefUpdate {
1110
name: o:: text( field ( um, 0 , " name" ) ? , " name" ) ? .to_owned ( ) ,
1111
old_state: o:: oref( field ( um, 1 , " old_state" ) ? , " old_state" ) ? ,
1112
new_state: o:: oref( field ( um, 2 , " new_state" ) ? , " new_state" ) ? ,
75167672 Andy McCall 1 month ago
1113
force: field ( um, 3 , " force" ) ?
1114
.as_bool ( )
1115
.ok_or_else ( | | schema ( " force: expected bool" ) ) ? ,
750251b2 Andy McCall 1 month ago ↶
1116
attestation: opt_attest_from ( field ( um, 4 , " attestation" ) ? ) ? ,
1117
} ) ;
1118
}
83e0677b Andy McCall 1 month ago ↶
1119
let policy = match m.get ( & 2 ) {
1120
Some ( p) => opt_policy_from ( p) ? ,
1121
None => None ,
1122
} ;
132b884b Andy McCall 1 month ago ↶
1123
let tags = match m.get ( & 3 ) {
1124
Some ( t) => tags_from ( t) ? ,
1125
None => Vec :: new( ) ,
1126
} ;
1127
Ok ( ( bundle, updates, policy, tags) )
750251b2 Andy McCall 1 month ago ↶
1128
}
1129
1130
pub fn encode_push_response ( results : & [RefResult]) -> Vec< u8 > {
1131
cbor:: encode( & Value:: map( [ (
1132
0 ,
1133
Value:: Array(
1134
results
1135
.iter ( )
75167672 Andy McCall 1 month ago
1136
.map ( | r | {
1137
Value:: map( [
1138
( 0 , Value:: Text( r.name.clone ( ) ) ) ,
1139
( 1 , Value:: Bool( r.ok) ) ,
1140
( 2 , Value:: Text( r.reason.clone ( ) ) ) ,
1141
] )
1142
} )
750251b2 Andy McCall 1 month ago ↶
1143
.collect ( ) ,
1144
) ,
1145
) ] ) )
1146
}
1147
pub fn decode_push_response ( bytes : & [u8 ]) -> Result< Vec< RefResult> > {
1148
let v = cbor:: decode( bytes) .map_err ( ObjectError:: from) ? ;
1149
let m = map ( & v, " push response" ) ? ;
1150
let mut out = Vec :: new( ) ;
1151
for r in o:: array( field ( m, 0 , " results" ) ? , " results" ) ? {
1152
let rm = map ( r, " result" ) ? ;
1153
out.push ( RefResult {
1154
name: o:: text( field ( rm, 0 , " name" ) ? , " name" ) ? .to_owned ( ) ,
75167672 Andy McCall 1 month ago
1155
ok: field ( rm, 1 , " ok" ) ?
1156
.as_bool ( )
1157
.ok_or_else ( | | schema ( " ok: expected bool" ) ) ? ,
750251b2 Andy McCall 1 month ago ↶
1158
reason: o:: text( field ( rm, 2 , " reason" ) ? , " reason" ) ? .to_owned ( ) ,
1159
} ) ;
1160
}
1161
Ok ( out)
1162
}
1163
}
1164
1165
# [ cfg ( test ) ]
1166
mod tests {
1167
use super :: * ;
1168
use crate :: attest:: AttestBody;
1169
use crate :: objects:: Hlc;
1170
1171
# [ test ]
1172
fn wire_round_trips ( ) {
1173
let c = crate :: objects:: tests:: sample_change( ) ;
75167672 Andy McCall 1 month ago
1174
let sk = crate :: identity:: SecretKey {
1175
ed_seed: [ 1 ; 32 ] ,
1176
ml_seed: [ 2 ; 32 ] ,
1177
} ;
750251b2 Andy McCall 1 month ago ↶
1178
let id = sk.identity ( " a" , " b" , 0 ) ;
1179
let key = id.hash ( ) ;
1180
let sig = sk.sign ( crate :: identity:: Domain:: Change, & key, & c.hash ( ) ) ;
1181
let seal = Attest:: seal( [ 3 ; 16 ] , vec! [ c.hash ( ) ] ) ;
1182
let seal_sig = sk.sign ( crate :: identity:: Domain:: Attest, & key, & seal.hash ( ) ) ;
28fc23d7 Andy McCall 1 month ago ↶
1183
let policy = Policy:: single_root( [ 3 ; 16 ] , id.clone ( ) , 99 ) ;
1184
let psig = sk.sign ( crate :: identity:: Domain:: Policy, & key, & policy.hash ( ) ) ;
750251b2 Andy McCall 1 month ago ↶
1185
let bundle = Bundle {
1186
changes: vec! [ ( c.clone ( ) , vec! [ sig.clone ( ) ] ) ] ,
75167672 Andy McCall 1 month ago
1187
attests: vec! [ SignedAttest {
1188
attest: seal.clone ( ) ,
1189
sigs: vec! [ seal_sig] ,
1190
} ] ,
1191
objects: vec! [
1192
( ObjectType:: Identity, id.encode ( ) ) ,
1193
( ObjectType:: Chunk, b " xyz" .to_vec ( ) ) ,
1194
] ,
1195
policies: vec! [ SignedPolicy {
1196
policy: policy.clone ( ) ,
1197
sigs: vec! [ psig.clone ( ) ] ,
1198
} ] ,
598009a7 Andy McCall 1 month ago ↶
1199
hashes: vec! [ ] ,
2c75672f Andy McCall 1 month ago ↶
1200
raw_changes: vec! [ ] ,
f0a517d2 Andy McCall 18 days ago ↶
1201
certificates: vec! [ ] ,
750251b2 Andy McCall 1 month ago ↶
1202
} ;
1203
let d = wire:: decode_bundle( & wire:: encode_bundle( & bundle) ) .unwrap ( ) ;
598009a7 Andy McCall 1 month ago ↶
1204
assert_eq! (
1205
d.hashes,
1206
vec! [ c.hash ( ) ] ,
1207
" the receiver hashes what it was sent"
1208
) ;
750251b2 Andy McCall 1 month ago ↶
1209
assert_eq! ( d.changes, bundle.changes) ;
1210
assert_eq! ( d.attests, bundle.attests) ;
1211
assert_eq! ( d.objects, bundle.objects) ;
28fc23d7 Andy McCall 1 month ago ↶
1212
assert_eq! ( d.policies, bundle.policies) ;
f0a517d2 Andy McCall 18 days ago ↶
1213
assert! ( d.certificates.is_empty ( ) ) ;
1214
1215
1216
let plain = wire:: encode_bundle( & bundle) ;
1217
let mut with = bundle.clone ( ) ;
1218
let issuer_sk = crate :: identity:: SecretKey:: generate( ) ;
1219
let issuer = issuer_sk.identity ( " hub" , " h@x" , 1 ) ;
1220
with.certificates.push ( crate :: cert:: SignedCertificate:: sign(
1221
crate :: cert:: Certificate {
1222
key: id.clone ( ) ,
1223
issuer: issuer.hash ( ) ,
1224
subject: " agent:a@b" .into ( ) ,
1225
claims: vec! [ ] ,
1226
scope: crate :: identity:: Scope:: CHANGES ,
1227
not_before: 1 ,
1228
expires: 2 ,
a2387d54 Andy McCall 18 days ago ↶
1229
grant: None ,
f0a517d2 Andy McCall 18 days ago ↶
1230
} ,
1231
& issuer,
1232
& issuer_sk,
1233
) ) ;
1234
let encoded = wire:: encode_bundle( & with) ;
1235
assert_ne! ( encoded, plain) ;
1236
assert_eq! (
1237
wire:: decode_bundle( & encoded) .unwrap ( ) .certificates,
1238
with.certificates
1239
) ;
1240
with.certificates.clear ( ) ;
1241
assert_eq! ( wire:: encode_bundle( & with) , plain) ;
750251b2 Andy McCall 1 month ago ↶
1242
75167672 Andy McCall 1 month ago
1243
let att = Attest {
1244
repo: [ 3 ; 16 ] ,
1245
hlc: Hlc:: default( ) ,
1246
body: AttestBody:: Ref {
1247
name: " main" .into ( ) ,
1248
old_state: ObjectRef( [ 1 ; 32 ] ) ,
1249
new_state: ObjectRef( [ 2 ; 32 ] ) ,
1250
serial: 4 ,
1251
} ,
1252
} ;
750251b2 Andy McCall 1 month ago ↶
1253
let refs = vec! [
75167672 Andy McCall 1 month ago
1254
RemoteRef {
1255
name: " main" .into ( ) ,
1256
state: ObjectRef( [ 2 ; 32 ] ) ,
1257
heads: vec! [ c.hash ( ) ] ,
1258
attestation: Some ( SignedAttest {
1259
attest: att.clone ( ) ,
1260
sigs: vec! [ sig.clone ( ) ] ,
1261
} ) ,
1262
last_serial: Some ( 4 ) ,
f53cbe7a Andy McCall 18 days ago ↶
1263
accepted: None ,
75167672 Andy McCall 1 month ago
1264
} ,
1265
RemoteRef {
1266
name: " dev" .into ( ) ,
1267
state: ObjectRef( [ 5 ; 32 ] ) ,
1268
heads: vec! [ ] ,
1269
attestation: None ,
1270
last_serial: None ,
f53cbe7a Andy McCall 18 days ago ↶
1271
accepted: None ,
75167672 Andy McCall 1 month ago
1272
} ,
750251b2 Andy McCall 1 month ago ↶
1273
] ;
75167672 Andy McCall 1 month ago
1274
let tag = TagRef {
1275
name: " v1" .into ( ) ,
1276
state: ObjectRef( [ 2 ; 32 ] ) ,
1277
heads: vec! [ c.hash ( ) ] ,
1278
attestation: None ,
1279
} ;
1280
let resp = RefsResponse {
1281
refs,
1282
policy: Some ( SignedPolicy {
1283
policy: policy.clone ( ) ,
1284
sigs: vec! [ psig.clone ( ) ] ,
1285
} ) ,
1286
tags: vec! [ tag.clone ( ) ] ,
1287
history: vec! [ SignedPolicy {
1288
policy: policy.clone ( ) ,
1289
sigs: vec! [ ] ,
1290
} ] ,
14edbaaf Andy McCall 1 month ago ↶
1291
default_bookmark: Some ( " develop" .into ( ) ) ,
75167672 Andy McCall 1 month ago
1292
} ;
83e0677b Andy McCall 1 month ago ↶
1293
let ( id_back, back) = wire:: decode_refs( & wire:: encode_refs( [ 3 ; 16 ] , & resp) ) .unwrap ( ) ;
750251b2 Andy McCall 1 month ago ↶
1294
assert_eq! ( id_back, [ 3 ; 16 ] ) ;
83e0677b Andy McCall 1 month ago ↶
1295
assert_eq! ( back.refs.len ( ) , 2 ) ;
1296
assert_eq! ( back.refs[ 0 ] .attestation.as_ref ( ) .unwrap ( ) .attest, att) ;
1297
assert_eq! ( back.refs[ 1 ] .last_serial, None ) ;
1298
assert_eq! ( back.policy.as_ref ( ) .unwrap ( ) .policy, policy) ;
28fc23d7 Andy McCall 1 month ago ↶
1299
assert_eq! ( back.history, resp.history) ;
132b884b Andy McCall 1 month ago ↶
1300
assert_eq! ( back.tags, vec! [ tag.clone ( ) ] ) ;
750251b2 Andy McCall 1 month ago ↶
1301
75167672 Andy McCall 1 month ago
1302
let ( h, w) = wire:: decode_negotiate( & wire:: encode_negotiate(
1303
& [ ObjectRef( [ 1 ; 32 ] ) ] ,
1304
& [ ObjectRef( [ 2 ; 32 ] ) ] ,
1305
) )
1306
.unwrap ( ) ;
750251b2 Andy McCall 1 month ago ↶
1307
assert_eq! ( ( h, w) , ( vec! [ ObjectRef( [ 1 ; 32 ] ) ] , vec! [ ObjectRef( [ 2 ; 32 ] ) ] ) ) ;
75167672 Andy McCall 1 month ago
1308
assert_eq! (
1309
wire:: decode_hashes( & wire:: encode_hashes( & [ c.hash ( ) ] ) ) .unwrap ( ) ,
1310
vec! [ c.hash ( ) ]
1311
) ;
1312
assert_eq! (
1313
wire:: decode_fetch( & wire:: encode_fetch( & [ c.hash ( ) ] , Some ( 4096 ) ) ) .unwrap ( ) ,
1314
( vec! [ c.hash ( ) ] , Some ( 4096 ) )
1315
) ;
1316
assert_eq! (
1317
wire:: decode_fetch( & wire:: encode_fetch( & [ ] , None ) ) .unwrap ( ) ,
1318
( vec! [ ] , None )
1319
) ;
1320
assert_eq! (
1321
wire:: decode_shallow( & wire:: encode_shallow( & [ c.hash ( ) ] , 3 ) ) .unwrap ( ) ,
1322
( vec! [ c.hash ( ) ] , 3 )
1323
) ;
b3c5a59a Andy McCall 1 month ago ↶
1324
75167672 Andy McCall 1 month ago
1325
assert_eq! (
1326
wire:: decode_fetch( & wire:: encode_hashes( & [ c.hash ( ) ] ) ) .unwrap ( ) ,
1327
( vec! [ c.hash ( ) ] , None )
1328
) ;
750251b2 Andy McCall 1 month ago ↶
1329
75167672 Andy McCall 1 month ago
1330
let up = RefUpdate {
1331
name: " main" .into ( ) ,
1332
old_state: ObjectRef( [ 1 ; 32 ] ) ,
1333
new_state: ObjectRef( [ 2 ; 32 ] ) ,
1334
force: true ,
1335
attestation: Some ( SignedAttest {
1336
attest: att,
1337
sigs: vec! [ sig] ,
1338
} ) ,
1339
} ;
1340
let sp = SignedPolicy {
1341
policy,
1342
sigs: vec! [ psig] ,
1343
} ;
1344
let ( b2, ups, pol, tags) = wire:: decode_push( & wire:: encode_push(
1345
& bundle,
1346
std:: slice:: from_ref( & up) ,
1347
Some ( & sp) ,
1348
std:: slice:: from_ref( & tag) ,
1349
) )
1350
.unwrap ( ) ;
83e0677b Andy McCall 1 month ago ↶
1351
assert_eq! ( pol, Some ( sp) ) ;
132b884b Andy McCall 1 month ago ↶
1352
assert_eq! ( tags, vec! [ tag] ) ;
750251b2 Andy McCall 1 month ago ↶
1353
assert_eq! ( b2.changes.len ( ) , 1 ) ;
1354
assert_eq! ( ups[ 0 ] .name, " main" ) ;
1355
assert! ( ups[ 0 ] .force) ;
1356
assert_eq! ( ups[ 0 ] .attestation, up.attestation) ;
75167672 Andy McCall 1 month ago
1357
let res = vec! [ RefResult {
1358
name: " main" .into ( ) ,
1359
ok: false ,
1360
reason: " stale" .into ( ) ,
1361
} ] ;
1362
assert_eq! (
1363
wire:: decode_push_response( & wire:: encode_push_response( & res) ) .unwrap ( ) ,
1364
res
1365
) ;
1366
assert_eq! (
1367
wire:: decode_capabilities( & wire:: encode_capabilities( [ 7 ; 16 ] ) ) .unwrap ( ) ,
1368
[ 7 ; 16 ]
1369
) ;
750251b2 Andy McCall 1 month ago ↶
1370
}
1371
}