File Coverage

blib/lib/CDS.pm
Criterion Covered Total %
statement 119 10998 1.0
branch 3 3122 0.1
condition 1 2178 0.0
subroutine 39 1533 2.5
pod 0 36 0.0
total 162 17867 0.9


line stmt bran cond sub pod time code
1             # This is part of the Condensation Perl Module 0.27 (cli) built on 2022-02-10.
2             # See https://condensation.io for information about the Condensation Data System.
3              
4 1     1   66709 use strict;
  1         2  
  1         24  
5 1     1   4 use warnings;
  1         1  
  1         18  
6 1     1   20 use 5.010000;
  1         3  
7 1     1   617 use CDS::C;
  1         1  
  1         37  
8              
9             =pod
10              
11             =head1 CDS - Condensation Data System
12              
13             Condensation is a general-purpose distributed data system with conflict-free synchronization, and inherent end-to-end security.
14              
15             This is the Perl implementation. It comes with a Perl module:
16              
17             use CDS;
18              
19             and a command line tool:
20              
21             cds
22              
23             More information is available on L.
24              
25             =cut
26              
27 1     1   5 use Cwd;
  1         2  
  1         52  
28 1     1   1158 use Digest::SHA;
  1         2627  
  1         42  
29 1     1   497 use Encode;
  1         16154  
  1         78  
30 1     1   7 use Fcntl;
  1         1  
  1         182  
31 1     1   421 use HTTP::Date;
  1         3483  
  1         50  
32 1     1   474 use HTTP::Headers;
  1         3921  
  1         29  
33 1     1   382 use HTTP::Request;
  1         14640  
  1         27  
34 1     1   470 use HTTP::Server::Simple;
  1         16139  
  1         29  
35 1     1   690 use LWP::UserAgent;
  1         21902  
  1         27  
36 1     1   5 use Time::Local;
  1         2  
  1         46  
37 1     1   505 use utf8;
  1         13  
  1         8  
38             package CDS;
39              
40             our $VERSION = '0.27';
41             our $edition = 'cli';
42             our $releaseDate = '2022-02-10';
43              
44 0     0 0 0 sub now { time * 1000 }
45              
46 0     0 0 0 sub SECOND { 1000 }
47 0     0 0 0 sub MINUTE { 60 * 1000 }
48 0     0 0 0 sub HOUR { 60 * 60 * 1000 }
49 0     0 0 0 sub DAY { 24 * 60 * 60 * 1000 }
50 0     0 0 0 sub WEEK { 7 * 24 * 60 * 60 * 1000 }
51 0     0 0 0 sub MONTH { 30 * 24 * 60 * 60 * 1000 }
52 0     0 0 0 sub YEAR { 365 * 24 * 60 * 60 * 1000 }
53              
54             # File system utility functions.
55              
56             sub readBytesFromFile {
57 0     0 0 0 my $class = shift;
58 0         0 my $filename = shift;
59              
60 0 0       0 open(my $fh, '<:bytes', $filename) || return;
61 0         0 local $/;
62 0         0 my $content = <$fh>;
63 0         0 close $fh;
64 0         0 return $content;
65             }
66              
67             sub writeBytesToFile {
68 0     0 0 0 my $class = shift;
69 0         0 my $filename = shift;
70              
71 0 0       0 open(my $fh, '>:bytes', $filename) || return;
72 0         0 print $fh @_;
73 0         0 close $fh;
74 0         0 return 1;
75             }
76              
77             sub readTextFromFile {
78 0     0 0 0 my $class = shift;
79 0         0 my $filename = shift;
80              
81 0 0       0 open(my $fh, '<:utf8', $filename) || return;
82 0         0 local $/;
83 0         0 my $content = <$fh>;
84 0         0 close $fh;
85 0         0 return $content;
86             }
87              
88             sub writeTextToFile {
89 0     0 0 0 my $class = shift;
90 0         0 my $filename = shift;
91              
92 0 0       0 open(my $fh, '>:utf8', $filename) || return;
93 0         0 print $fh @_;
94 0         0 close $fh;
95 0         0 return 1;
96             }
97              
98             sub listFolder {
99 0     0 0 0 my $class = shift;
100 0         0 my $folder = shift;
101              
102 0 0       0 opendir(my $dh, $folder) || return;
103 0         0 my @files = readdir $dh;
104 0         0 closedir $dh;
105 0         0 return @files;
106             }
107              
108             sub intermediateFolders {
109 0     0 0 0 my $class = shift;
110 0         0 my $path = shift;
111              
112 0         0 my @paths = ($path);
113 0         0 while (1) {
114 0 0       0 $path =~ /^(.+)\/(.*?)$/ || last;
115 0         0 $path = $1;
116 0 0       0 next if ! length $2;
117 0         0 unshift @paths, $path;
118             }
119 0         0 return @paths;
120             }
121              
122             # This is for debugging purposes only.
123             sub log {
124 0     0 0 0 my $class = shift;
125              
126 0         0 print STDERR @_, "\n";
127             }
128              
129             sub min {
130 0     0 0 0 my $class = shift;
131              
132 0         0 my $min = shift;
133 0         0 for my $number (@_) {
134 0 0       0 $min = $min < $number ? $min : $number;
135             }
136              
137 0         0 return $min;
138             }
139              
140             sub max {
141 0     0 0 0 my $class = shift;
142              
143 0         0 my $max = shift;
144 0         0 for my $number (@_) {
145 0 0       0 $max = $max > $number ? $max : $number;
146             }
147              
148 0         0 return $max;
149             }
150              
151             sub booleanCompare {
152 0     0 0 0 my $class = shift;
153 0         0 my $a = shift;
154 0         0 my $b = shift;
155 0 0 0     0 $a && $b ? 0 : $a ? 1 : $b ? -1 : 0 }
    0          
    0          
156              
157             # Utility functions for random sequences
158              
159             srand(time);
160             our @hexDigits = ('0'..'9', 'a'..'f');
161              
162             sub randomHex {
163 0     0 0 0 my $class = shift;
164 0         0 my $length = shift;
165              
166 0         0 return substr(unpack('H*', CDS::C::randomBytes(int(($length + 1) / 2))), 0, $length);
167             }
168              
169             sub randomBytes {
170 0     0 0 0 my $class = shift;
171 0         0 my $length = shift;
172              
173 0         0 return CDS::C::randomBytes($length);
174             }
175              
176             sub randomKey {
177 0     0 0 0 my $class = shift;
178              
179 0         0 return CDS::C::randomBytes(32);
180             }
181              
182 0     0 0 0 sub version { 'Condensation, Perl, '.$CDS::VERSION }
183              
184             # Conversion of numbers and booleans to and from bytes.
185             # To convert text, use Encode::encode_utf8($text) and Encode::decode_utf8($bytes).
186             # To convert hex sequences, use pack('H*', $hex) and unpack('H*', $bytes).
187              
188             sub bytesFromBoolean {
189 0     0 0 0 my $class = shift;
190 0         0 my $value = shift;
191 0 0       0 $value ? 'y' : '' }
192              
193             sub bytesFromInteger {
194 0     0 0 0 my $class = shift;
195 0         0 my $value = shift;
196              
197 0 0 0     0 return '' if $value >= 0 && $value < 1;
198 0 0 0     0 return pack 'c', $value if $value >= -0x80 && $value < 0x80;
199 0 0 0     0 return pack 's>', $value if $value >= -0x8000 && $value < 0x8000;
200              
201             # This works up to 63 bits, plus 1 sign bit
202 0         0 my $bytes = pack 'q>', $value;
203              
204 0         0 my $pos = 0;
205 0         0 my $first = ord(substr($bytes, 0, 1));
206 0 0       0 if ($value > 0) {
    0          
207             # Perl internally uses an unsigned 64-bit integer if the value is positive
208 0 0       0 return "\x7f\xff\xff\xff\xff\xff\xff\xff" if $first >= 128;
209 0         0 while ($first == 0) {
210 0         0 my $next = ord(substr($bytes, $pos + 1, 1));
211 0 0       0 last if $next >= 128;
212 0         0 $first = $next;
213 0         0 $pos += 1;
214             }
215             } elsif ($first == 255) {
216 0         0 while ($first == 255) {
217 0         0 my $next = ord(substr($bytes, $pos + 1, 1));
218 0 0       0 last if $next < 128;
219 0         0 $first = $next;
220 0         0 $pos += 1;
221             }
222             }
223              
224 0         0 return substr($bytes, $pos);
225             }
226              
227             sub bytesFromUnsigned {
228 0     0 0 0 my $class = shift;
229 0         0 my $value = shift;
230              
231 0 0       0 return '' if $value < 1;
232 0 0       0 return pack 'C', $value if $value < 0x100;
233 0 0       0 return pack 'S>', $value if $value < 0x10000;
234              
235             # This works up to 64 bits
236 0         0 my $bytes = pack 'Q>', $value;
237 0         0 my $pos = 0;
238 0         0 $pos += 1 while substr($bytes, $pos, 1) eq "\0";
239 0         0 return substr($bytes, $pos);
240             }
241              
242             sub bytesFromFloat32 {
243 0     0 0 0 my $class = shift;
244 0         0 my $value = shift;
245 0         0 pack('f', $value) }
246             sub bytesFromFloat64 {
247 0     0 0 0 my $class = shift;
248 0         0 my $value = shift;
249 0         0 pack('d', $value) }
250              
251             sub booleanFromBytes {
252 0     0 0 0 my $class = shift;
253 0         0 my $bytes = shift;
254              
255 0         0 return length $bytes > 0;
256             }
257              
258             sub integerFromBytes {
259 0     0 0 0 my $class = shift;
260 0         0 my $bytes = shift;
261              
262 0 0       0 return 0 if ! length $bytes;
263 0         0 my $value = unpack('C', substr($bytes, 0, 1));
264 0 0       0 $value -= 0x100 if $value & 0x80;
265 0         0 for my $i (1 .. length($bytes) - 1) {
266 0         0 $value *= 256;
267 0         0 $value += unpack('C', substr($bytes, $i, 1));
268             }
269 0         0 return $value;
270             }
271              
272             sub unsignedFromBytes {
273 0     0 0 0 my $class = shift;
274 0         0 my $bytes = shift;
275              
276 0         0 my $value = 0;
277 0         0 for my $i (0 .. length($bytes) - 1) {
278 0         0 $value *= 256;
279 0         0 $value += unpack('C', substr($bytes, $i, 1));
280             }
281 0         0 return $value;
282             }
283              
284             sub floatFromBytes {
285 0     0 0 0 my $class = shift;
286 0         0 my $bytes = shift;
287              
288 0 0       0 return unpack('f', $bytes) if length $bytes == 4;
289 0 0       0 return unpack('d', $bytes) if length $bytes == 8;
290 0         0 return undef;
291             }
292              
293             # Initial counter value for AES in CTR mode
294 0     0 0 0 sub zeroCTR { "\0" x 16 }
295              
296             my $emptyBytesHash = CDS::Hash->fromHex('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855');
297 0     0 0 0 sub emptyBytesHash { $emptyBytesHash }
298              
299             # Checks if a box label is valid.
300             sub isValidBoxLabel {
301 0     0 0 0 my $class = shift;
302 0         0 my $label = shift;
303 0 0 0     0 $label eq 'messages' || $label eq 'private' || $label eq 'public' }
304              
305             # Groups box additions or removals by account hash and box label.
306             sub groupedBoxOperations {
307 0     0 0 0 my $class = shift;
308 0         0 my $operations = shift;
309              
310 0         0 my %byAccountHash;
311 0         0 for my $operation (@$operations) {
312 0         0 my $accountHashBytes = $operation->{accountHash}->bytes;
313 0 0       0 $byAccountHash{$accountHashBytes} = {accountHash => $operation->{accountHash}, byBoxLabel => {}} if ! exists $byAccountHash{$accountHashBytes};
314 0         0 my $byBoxLabel = $byAccountHash{$accountHashBytes}->{byBoxLabel};
315 0         0 my $boxLabel = $operation->{boxLabel};
316 0 0       0 $byBoxLabel->{$boxLabel} = [] if ! exists $byBoxLabel->{$boxLabel};
317 0         0 push @{$byBoxLabel->{$boxLabel}}, $operation;
  0         0  
318             }
319              
320 0         0 return values %byAccountHash;
321             }
322              
323             ### Open envelopes ###
324              
325             sub verifyEnvelopeSignature {
326 0     0 0 0 my $class = shift;
327 0 0 0     0 my $envelope = shift; die 'wrong type '.ref($envelope).' for $envelope' if defined $envelope && ref $envelope ne 'CDS::Record';
  0         0  
328 0 0 0     0 my $publicKey = shift; die 'wrong type '.ref($publicKey).' for $publicKey' if defined $publicKey && ref $publicKey ne 'CDS::PublicKey';
  0         0  
329 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
330              
331             # Read the signature
332 0         0 my $signature = $envelope->child('signature')->bytesValue;
333 0 0       0 return if length $signature < 1;
334              
335             # Verify the signature
336 0 0       0 return if ! $publicKey->verifyHash($hash, $signature);
337 0         0 return 1;
338             }
339              
340             # The result of parsing an ACCOUNT token (see Token.pm).
341             package CDS::AccountToken;
342              
343             sub new {
344 0     0   0 my $class = shift;
345 0         0 my $cliStore = shift;
346 0 0 0     0 my $actorHash = shift; die 'wrong type '.ref($actorHash).' for $actorHash' if defined $actorHash && ref $actorHash ne 'CDS::Hash';
  0         0  
347              
348 0         0 return bless {
349             cliStore => $cliStore,
350             actorHash => $actorHash,
351             };
352             }
353              
354 0     0   0 sub cliStore { shift->{cliStore} }
355 0     0   0 sub actorHash { shift->{actorHash} }
356             sub url {
357 0     0   0 my $o = shift;
358 0         0 $o->{cliStore}->url.'/accounts/'.$o->{actorHash}->hex }
359              
360             package CDS::ActorGroup;
361              
362             # Members must be sorted in descending revision order, such that the member with the most recent revision is first. Members must not include any revoked actors.
363             sub new {
364 0     0   0 my $class = shift;
365 0         0 my $members = shift;
366 0         0 my $entrustedActorsRevision = shift;
367 0         0 my $entrustedActors = shift;
368              
369             # Create the cache for the "contains" method
370 0         0 my $containCache = {};
371 0         0 for my $member (@$members) {
372 0         0 $containCache->{$member->actorOnStore->publicKey->hash->bytes} = 1;
373             }
374              
375 0         0 return bless {
376             members => $members,
377             entrustedActorsRevision => $entrustedActorsRevision,
378             entrustedActors => $entrustedActors,
379             containsCache => $containCache,
380             };
381             }
382              
383             sub members {
384 0     0   0 my $o = shift;
385 0         0 @{$o->{members}} }
  0         0  
386 0     0   0 sub entrustedActorsRevision { shift->{entrustedActorsRevision} }
387             sub entrustedActors {
388 0     0   0 my $o = shift;
389 0         0 @{$o->{entrustedActors}} }
  0         0  
390              
391             # Checks whether the actor group contains at least one active member.
392             sub isActive {
393 0     0   0 my $o = shift;
394              
395 0         0 for my $member (@{$o->{members}}) {
  0         0  
396 0 0       0 return 1 if $member->isActive;
397             }
398 0         0 return;
399             }
400              
401             # Returns the most recent active member, the most recent idle member, or undef if the group is empty.
402             sub leader {
403 0     0   0 my $o = shift;
404              
405 0         0 for my $member (@{$o->{members}}) {
  0         0  
406 0 0       0 return $member if $member->isActive;
407             }
408 0         0 return $o->{members}->[0];
409             }
410              
411             # Returns true if the account belongs to this actor group.
412             # Note that multiple (different) actor groups may claim that the account belongs to them. In practice, an account usually belongs to one actor group.
413             sub contains {
414 0     0   0 my $o = shift;
415 0 0 0     0 my $actorHash = shift; die 'wrong type '.ref($actorHash).' for $actorHash' if defined $actorHash && ref $actorHash ne 'CDS::Hash';
  0         0  
416              
417 0         0 return exists $o->{containsCache}->{$actorHash->bytes};
418             }
419              
420             # Returns true if the account is entrusted by this actor group.
421             sub entrusts {
422 0     0   0 my $o = shift;
423 0 0 0     0 my $actorHash = shift; die 'wrong type '.ref($actorHash).' for $actorHash' if defined $actorHash && ref $actorHash ne 'CDS::Hash';
  0         0  
424              
425 0         0 for my $actor (@{$o->{entrustedActors}}) {
  0         0  
426 0 0       0 return 1 if $actorHash->equals($actor->publicKey->hash);
427             }
428 0         0 return;
429             }
430              
431             # Returns all public keys.
432             sub publicKeys {
433 0     0   0 my $o = shift;
434              
435 0         0 my @publicKeys;
436 0         0 for my $member (@{$o->{members}}) {
  0         0  
437 0         0 push @publicKeys, $member->actorOnStore->publicKey;
438             }
439 0         0 for my $actor (@{$o->{entrustedActors}}) {
  0         0  
440 0         0 push @publicKeys, $actor->actorOnStore->publicKey;
441             }
442 0         0 return @publicKeys;
443             }
444              
445             # Returns an ActorGroupBuilder with all members and entrusted keys of this ActorGroup.
446             sub toBuilder {
447 0     0   0 my $o = shift;
448              
449 0         0 my $builder = CDS::ActorGroupBuilder->new;
450 0         0 $builder->mergeEntrustedActors($o->{entrustedActorsRevision});
451 0         0 for my $member (@{$o->{members}}) {
  0         0  
452 0         0 my $publicKey = $member->actorOnStore->publicKey;
453 0         0 $builder->addKnownPublicKey($publicKey);
454 0 0       0 $builder->addMember($publicKey->hash, $member->storeUrl, $member->revision, $member->isActive ? 'active' : 'idle');
455             }
456 0         0 for my $actor (@{$o->{entrustedActors}}) {
  0         0  
457 0         0 my $publicKey = $actor->actorOnStore->publicKey;
458 0         0 $builder->addKnownPublicKey($publicKey);
459 0         0 $builder->addEntrustedActor($publicKey->hash, $actor->storeUrl);
460             }
461 0         0 return $builder;
462             }
463              
464             package CDS::ActorGroup::EntrustedActor;
465              
466             sub new {
467 0     0   0 my $class = shift;
468 0 0 0     0 my $actorOnStore = shift; die 'wrong type '.ref($actorOnStore).' for $actorOnStore' if defined $actorOnStore && ref $actorOnStore ne 'CDS::ActorOnStore';
  0         0  
469 0         0 my $storeUrl = shift;
470              
471 0         0 return bless {
472             actorOnStore => $actorOnStore,
473             storeUrl => $storeUrl,
474             };
475             }
476              
477 0     0   0 sub actorOnStore { shift->{actorOnStore} }
478 0     0   0 sub storeUrl { shift->{storeUrl} }
479              
480             package CDS::ActorGroup::Member;
481              
482             sub new {
483 0     0   0 my $class = shift;
484 0 0 0     0 my $actorOnStore = shift; die 'wrong type '.ref($actorOnStore).' for $actorOnStore' if defined $actorOnStore && ref $actorOnStore ne 'CDS::ActorOnStore';
  0         0  
485 0         0 my $storeUrl = shift;
486 0         0 my $revision = shift;
487 0         0 my $isActive = shift;
488              
489 0         0 return bless {
490             actorOnStore => $actorOnStore,
491             storeUrl => $storeUrl,
492             revision => $revision,
493             isActive => $isActive,
494             };
495             }
496              
497 0     0   0 sub actorOnStore { shift->{actorOnStore} }
498 0     0   0 sub storeUrl { shift->{storeUrl} }
499 0     0   0 sub revision { shift->{revision} }
500 0     0   0 sub isActive { shift->{isActive} }
501              
502             package CDS::ActorGroupBuilder;
503              
504             sub new {
505 0     0   0 my $class = shift;
506              
507 0         0 return bless {
508             knownPublicKeys => {}, # A hashref of known public keys (e.g. from the existing actor group)
509             members => {}, # Members by URL
510             entrustedActorsRevision => 0, # Revision of the list of entrusted actors
511             entrustedActors => {}, # Entrusted actors by hash
512             };
513             }
514              
515             sub members {
516 0     0   0 my $o = shift;
517 0         0 values %{$o->{members}} }
  0         0  
518 0     0   0 sub entrustedActorsRevision { shift->{entrustedActorsRevision} }
519             sub entrustedActors {
520 0     0   0 my $o = shift;
521 0         0 values %{$o->{entrustedActors}} }
  0         0  
522 0     0   0 sub knownPublicKeys { shift->{knownPublicKeys} }
523              
524             sub addKnownPublicKey {
525 0     0   0 my $o = shift;
526 0 0 0     0 my $publicKey = shift; die 'wrong type '.ref($publicKey).' for $publicKey' if defined $publicKey && ref $publicKey ne 'CDS::PublicKey';
  0         0  
527              
528 0         0 $o->{publicKeys}->{$publicKey->hash->bytes} = $publicKey;
529             }
530              
531             sub addMember {
532 0     0   0 my $o = shift;
533 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
534 0         0 my $storeUrl = shift;
535 0   0     0 my $revision = shift // 0;
536 0   0     0 my $status = shift // 'active';
537              
538 0         0 my $url = $storeUrl.'/accounts/'.$hash->hex;
539 0         0 my $member = $o->{members}->{$url};
540 0 0 0     0 return if $member && $revision <= $member->revision;
541 0         0 $o->{members}->{$url} = CDS::ActorGroupBuilder::Member->new($hash, $storeUrl, $revision, $status);
542 0         0 return 1;
543             }
544              
545             sub removeMember {
546 0     0   0 my $o = shift;
547 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
548 0         0 my $storeUrl = shift;
549              
550 0         0 my $url = $storeUrl.'/accounts/'.$hash->hex;
551 0         0 delete $o->{members}->{$url};
552             }
553              
554             sub parseMembers {
555 0     0   0 my $o = shift;
556 0 0 0     0 my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0         0  
557 0         0 my $linkedPublicKeys = shift;
558              
559 0 0       0 die 'linked public keys?' if ! defined $linkedPublicKeys;
560 0         0 for my $storeRecord ($record->children) {
561 0         0 my $accountStoreUrl = $storeRecord->asText;
562              
563 0         0 for my $statusRecord ($storeRecord->children) {
564 0         0 my $status = $statusRecord->bytes;
565              
566 0         0 for my $child ($statusRecord->children) {
567 0 0       0 my $hash = $linkedPublicKeys ? $child->hash : CDS::Hash->fromBytes($child->bytes);
568 0   0     0 $o->addMember($hash // next, $accountStoreUrl, $child->integerValue, $status);
569             }
570             }
571             }
572             }
573              
574             sub mergeEntrustedActors {
575 0     0   0 my $o = shift;
576 0         0 my $revision = shift;
577              
578 0 0       0 return if $revision <= $o->{entrustedActorsRevision};
579 0         0 $o->{entrustedActorsRevision} = $revision;
580 0         0 $o->{entrustedActors} = {};
581 0         0 return 1;
582             }
583              
584             sub addEntrustedActor {
585 0     0   0 my $o = shift;
586 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
587 0         0 my $storeUrl = shift;
588              
589 0         0 my $actor = CDS::ActorGroupBuilder::EntrustedActor->new($hash, $storeUrl);
590 0         0 $o->{entrustedActors}->{$hash->bytes} = $actor;
591             }
592              
593             sub removeEntrustedActor {
594 0     0   0 my $o = shift;
595 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
596              
597 0         0 delete $o->{entrustedActors}->{$hash->bytes};
598             }
599              
600             sub parseEntrustedActors {
601 0     0   0 my $o = shift;
602 0 0 0     0 my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0         0  
603 0         0 my $linkedPublicKeys = shift;
604              
605 0         0 for my $revisionRecord ($record->children) {
606 0 0       0 next if ! $o->mergeEntrustedActors($revisionRecord->asInteger);
607 0         0 $o->parseEntrustedActorList($revisionRecord, $linkedPublicKeys);
608             }
609             }
610              
611             sub parseEntrustedActorList {
612 0     0   0 my $o = shift;
613 0 0 0     0 my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0         0  
614 0         0 my $linkedPublicKeys = shift;
615              
616 0 0       0 die 'linked public keys?' if ! defined $linkedPublicKeys;
617 0         0 for my $storeRecord ($record->children) {
618 0         0 my $storeUrl = $storeRecord->asText;
619              
620 0         0 for my $child ($storeRecord->children) {
621 0 0       0 my $hash = $linkedPublicKeys ? $child->hash : CDS::Hash->fromBytes($child->bytes);
622 0   0     0 $o->addEntrustedActor($hash // next, $storeUrl);
623             }
624             }
625             }
626              
627             sub parse {
628 0     0   0 my $o = shift;
629 0 0 0     0 my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0         0  
630 0         0 my $linkedPublicKeys = shift;
631              
632 0         0 $o->parseMembers($record->child('actor group'), $linkedPublicKeys);
633 0         0 $o->parseEntrustedActors($record->child('entrusted actors'), $linkedPublicKeys);
634             }
635              
636             sub load {
637 0     0   0 my $o = shift;
638 0         0 my $store = shift;
639 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
640 0         0 my $delegate = shift;
641              
642 0         0 return CDS::LoadActorGroup->load($o, $store, $keyPair, $delegate);
643             }
644              
645             sub discover {
646 0     0   0 my $o = shift;
647 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
648 0         0 my $delegate = shift;
649              
650 0         0 return CDS::DiscoverActorGroup->discover($o, $keyPair, $delegate);
651             }
652              
653             # Serializes the actor group to a record that can be passed to parse.
654             sub addToRecord {
655 0     0   0 my $o = shift;
656 0 0 0     0 my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0         0  
657 0         0 my $linkedPublicKeys = shift;
658              
659 0 0       0 die 'linked public keys?' if ! defined $linkedPublicKeys;
660              
661 0         0 my $actorGroupRecord = $record->add('actor group');
662 0         0 my $currentStoreUrl = undef;
663 0         0 my $currentStoreRecord = undef;
664 0         0 my $currentStatus = undef;
665 0         0 my $currentStatusRecord = undef;
666 0 0       0 for my $member (sort { $a->storeUrl cmp $b->storeUrl || CDS->booleanCompare($b->status, $a->status) } $o->members) {
  0         0  
667 0 0       0 next if ! $member->revision;
668              
669 0 0 0     0 if (! defined $currentStoreUrl || $currentStoreUrl ne $member->storeUrl) {
670 0         0 $currentStoreUrl = $member->storeUrl;
671 0         0 $currentStoreRecord = $actorGroupRecord->addText($currentStoreUrl);
672 0         0 $currentStatus = undef;
673 0         0 $currentStatusRecord = undef;
674             }
675              
676 0 0 0     0 if (! defined $currentStatus || $currentStatus ne $member->status) {
677 0         0 $currentStatus = $member->status;
678 0         0 $currentStatusRecord = $currentStoreRecord->add($currentStatus);
679             }
680              
681 0 0       0 my $hashRecord = $linkedPublicKeys ? $currentStatusRecord->addHash($member->hash) : $currentStatusRecord->add($member->hash->bytes);
682 0         0 $hashRecord->addInteger($member->revision);
683             }
684              
685 0 0       0 if ($o->{entrustedActorsRevision}) {
686 0         0 my $listRecord = $o->entrustedActorListToRecord($linkedPublicKeys);
687 0         0 $record->add('entrusted actors')->addInteger($o->{entrustedActorsRevision})->addRecord($listRecord->children);
688             }
689             }
690              
691             sub toRecord {
692 0     0   0 my $o = shift;
693 0         0 my $linkedPublicKeys = shift;
694              
695 0         0 my $record = CDS::Record->new;
696 0         0 $o->addToRecord($record, $linkedPublicKeys);
697 0         0 return $record;
698             }
699              
700             sub entrustedActorListToRecord {
701 0     0   0 my $o = shift;
702 0         0 my $linkedPublicKeys = shift;
703              
704 0         0 my $record = CDS::Record->new;
705 0         0 my $currentStoreUrl = undef;
706 0         0 my $currentStoreRecord = undef;
707 0         0 for my $actor ($o->entrustedActors) {
708 0 0 0     0 if (! defined $currentStoreUrl || $currentStoreUrl ne $actor->storeUrl) {
709 0         0 $currentStoreUrl = $actor->storeUrl;
710 0         0 $currentStoreRecord = $record->addText($currentStoreUrl);
711             }
712              
713 0 0       0 $linkedPublicKeys ? $currentStoreRecord->addHash($actor->hash) : $currentStoreRecord->add($actor->hash->bytes);
714             }
715              
716 0         0 return $record;
717             }
718              
719             package CDS::ActorGroupBuilder::EntrustedActor;
720              
721             sub new {
722 0     0   0 my $class = shift;
723 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
724 0         0 my $storeUrl = shift;
725              
726 0         0 return bless {
727             hash => $hash,
728             storeUrl => $storeUrl,
729             };
730             }
731              
732 0     0   0 sub hash { shift->{hash} }
733 0     0   0 sub storeUrl { shift->{storeUrl} }
734              
735             package CDS::ActorGroupBuilder::Member;
736              
737             sub new {
738 0     0   0 my $class = shift;
739 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
740 0         0 my $storeUrl = shift;
741 0         0 my $revision = shift;
742 0         0 my $status = shift;
743              
744 0         0 return bless {
745             hash => $hash,
746             storeUrl => $storeUrl,
747             revision => $revision,
748             status => $status,
749             };
750             }
751              
752 0     0   0 sub hash { shift->{hash} }
753 0     0   0 sub storeUrl { shift->{storeUrl} }
754 0     0   0 sub revision { shift->{revision} }
755 0     0   0 sub status { shift->{status} }
756              
757             # The result of parsing an ACTORGROUP token (see Token.pm).
758             package CDS::ActorGroupToken;
759              
760             sub new {
761 0     0   0 my $class = shift;
762 0         0 my $label = shift;
763 0 0 0     0 my $actorGroup = shift; die 'wrong type '.ref($actorGroup).' for $actorGroup' if defined $actorGroup && ref $actorGroup ne 'CDS::ActorGroup';
  0         0  
764              
765 0         0 return bless {
766             label => $label,
767             actorGroup => $actorGroup,
768             };
769             }
770              
771 0     0   0 sub label { shift->{label} }
772 0     0   0 sub actorGroup { shift->{actorGroup} }
773              
774             # A public key and a store.
775             package CDS::ActorOnStore;
776              
777             sub new {
778 0     0   0 my $class = shift;
779 0 0 0     0 my $publicKey = shift; die 'wrong type '.ref($publicKey).' for $publicKey' if defined $publicKey && ref $publicKey ne 'CDS::PublicKey';
  0         0  
780 0         0 my $store = shift;
781              
782 0         0 return bless {
783             publicKey => $publicKey,
784             store => $store
785             };
786             }
787              
788 0     0   0 sub publicKey { shift->{publicKey} }
789 0     0   0 sub store { shift->{store} }
790              
791             sub equals {
792 0     0   0 my $this = shift;
793 0         0 my $that = shift;
794              
795 0 0 0     0 return 1 if ! defined $this && ! defined $that;
796 0 0 0     0 return if ! defined $this || ! defined $that;
797 0   0     0 return $this->{store}->id eq $that->{store}->id && $this->{publicKey}->{hash}->equals($that->{publicKey}->{hash});
798             }
799              
800             package CDS::ActorWithDocument;
801              
802             sub new {
803 0     0   0 my $class = shift;
804 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
805 0         0 my $storageStore = shift;
806 0         0 my $messagingStore = shift;
807 0         0 my $messagingStoreUrl = shift;
808 0         0 my $publicKeyCache = shift;
809              
810 0         0 my $o = bless {
811             keyPair => $keyPair,
812             storageStore => $storageStore,
813             messagingStore => $messagingStore,
814             messagingStoreUrl => $messagingStoreUrl,
815             groupDataHandlers => [],
816             }, $class;
817              
818             # Private data on the storage store
819 0         0 $o->{storagePrivateRoot} = CDS::PrivateRoot->new($keyPair, $storageStore, $o);
820 0         0 $o->{groupDocument} = CDS::RootDocument->new($o->{storagePrivateRoot}, 'group data');
821 0         0 $o->{localDocument} = CDS::RootDocument->new($o->{storagePrivateRoot}, 'local data');
822              
823             # Private data on the messaging store
824 0 0       0 $o->{messagingPrivateRoot} = $storageStore->id eq $messagingStore->id ? $o->{storagePrivateRoot} : CDS::PrivateRoot->new($keyPair, $messagingStore, $o);
825 0         0 $o->{sentList} = CDS::SentList->new($o->{messagingPrivateRoot});
826 0         0 $o->{sentListReady} = 0;
827              
828             # Group data sharing
829 0         0 $o->{groupDataSharer} = CDS::GroupDataSharer->new($o);
830 0         0 $o->{groupDataSharer}->addDataHandler($o->{groupDocument}->label, $o->{groupDocument});
831              
832             # Selectors
833 0         0 $o->{groupRoot} = $o->{groupDocument}->root;
834 0         0 $o->{localRoot} = $o->{localDocument}->root;
835 0         0 $o->{publicDataSelector} = $o->{groupRoot}->child('public data');
836 0         0 $o->{actorGroupSelector} = $o->{groupRoot}->child('actor group');
837 0         0 $o->{actorSelector} = $o->{actorGroupSelector}->child(substr($keyPair->publicKey->hash->bytes, 0, 16));
838 0         0 $o->{entrustedActorsSelector} = $o->{groupRoot}->child('entrusted actors');
839              
840             # Message reader
841 0         0 my $pool = CDS::MessageBoxReaderPool->new($keyPair, $publicKeyCache, $o);
842 0         0 $o->{messageBoxReader} = CDS::MessageBoxReader->new($pool, CDS::ActorOnStore->new($keyPair->publicKey, $messagingStore), CDS->HOUR);
843              
844             # Active actor group members and entrusted keys
845 0         0 $o->{cachedGroupDataMembers} = {};
846 0         0 $o->{cachedEntrustedKeys} = {};
847 0         0 return $o;
848             }
849              
850 0     0   0 sub keyPair { shift->{keyPair} }
851 0     0   0 sub storageStore { shift->{storageStore} }
852 0     0   0 sub messagingStore { shift->{messagingStore} }
853 0     0   0 sub messagingStoreUrl { shift->{messagingStoreUrl} }
854              
855 0     0   0 sub storagePrivateRoot { shift->{storagePrivateRoot} }
856 0     0   0 sub groupDocument { shift->{groupDocument} }
857 0     0   0 sub localDocument { shift->{localDocument} }
858              
859 0     0   0 sub messagingPrivateRoot { shift->{messagingPrivateRoot} }
860 0     0   0 sub sentList { shift->{sentList} }
861 0     0   0 sub sentListReady { shift->{sentListReady} }
862              
863 0     0   0 sub groupDataSharer { shift->{groupDataSharer} }
864              
865 0     0   0 sub groupRoot { shift->{groupRoot} }
866 0     0   0 sub localRoot { shift->{localRoot} }
867 0     0   0 sub publicDataSelector { shift->{publicDataSelector} }
868 0     0   0 sub actorGroupSelector { shift->{actorGroupSelector} }
869 0     0   0 sub actorSelector { shift->{actorSelector} }
870 0     0   0 sub entrustedActorsSelector { shift->{entrustedActorsSelector} }
871              
872             ### Our own actor ###
873              
874             sub isMe {
875 0     0   0 my $o = shift;
876 0 0 0     0 my $actorHash = shift; die 'wrong type '.ref($actorHash).' for $actorHash' if defined $actorHash && ref $actorHash ne 'CDS::Hash';
  0         0  
877              
878 0         0 return $o->{keyPair}->publicKey->hash->equals($actorHash);
879             }
880              
881             sub setName {
882 0     0   0 my $o = shift;
883 0         0 my $name = shift;
884              
885 0         0 $o->{actorSelector}->child('name')->set($name);
886             }
887              
888             sub getName {
889 0     0   0 my $o = shift;
890              
891 0         0 return $o->{actorSelector}->child('name')->textValue;
892             }
893              
894             sub updateMyRegistration {
895 0     0   0 my $o = shift;
896              
897 0         0 $o->{actorSelector}->addObject($o->{keyPair}->publicKey->hash, $o->{keyPair}->publicKey->object);
898 0         0 my $record = CDS::Record->new;
899 0         0 $record->add('hash')->addHash($o->{keyPair}->publicKey->hash);
900 0         0 $record->add('store')->addText($o->{messagingStoreUrl});
901 0         0 $o->{actorSelector}->set($record);
902             }
903              
904             sub setMyActiveFlag {
905 0     0   0 my $o = shift;
906 0         0 my $flag = shift;
907              
908 0         0 $o->{actorSelector}->child('active')->setBoolean($flag);
909             }
910              
911             sub setMyGroupDataFlag {
912 0     0   0 my $o = shift;
913 0         0 my $flag = shift;
914              
915 0         0 $o->{actorSelector}->child('group data')->setBoolean($flag);
916             }
917              
918             ### Actor group
919              
920             sub isGroupMember {
921 0     0   0 my $o = shift;
922 0 0 0     0 my $actorHash = shift; die 'wrong type '.ref($actorHash).' for $actorHash' if defined $actorHash && ref $actorHash ne 'CDS::Hash';
  0         0  
923              
924 0 0       0 return 1 if $actorHash->equals($o->{keyPair}->publicKey->hash);
925 0   0     0 my $memberSelector = $o->findMember($actorHash) // return;
926 0         0 return ! $memberSelector->child('revoked')->isSet;
927             }
928              
929             sub findMember {
930 0     0   0 my $o = shift;
931 0 0 0     0 my $memberHash = shift; die 'wrong type '.ref($memberHash).' for $memberHash' if defined $memberHash && ref $memberHash ne 'CDS::Hash';
  0         0  
932              
933 0         0 for my $child ($o->{actorGroupSelector}->children) {
934 0         0 my $record = $child->record;
935 0   0     0 my $hash = $record->child('hash')->hashValue // next;
936 0 0       0 next if ! $hash->equals($memberHash);
937 0         0 return $child;
938             }
939              
940 0         0 return;
941             }
942              
943             sub forgetOldIdleActors {
944 0     0   0 my $o = shift;
945 0         0 my $limit = shift;
946              
947 0         0 for my $child ($o->{actorGroupSelector}->children) {
948 0 0       0 next if $child->child('active')->booleanValue;
949 0 0       0 next if $child->child('group data')->booleanValue;
950 0 0       0 next if $child->revision > $limit;
951 0         0 $child->forgetBranch;
952             }
953             }
954              
955             ### Group data members
956              
957             sub getGroupDataMembers {
958 0     0   0 my $o = shift;
959              
960             # Update the cached list
961 0         0 for my $child ($o->{actorGroupSelector}->children) {
962 0         0 my $record = $child->record;
963 0         0 my $hash = $record->child('hash')->hashValue;
964 0 0       0 $hash = undef if $hash->equals($o->{keyPair}->publicKey->hash);
965 0 0       0 $hash = undef if $child->child('revoked')->isSet;
966 0 0       0 $hash = undef if ! $child->child('group data')->isSet;
967              
968             # Remove
969 0 0       0 if (! $hash) {
970 0         0 delete $o->{cachedGroupDataMembers}->{$child->label};
971 0         0 next;
972             }
973              
974             # Keep
975 0         0 my $member = $o->{cachedGroupDataMembers}->{$child->label};
976 0         0 my $storeUrl = $record->child('store')->textValue;
977 0 0 0     0 next if $member && $member->storeUrl eq $storeUrl && $member->actorOnStore->publicKey->hash->equals($hash);
      0        
978              
979             # Verify the store
980 0         0 my $store = $o->onVerifyMemberStore($storeUrl, $child);
981 0 0       0 if (! $store) {
982 0         0 delete $o->{cachedGroupDataMembers}->{$child->label};
983 0         0 next;
984             }
985              
986             # Reuse the public key and add
987 0 0 0     0 if ($member && $member->actorOnStore->publicKey->hash->equals($hash)) {
988 0         0 my $actorOnStore = CDS::ActorOnStore->new($member->actorOnStore->publicKey, $store);
989 0         0 $o->{cachedEntrustedKeys}->{$child->label} = {storeUrl => $storeUrl, actorOnStore => $actorOnStore};
990             }
991              
992             # Get the public key and add
993 0         0 my ($publicKey, $invalidReason, $storeError) = $o->{keyPair}->getPublicKey($hash, $o->{groupDocument}->unsaved);
994 0 0       0 return if defined $storeError;
995 0 0       0 if (defined $invalidReason) {
996 0         0 delete $o->{cachedGroupDataMembers}->{$child->label};
997 0         0 next;
998             }
999              
1000 0         0 my $actorOnStore = CDS::ActorOnStore->new($publicKey, $store);
1001 0         0 $o->{cachedGroupDataMembers}->{$child->label} = {storeUrl => $storeUrl, actorOnStore => $actorOnStore};
1002             }
1003              
1004             # Return the current list
1005 0         0 return [map { $_->{actorOnStore} } values %{$o->{cachedGroupDataMembers}}];
  0         0  
  0         0  
1006             }
1007              
1008             ### Entrusted actors
1009              
1010             sub entrust {
1011 0     0   0 my $o = shift;
1012 0         0 my $storeUrl = shift;
1013 0 0 0     0 my $publicKey = shift; die 'wrong type '.ref($publicKey).' for $publicKey' if defined $publicKey && ref $publicKey ne 'CDS::PublicKey';
  0         0  
1014              
1015             # TODO: this is not compatible with the Java implementation (which uses a record with "hash" and "store")
1016 0         0 my $selector = $o->{entrustedActorsSelector};
1017 0         0 my $builder = CDS::ActorGroupBuilder->new;
1018 0         0 $builder->parseEntrustedActorList($selector->record, 1);
1019 0         0 $builder->removeEntrustedActor($publicKey->hash);
1020 0         0 $builder->addEntrustedActor($storeUrl, $publicKey->hash);
1021 0         0 $selector->addObject($publicKey->hash, $publicKey->object);
1022 0         0 $selector->set($builder->entrustedActorListToRecord(1));
1023 0         0 $o->{cachedEntrustedKeys}->{$publicKey->hash->bytes} = $publicKey;
1024             }
1025              
1026             sub doNotEntrust {
1027 0     0   0 my $o = shift;
1028 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
1029              
1030 0         0 my $selector = $o->{entrustedActorsSelector};
1031 0         0 my $builder = CDS::ActorGroupBuilder->new;
1032 0         0 $builder->parseEntrustedActorList($selector->record, 1);
1033 0         0 $builder->removeEntrustedActor($hash);
1034 0         0 $selector->set($builder->entrustedActorListToRecord(1));
1035 0         0 delete $o->{cachedEntrustedKeys}->{$hash->bytes};
1036             }
1037              
1038             sub getEntrustedKeys {
1039 0     0   0 my $o = shift;
1040              
1041 0         0 my $entrustedKeys = [];
1042 0         0 for my $storeRecord ($o->{entrustedActorsSelector}->record->children) {
1043 0         0 for my $child ($storeRecord->children) {
1044 0   0     0 my $hash = $child->hash // next;
1045 0   0     0 push @$entrustedKeys, $o->getEntrustedKey($hash) // next;
1046             }
1047             }
1048              
1049             # We could remove unused keys from $o->{cachedEntrustedKeys} here, but since this is
1050             # such a rare event, and doesn't consume a lot of memory, this would be overkill.
1051              
1052 0         0 return $entrustedKeys;
1053             }
1054              
1055             sub getEntrustedKey {
1056 0     0   0 my $o = shift;
1057 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
1058              
1059 0         0 my $entrustedKey = $o->{cachedEntrustedKeys}->{$hash->bytes};
1060 0 0       0 return $entrustedKey if $entrustedKey;
1061              
1062 0         0 my ($publicKey, $invalidReason, $storeError) = $o->{keyPair}->getPublicKey($hash, $o->{groupDocument}->unsaved);
1063 0 0       0 return if defined $storeError;
1064 0 0       0 return if defined $invalidReason;
1065 0         0 $o->{cachedEntrustedKeys}->{$hash->bytes} = $publicKey;
1066 0         0 return $publicKey;
1067             }
1068              
1069             ### Private data
1070              
1071             sub procurePrivateData {
1072 0     0   0 my $o = shift;
1073 0   0     0 my $interval = shift // CDS->DAY;
1074              
1075 0   0     0 $o->{storagePrivateRoot}->procure($interval) // return;
1076 0   0     0 $o->{groupDocument}->read // return;
1077 0   0     0 $o->{localDocument}->read // return;
1078 0         0 return 1;
1079             }
1080              
1081             sub savePrivateDataAndShareGroupData {
1082 0     0   0 my $o = shift;
1083              
1084 0         0 $o->{localDocument}->save;
1085 0         0 $o->{groupDocument}->save;
1086 0         0 $o->groupDataSharer->share;
1087 0   0     0 my $entrustedKeys = $o->getEntrustedKeys // return;
1088 0         0 my ($ok, $missingHash) = $o->{storagePrivateRoot}->save($entrustedKeys);
1089 0 0       0 return 1 if $ok;
1090 0 0       0 $o->onMissingObject($missingHash) if $missingHash;
1091 0         0 return;
1092             }
1093              
1094             # abstract sub onVerifyMemberStore($storeUrl, $selector)
1095             # abstract sub onPrivateRootReadingInvalidEntry($o, $source, $reason)
1096             # abstract sub onMissingObject($missingHash)
1097              
1098             ### Sending messages
1099              
1100             sub procureSentList {
1101 0     0   0 my $o = shift;
1102 0   0     0 my $interval = shift // CDS->DAY;
1103              
1104 0   0     0 $o->{messagingPrivateRoot}->procure($interval) // return;
1105 0   0     0 $o->{sentList}->read // return;
1106 0         0 $o->{sentListReady} = 1;
1107 0         0 return 1;
1108             }
1109              
1110             sub openMessageChannel {
1111 0     0   0 my $o = shift;
1112 0         0 my $label = shift;
1113 0         0 my $validity = shift;
1114              
1115 0         0 return CDS::MessageChannel->new($o, $label, $validity);
1116             }
1117              
1118             sub sendMessages {
1119 0     0   0 my $o = shift;
1120              
1121 0 0       0 return 1 if ! $o->{sentList}->hasChanges;
1122 0         0 $o->{sentList}->save;
1123 0   0     0 my $entrustedKeys = $o->getEntrustedKeys // return;
1124 0         0 my ($ok, $missingHash) = $o->{messagingPrivateRoot}->save($entrustedKeys);
1125 0 0       0 return 1 if $ok;
1126 0 0       0 $o->onMissingObject($missingHash) if $missingHash;
1127 0         0 return;
1128             }
1129              
1130             ### Receiving messages
1131              
1132             # abstract sub onMessageBoxVerifyStore($o, $senderStoreUrl, $hash, $envelope, $senderHash)
1133             # abstract sub onMessage($o, $message)
1134             # abstract sub onInvalidMessage($o, $source, $reason)
1135             # abstract sub onMessageBoxEntry($o, $message)
1136             # abstract sub onMessageBoxInvalidEntry($o, $source, $reason)
1137              
1138             ### Announcing ###
1139              
1140             sub announceOnAllStores {
1141 0     0   0 my $o = shift;
1142              
1143 0         0 $o->announce($o->{storageStore});
1144 0 0       0 $o->announce($o->{messagingStore}) if $o->{messagingStore}->id ne $o->{storageStore}->id;
1145             }
1146              
1147             sub announce {
1148 0     0   0 my $o = shift;
1149 0         0 my $store = shift;
1150              
1151 0 0       0 die 'probably calling old announce, which should now be announceOnAllStores' if ! defined $store;
1152              
1153             # Prepare the actor group
1154 0         0 my $builder = CDS::ActorGroupBuilder->new;
1155              
1156 0         0 my $me = $o->keyPair->publicKey->hash;
1157 0         0 $builder->addMember($me, $o->messagingStoreUrl, CDS->now, 'active');
1158 0         0 for my $child ($o->actorGroupSelector->children) {
1159 0         0 my $record = $child->record;
1160 0   0     0 my $hash = $record->child('hash')->hashValue // next;
1161 0 0       0 next if $hash->equals($me);
1162 0         0 my $storeUrl = $record->child('store')->textValue;
1163 0         0 my $revokedSelector = $child->child('revoked');
1164 0         0 my $activeSelector = $child->child('active');
1165 0         0 my $revision = CDS->max($child->revision, $revokedSelector->revision, $activeSelector->revision);
1166 0 0       0 my $actorStatus = $revokedSelector->booleanValue ? 'revoked' : $activeSelector->booleanValue ? 'active' : 'idle';
    0          
1167 0         0 $builder->addMember($hash, $storeUrl, $revision, $actorStatus);
1168             }
1169              
1170 0 0       0 $builder->parseEntrustedActorList($o->entrustedActorsSelector->record, 1) if $builder->mergeEntrustedActors($o->entrustedActorsSelector->revision);
1171              
1172             # Create the card
1173 0         0 my $card = $builder->toRecord(0);
1174 0         0 $card->add('public key')->addHash($o->{keyPair}->publicKey->hash);
1175              
1176             # Add the public data
1177 0         0 for my $child ($o->publicDataSelector->children) {
1178 0         0 my $childRecord = $child->record;
1179 0         0 $card->addRecord($childRecord->children);
1180             }
1181              
1182             # Create an unsaved state
1183 0         0 my $unsaved = CDS::Unsaved->new($o->publicDataSelector->document->unsaved);
1184              
1185             # Add the public card and the public key
1186 0         0 my $cardObject = $card->toObject;
1187 0         0 my $cardHash = $cardObject->calculateHash;
1188 0         0 $unsaved->state->addObject($cardHash, $cardObject);
1189 0         0 $unsaved->state->addObject($me, $o->keyPair->publicKey->object);
1190              
1191             # Prepare the public envelope
1192 0         0 my $envelopeObject = $o->keyPair->createPublicEnvelope($cardHash)->toObject;
1193 0         0 my $envelopeHash = $envelopeObject->calculateHash;
1194              
1195             # Upload the objects
1196 0         0 my ($missingObject, $transferStore, $transferError) = $o->keyPair->transfer([$cardHash], $unsaved, $store);
1197 0 0       0 return if defined $transferError;
1198 0 0       0 if ($missingObject) {
1199 0         0 $missingObject->{context} = 'announce on '.$store->id;
1200 0         0 $o->onMissingObject($missingObject);
1201 0         0 return;
1202             }
1203              
1204             # Prepare to modify
1205 0         0 my $modifications = CDS::StoreModifications->new;
1206 0         0 $modifications->add($me, 'public', $envelopeHash, $envelopeObject);
1207              
1208             # List the current cards to remove them
1209             # Ignore errors, in the worst case, we are going to have multiple entries in the public box
1210 0         0 my ($hashes, $error) = $store->list($me, 'public', 0, $o->keyPair);
1211 0 0       0 if ($hashes) {
1212 0         0 for my $hash (@$hashes) {
1213 0         0 $modifications->remove($me, 'public', $hash);
1214             }
1215             }
1216              
1217             # Modify the public box
1218 0         0 my $modifyError = $store->modify($modifications, $o->keyPair);
1219 0 0       0 return if defined $modifyError;
1220 0         0 return $envelopeHash, $cardHash;
1221             }
1222              
1223             # The result of parsing a BOX token (see Token.pm).
1224             package CDS::BoxToken;
1225              
1226             sub new {
1227 0     0   0 my $class = shift;
1228 0         0 my $accountToken = shift;
1229 0         0 my $boxLabel = shift;
1230              
1231 0         0 return bless {
1232             accountToken => $accountToken,
1233             boxLabel => $boxLabel
1234             };
1235             }
1236              
1237 0     0   0 sub accountToken { shift->{accountToken} }
1238 0     0   0 sub boxLabel { shift->{boxLabel} }
1239             sub url {
1240 0     0   0 my $o = shift;
1241 0         0 $o->{accountToken}->url.'/'.$o->{boxLabel} }
1242              
1243             package CDS::CLIActor;
1244              
1245 1     1   7719 use parent -norequire, 'CDS::ActorWithDocument';
  1         2  
  1         5  
1246              
1247             sub openOrCreateDefault {
1248 0     0   0 my $class = shift;
1249 0         0 my $ui = shift;
1250              
1251 0         0 $class->open(CDS::Configuration->getOrCreateDefault($ui));
1252             }
1253              
1254             sub open {
1255 0     0   0 my $class = shift;
1256 0         0 my $configuration = shift;
1257              
1258             # Read the store configuration
1259 0         0 my $ui = $configuration->ui;
1260 0         0 my $storeManager = CDS::CLIStoreManager->new($ui);
1261              
1262 0         0 my $storageStoreUrl = $configuration->storageStoreUrl;
1263 0   0     0 my $storageStore = $storeManager->storeForUrl($storageStoreUrl) // return $ui->error('Your storage store "', $storageStoreUrl, '" cannot be accessed. You can set this store in "', $configuration->file('store'), '".');
1264              
1265 0         0 my $messagingStoreUrl = $configuration->messagingStoreUrl;
1266 0   0     0 my $messagingStore = $storeManager->storeForUrl($messagingStoreUrl) // return $ui->error('Your messaging store "', $messagingStoreUrl, '" cannot be accessed. You can set this store in "', $configuration->file('messaging-store'), '".');
1267              
1268             # Read the key pair
1269 0   0     0 my $keyPair = $configuration->keyPair // return $ui->error('Your key pair (', $configuration->file('key-pair'), ') is missing.');
1270              
1271             # Create the actor
1272 0         0 my $publicKeyCache = CDS::PublicKeyCache->new(128);
1273 0         0 my $o = $class->SUPER::new($keyPair, $storageStore, $messagingStore, $messagingStoreUrl, $publicKeyCache);
1274 0         0 $o->{ui} = $ui;
1275 0         0 $o->{storeManager} = $storeManager;
1276 0         0 $o->{configuration} = $configuration;
1277 0         0 $o->{sessionRoot} = $o->localRoot->child('sessions')->child(''.getppid);
1278 0         0 $o->{keyPairToken} = CDS::KeyPairToken->new($configuration->file('key-pair'), $keyPair);
1279              
1280             # Message handlers
1281 0         0 $o->{messageHandlers} = {};
1282 0         0 $o->setMessageHandler('sender', \&onIgnoreMessage);
1283 0         0 $o->setMessageHandler('store', \&onIgnoreMessage);
1284 0         0 $o->setMessageHandler('group data', \&onGroupDataMessage);
1285              
1286             # Read the private data
1287 0 0       0 if (! $o->procurePrivateData) {
1288 0         0 $o->{ui}->space;
1289 0         0 $ui->pRed('Failed to read the local private data.');
1290 0         0 $o->{ui}->space;
1291 0         0 return;
1292             }
1293              
1294 0         0 return $o;
1295             }
1296              
1297 0     0   0 sub ui { shift->{ui} }
1298 0     0   0 sub storeManager { shift->{storeManager} }
1299 0     0   0 sub configuration { shift->{configuration} }
1300 0     0   0 sub sessionRoot { shift->{sessionRoot} }
1301 0     0   0 sub keyPairToken { shift->{keyPairToken} }
1302              
1303             ### Saving
1304              
1305             sub saveOrShowError {
1306 0     0   0 my $o = shift;
1307              
1308 0         0 $o->forgetOldSessions;
1309 0         0 my ($ok, $missingHash) = $o->savePrivateDataAndShareGroupData;
1310 0 0       0 return if ! $ok;
1311 0 0       0 return $o->onMissingObject($missingHash) if $missingHash;
1312 0         0 $o->sendMessages;
1313 0         0 return 1;
1314             }
1315              
1316             sub onMissingObject {
1317 0     0   0 my $o = shift;
1318 0 0 0     0 my $missingObject = shift; die 'wrong type '.ref($missingObject).' for $missingObject' if defined $missingObject && ref $missingObject ne 'CDS::Object';
  0         0  
1319              
1320 0         0 $o->{ui}->space;
1321 0         0 $o->{ui}->pRed('The object ', $missingObject->hash->hex, ' was missing while saving data.');
1322 0         0 $o->{ui}->space;
1323 0         0 $o->{ui}->p('This is a fatal error with two possible sources:');
1324 0         0 $o->{ui}->p('- A store may have lost objects, e.g. due to an error with the underlying storage, misconfiguration, or too aggressive garbage collection.');
1325 0         0 $o->{ui}->p('- The application is linking objects without properly storing them. This is an error in the application, that must be fixed by a developer.');
1326 0         0 $o->{ui}->space;
1327             }
1328              
1329             sub onGroupDataSharingStoreError {
1330 0     0   0 my $o = shift;
1331 0 0 0     0 my $recipientActorOnStore = shift; die 'wrong type '.ref($recipientActorOnStore).' for $recipientActorOnStore' if defined $recipientActorOnStore && ref $recipientActorOnStore ne 'CDS::ActorOnStore';
  0         0  
1332 0         0 my $storeError = shift;
1333              
1334 0         0 $o->{ui}->space;
1335 0         0 $o->{ui}->pRed('Unable to share the group data with ', $recipientActorOnStore->publicKey->hash->hex, '.');
1336 0         0 $o->{ui}->space;
1337             }
1338              
1339             ### Reading
1340              
1341             sub onPrivateRootReadingInvalidEntry {
1342 0     0   0 my $o = shift;
1343 0 0 0     0 my $source = shift; die 'wrong type '.ref($source).' for $source' if defined $source && ref $source ne 'CDS::Source';
  0         0  
1344 0         0 my $reason = shift;
1345              
1346 0         0 $o->{ui}->space;
1347 0         0 $o->{ui}->pRed('The envelope ', $source->hash->shortHex, ' points to invalid private data (', $reason, ').');
1348 0         0 $o->{ui}->p('This could be due to a storage system failure, a malicious attempt to delete or modify your data, or simply an application error. To investigate what is going on, the following commands may be helpful:');
1349 0         0 $o->{ui}->line(' cds open envelope ', $source->hash->hex, ' from ', $source->actorOnStore->publicKey->hash->hex, ' on ', $source->actorOnStore->store->url);
1350 0         0 $o->{ui}->line(' cds show record ', $source->hash->hex, ' on ', $source->actorOnStore->store->url);
1351 0         0 $o->{ui}->line(' cds list private box of ', $source->actorOnStore->publicKey->hash->hex, ' on ', $source->actorOnStore->store->url);
1352 0         0 $o->{ui}->p('To remove the invalid entry, type:');
1353 0         0 $o->{ui}->line(' cds remove ', $source->hash->hex, ' from private box of ', $source->actorOnStore->publicKey->hash->hex, ' on ', $source->actorOnStore->store->url);
1354 0         0 $o->{ui}->space;
1355             }
1356              
1357             sub onVerifyMemberStore {
1358 0     0   0 my $o = shift;
1359 0         0 my $storeUrl = shift;
1360 0 0 0     0 my $actorSelector = shift; die 'wrong type '.ref($actorSelector).' for $actorSelector' if defined $actorSelector && ref $actorSelector ne 'CDS::Selector';
  0         0  
1361 0         0 $o->storeForUrl($storeUrl) }
1362              
1363             ### Announcing
1364              
1365             sub registerIfNecessary {
1366 0     0   0 my $o = shift;
1367              
1368 0         0 my $now = CDS->now;
1369 0 0       0 return if $o->{actorSelector}->revision > $now - CDS->DAY;
1370 0         0 $o->updateMyRegistration;
1371 0         0 $o->setMyActiveFlag(1);
1372 0         0 $o->setMyGroupDataFlag(1);
1373             }
1374              
1375             sub announceIfNecessary {
1376 0     0   0 my $o = shift;
1377              
1378 0         0 my $state = join('', map { CDS->bytesFromUnsigned($_->revision) } sort { $a->label cmp $b->label } $o->{actorGroupSelector}->children);
  0         0  
  0         0  
1379 0         0 $o->announceOnStoreIfNecessary($o->{storageStore}, $state);
1380 0 0       0 $o->announceOnStoreIfNecessary($o->{messagingStore}, $state) if $o->{messagingStore}->id ne $o->{storageStore}->id;
1381             }
1382              
1383             sub announceOnStoreIfNecessary {
1384 0     0   0 my $o = shift;
1385 0         0 my $store = shift;
1386 0         0 my $state = shift;
1387              
1388 0         0 my $stateSelector = $o->{localRoot}->child('announced')->childWithText($store->id);
1389 0 0       0 return if $stateSelector->bytesValue eq $state;
1390 0         0 my ($envelopeHash, $cardHash) = $o->announce($store);
1391 0 0       0 return $o->{ui}->pRed('Updating the card on ', $store->url, ' failed.') if ! $envelopeHash;
1392 0         0 $stateSelector->setBytes($state);
1393 0         0 $o->{ui}->pGreen('The card on ', $store->url, ' has been updated.');
1394 0         0 return 1;
1395             }
1396              
1397             ### Store resolving
1398              
1399             sub storeForUrl {
1400 0     0   0 my $o = shift;
1401 0         0 my $url = shift;
1402              
1403 0         0 $o->{storeManager}->setCacheStoreUrl($o->{sessionRoot}->child('use cache')->textValue);
1404 0         0 return $o->{storeManager}->storeForUrl($url);
1405             }
1406              
1407             ### Processing messages
1408              
1409             sub setMessageHandler {
1410 0     0   0 my $o = shift;
1411 0         0 my $type = shift;
1412 0         0 my $handler = shift;
1413              
1414 0         0 $o->{messageHandlers}->{$type} = $handler;
1415             }
1416              
1417             sub readMessages {
1418 0     0   0 my $o = shift;
1419              
1420 0         0 $o->{ui}->title('Messages');
1421 0         0 $o->{countMessages} = 0;
1422 0         0 $o->{messageBoxReader}->read;
1423 0 0       0 $o->{ui}->line($o->{ui}->gray('none')) if ! $o->{countMessages};
1424             }
1425              
1426             sub onMessageBoxVerifyStore {
1427 0     0   0 my $o = shift;
1428 0         0 my $senderStoreUrl = shift;
1429 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
1430 0 0 0     0 my $envelope = shift; die 'wrong type '.ref($envelope).' for $envelope' if defined $envelope && ref $envelope ne 'CDS::Record';
  0         0  
1431 0 0 0     0 my $senderHash = shift; die 'wrong type '.ref($senderHash).' for $senderHash' if defined $senderHash && ref $senderHash ne 'CDS::Hash';
  0         0  
1432              
1433 0         0 return $o->storeForUrl($senderStoreUrl);
1434             }
1435              
1436             sub onMessageBoxEntry {
1437 0     0   0 my $o = shift;
1438 0         0 my $message = shift;
1439              
1440 0         0 $o->{countMessages} += 1;
1441              
1442 0         0 for my $section ($message->content->children) {
1443 0         0 my $type = $section->bytes;
1444 0   0     0 my $handler = $o->{messageHandlers}->{$type} // \&onUnknownMessage;
1445 0         0 &$handler($o, $message, $section);
1446             }
1447              
1448             # 1. message processed
1449             # -> source can be deleted immediately (e.g. invalid)
1450             # source.discard()
1451             # -> source has been merged, and will be deleted when changes have been saved
1452             # document.addMergedSource(source)
1453             # 2. wait for sender store
1454             # -> set entry.waitForStore = senderStore
1455             # 3. skip
1456             # -> set entry.processed = false
1457              
1458 0         0 my $source = $message->source;
1459 0         0 $message->source->discard;
1460             }
1461              
1462             sub onGroupDataMessage {
1463 0     0   0 my $o = shift;
1464 0         0 my $message = shift;
1465 0         0 my $section = shift;
1466              
1467 0         0 my $ok = $o->{groupDataSharer}->processGroupDataMessage($message, $section);
1468 0         0 $o->{groupDocument}->read;
1469 0 0       0 return $o->{ui}->line('Group data from ', $message->sender->publicKey->hash->hex) if $ok;
1470 0         0 $o->{ui}->line($o->{ui}->red('Group data from foreign actor ', $message->sender->publicKey->hash->hex, ' (ignored)'));
1471             }
1472              
1473             sub onIgnoreMessage {
1474 0     0   0 my $o = shift;
1475 0         0 my $message = shift;
1476 0         0 my $section = shift;
1477             }
1478              
1479             sub onUnknownMessage {
1480 0     0   0 my $o = shift;
1481 0         0 my $message = shift;
1482 0         0 my $section = shift;
1483              
1484 0         0 $o->{ui}->line($o->{ui}->orange('Unknown message of type "', $section->asText, '" from ', $message->sender->publicKey->hash->hex));
1485             }
1486              
1487             sub onMessageBoxInvalidEntry {
1488 0     0   0 my $o = shift;
1489 0 0 0     0 my $source = shift; die 'wrong type '.ref($source).' for $source' if defined $source && ref $source ne 'CDS::Source';
  0         0  
1490 0         0 my $reason = shift;
1491              
1492 0         0 $o->{ui}->warning('Discarding invalid message ', $source->hash->hex, ' (', $reason, ').');
1493 0         0 $source->discard;
1494             }
1495              
1496             ### Remembered values
1497              
1498             sub labelSelector {
1499 0     0   0 my $o = shift;
1500 0         0 my $label = shift;
1501              
1502 0         0 my $bytes = Encode::encode_utf8($label);
1503 0         0 return $o->groupRoot->child('labels')->child($bytes);
1504             }
1505              
1506             sub remembered {
1507 0     0   0 my $o = shift;
1508 0         0 my $label = shift;
1509              
1510 0         0 return $o->labelSelector($label)->record;
1511             }
1512              
1513             sub remember {
1514 0     0   0 my $o = shift;
1515 0         0 my $label = shift;
1516 0 0 0     0 my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0         0  
1517              
1518 0         0 $o->labelSelector($label)->set($record);
1519             }
1520              
1521             sub rememberedRecords {
1522 0     0   0 my $o = shift;
1523              
1524 0         0 my $records = {};
1525 0         0 for my $child ($o->{groupRoot}->child('labels')->children) {
1526 0 0       0 next if ! $child->isSet;
1527 0         0 my $label = Encode::decode_utf8($child->label);
1528 0         0 $records->{$label} = $child->record;
1529             }
1530              
1531 0         0 return $records;
1532             }
1533              
1534             sub storeLabel {
1535 0     0   0 my $o = shift;
1536 0         0 my $storeUrl = shift;
1537              
1538 0         0 my $records = $o->rememberedRecords;
1539 0         0 for my $label (keys %$records) {
1540 0         0 my $record = $records->{$label};
1541 0 0       0 next if length $record->child('actor')->bytesValue;
1542 0 0       0 next if $storeUrl ne $record->child('store')->textValue;
1543 0         0 return $label;
1544             }
1545              
1546 0         0 return;
1547             }
1548              
1549             sub actorLabel {
1550 0     0   0 my $o = shift;
1551 0 0 0     0 my $actorHash = shift; die 'wrong type '.ref($actorHash).' for $actorHash' if defined $actorHash && ref $actorHash ne 'CDS::Hash';
  0         0  
1552              
1553 0         0 my $records = $o->rememberedRecords;
1554 0         0 for my $label (keys %$records) {
1555 0         0 my $record = $records->{$label};
1556 0 0       0 next if $actorHash->bytes ne $record->child('actor')->bytesValue;
1557 0         0 return $label;
1558             }
1559              
1560 0         0 return;
1561             }
1562              
1563             sub actorLabelByHashStartBytes {
1564 0     0   0 my $o = shift;
1565 0         0 my $actorHashStartBytes = shift;
1566              
1567 0         0 my $length = length $actorHashStartBytes;
1568 0         0 my $records = $o->rememberedRecords;
1569 0         0 for my $label (keys %$records) {
1570 0         0 my $record = $records->{$label};
1571 0 0       0 next if $actorHashStartBytes ne substr($record->child('actor')->bytesValue, 0, $length);
1572 0         0 return $label;
1573             }
1574              
1575 0         0 return;
1576             }
1577              
1578             sub accountLabel {
1579 0     0   0 my $o = shift;
1580 0         0 my $storeUrl = shift;
1581 0 0 0     0 my $actorHash = shift; die 'wrong type '.ref($actorHash).' for $actorHash' if defined $actorHash && ref $actorHash ne 'CDS::Hash';
  0         0  
1582              
1583 0         0 my $storeLabel;
1584             my $actorLabel;
1585              
1586 0         0 my $records = $o->rememberedRecords;
1587 0         0 for my $label (keys %$records) {
1588 0         0 my $record = $records->{$label};
1589 0         0 my $actorBytes = $record->child('actor')->bytesValue;
1590              
1591 0         0 my $correctActor = $actorHash->bytes eq $actorBytes;
1592 0 0       0 $actorLabel = $label if $correctActor;
1593              
1594 0 0       0 if ($storeUrl eq $record->child('store')->textValue) {
1595 0 0       0 return $label if $correctActor;
1596 0 0       0 $storeLabel = $label if ! length $actorBytes;
1597             }
1598             }
1599              
1600 0         0 return (undef, $storeLabel, $actorLabel);
1601             }
1602              
1603             sub keyPairLabel {
1604 0     0   0 my $o = shift;
1605 0         0 my $file = shift;
1606              
1607 0         0 my $records = $o->rememberedRecords;
1608 0         0 for my $label (keys %$records) {
1609 0         0 my $record = $records->{$label};
1610 0 0       0 next if $file ne $record->child('key pair')->textValue;
1611 0         0 return $label;
1612             }
1613              
1614 0         0 return;
1615             }
1616              
1617             ### References that can be used in commands
1618              
1619             sub actorReference {
1620 0     0   0 my $o = shift;
1621 0 0 0     0 my $actorHash = shift; die 'wrong type '.ref($actorHash).' for $actorHash' if defined $actorHash && ref $actorHash ne 'CDS::Hash';
  0         0  
1622              
1623 0   0     0 return $o->actorLabel($actorHash) // $actorHash->hex;
1624             }
1625              
1626             sub storeReference {
1627 0     0   0 my $o = shift;
1628 0         0 my $store = shift;
1629 0         0 $o->storeUrlReference($store->url); }
1630              
1631             sub storeUrlReference {
1632 0     0   0 my $o = shift;
1633 0         0 my $storeUrl = shift;
1634              
1635 0   0     0 return $o->storeLabel($storeUrl) // $storeUrl;
1636             }
1637              
1638             sub accountReference {
1639 0     0   0 my $o = shift;
1640 0         0 my $accountToken = shift;
1641              
1642 0         0 my ($accountLabel, $storeLabel, $actorLabel) = $o->accountLabel($accountToken->{cliStore}->url, $accountToken->{actorHash});
1643 0 0       0 return $accountLabel if defined $accountLabel;
1644 0 0       0 return defined $actorLabel ? $actorLabel : $accountToken->{actorHash}->hex, ' on ', defined $storeLabel ? $storeLabel : $accountToken->{cliStore}->url;
    0          
1645             }
1646              
1647             sub boxReference {
1648 0     0   0 my $o = shift;
1649 0         0 my $boxToken = shift;
1650              
1651 0         0 return $o->boxName($boxToken->{boxLabel}), ' of ', $o->accountReference($boxToken->{accountToken});
1652             }
1653              
1654             sub keyPairReference {
1655 0     0   0 my $o = shift;
1656 0         0 my $keyPairToken = shift;
1657              
1658 0   0     0 return $o->keyPairLabel($keyPairToken->file) // $keyPairToken->file;
1659             }
1660              
1661             sub blueActorReference {
1662 0     0   0 my $o = shift;
1663 0 0 0     0 my $actorHash = shift; die 'wrong type '.ref($actorHash).' for $actorHash' if defined $actorHash && ref $actorHash ne 'CDS::Hash';
  0         0  
1664              
1665 0         0 my $label = $o->actorLabel($actorHash);
1666 0 0       0 return defined $label ? $o->{ui}->blue($label) : $actorHash->hex;
1667             }
1668              
1669             sub blueStoreReference {
1670 0     0   0 my $o = shift;
1671 0         0 my $store = shift;
1672 0         0 $o->blueStoreUrlReference($store->url); }
1673              
1674             sub blueStoreUrlReference {
1675 0     0   0 my $o = shift;
1676 0         0 my $storeUrl = shift;
1677              
1678 0         0 my $label = $o->storeLabel($storeUrl);
1679 0 0       0 return defined $label ? $o->{ui}->blue($label) : $storeUrl;
1680             }
1681              
1682             sub blueAccountReference {
1683 0     0   0 my $o = shift;
1684 0         0 my $accountToken = shift;
1685              
1686 0         0 my ($accountLabel, $storeLabel, $actorLabel) = $o->accountLabel($accountToken->{cliStore}->url, $accountToken->{actorHash});
1687 0 0       0 return $o->{ui}->blue($accountLabel) if defined $accountLabel;
1688 0 0       0 return defined $actorLabel ? $o->{ui}->blue($actorLabel) : $accountToken->{actorHash}->hex, ' on ', defined $storeLabel ? $o->{ui}->blue($storeLabel) : $accountToken->{cliStore}->url;
    0          
1689             }
1690              
1691             sub blueBoxReference {
1692 0     0   0 my $o = shift;
1693 0         0 my $boxToken = shift;
1694              
1695 0         0 return $o->boxName($boxToken->{boxLabel}), ' of ', $o->blueAccountReference($boxToken->{accountToken});
1696             }
1697              
1698             sub blueKeyPairReference {
1699 0     0   0 my $o = shift;
1700 0         0 my $keyPairToken = shift;
1701              
1702 0         0 my $label = $o->keyPairLabel($keyPairToken->file);
1703 0 0       0 return defined $label ? $o->{ui}->blue($label) : $keyPairToken->file;
1704             }
1705              
1706             sub boxName {
1707 0     0   0 my $o = shift;
1708 0         0 my $boxLabel = shift;
1709              
1710 0 0       0 return 'private box' if $boxLabel eq 'private';
1711 0 0       0 return 'public box' if $boxLabel eq 'public';
1712 0 0       0 return 'message box' if $boxLabel eq 'messages';
1713 0         0 return $boxLabel;
1714             }
1715              
1716             ### Session
1717              
1718             sub forgetOldSessions {
1719 0     0   0 my $o = shift;
1720              
1721 0         0 for my $child ($o->{sessionRoot}->parent->children) {
1722 0         0 my $pid = $child->label;
1723 0 0       0 next if -e '/proc/'.$pid;
1724 0         0 $child->forgetBranch;
1725             }
1726             }
1727              
1728             sub selectedKeyPairToken {
1729 0     0   0 my $o = shift;
1730              
1731 0         0 my $file = $o->{sessionRoot}->child('selected key pair')->textValue;
1732 0 0       0 return if ! length $file;
1733 0   0     0 my $keyPair = CDS::KeyPair->fromFile($file) // return;
1734 0         0 return CDS::KeyPairToken->new($file, $keyPair);
1735             }
1736              
1737             sub selectedStoreUrl {
1738 0     0   0 my $o = shift;
1739              
1740 0         0 my $storeUrl = $o->{sessionRoot}->child('selected store')->textValue;
1741 0 0       0 return if ! length $storeUrl;
1742 0         0 return $storeUrl;
1743             }
1744              
1745             sub selectedStore {
1746 0     0   0 my $o = shift;
1747              
1748 0   0     0 my $storeUrl = $o->selectedStoreUrl // return;
1749 0         0 return $o->storeForUrl($storeUrl);
1750             }
1751              
1752             sub selectedActorHash {
1753 0     0   0 my $o = shift;
1754              
1755 0         0 return CDS::Hash->fromBytes($o->{sessionRoot}->child('selected actor')->bytesValue);
1756             }
1757              
1758             sub preferredKeyPairToken {
1759 0     0   0 my $o = shift;
1760 0   0     0 $o->selectedKeyPairToken // $o->keyPairToken }
1761             sub preferredStore {
1762 0     0   0 my $o = shift;
1763 0   0     0 $o->selectedStore // $o->storageStore }
1764             sub preferredStores {
1765 0     0   0 my $o = shift;
1766 0   0     0 $o->selectedStore // ($o->storageStore, $o->messagingStore) }
1767             sub preferredActorHash {
1768 0     0   0 my $o = shift;
1769 0   0     0 $o->selectedActorHash // $o->keyPair->publicKey->hash }
1770              
1771             ### Common functions
1772              
1773             sub uiGetObject {
1774 0     0   0 my $o = shift;
1775 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
1776 0         0 my $store = shift;
1777 0         0 my $keyPairToken = shift;
1778              
1779 0         0 my ($object, $storeError) = $store->get($hash, $keyPairToken->keyPair);
1780 0 0       0 return if defined $storeError;
1781 0 0       0 return $o->{ui}->error('The object ', $hash->hex, ' does not exist on "', $store->url, '".') if ! $object;
1782 0         0 return $object;
1783             }
1784              
1785             sub uiGetRecord {
1786 0     0   0 my $o = shift;
1787 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
1788 0         0 my $store = shift;
1789 0         0 my $keyPairToken = shift;
1790              
1791 0   0     0 my $object = $o->uiGetObject($hash, $store, $keyPairToken) // return;
1792 0   0     0 return CDS::Record->fromObject($object) // return $o->{ui}->error('The object ', $hash->hex, ' is not a record.');
1793             }
1794              
1795             sub uiGetPublicKey {
1796 0     0   0 my $o = shift;
1797 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
1798 0         0 my $store = shift;
1799 0         0 my $keyPairToken = shift;
1800              
1801 0   0     0 my $object = $o->uiGetObject($hash, $store, $keyPairToken) // return;
1802 0   0     0 return CDS::PublicKey->fromObject($object) // return $o->{ui}->error('The object ', $hash->hex, ' is not a public key.');
1803             }
1804              
1805             sub isEnvelope {
1806 0     0   0 my $o = shift;
1807 0 0 0     0 my $object = shift; die 'wrong type '.ref($object).' for $object' if defined $object && ref $object ne 'CDS::Object';
  0         0  
1808              
1809 0   0     0 my $record = CDS::Record->fromObject($object) // return;
1810 0 0       0 return if ! $record->contains('signed');
1811 0         0 my $signatureRecord = $record->child('signature')->firstChild;
1812 0 0       0 return if ! $signatureRecord->hash;
1813 0 0       0 return if ! length $signatureRecord->bytes;
1814 0         0 return 1;
1815             }
1816              
1817             package CDS::CLIStoreManager;
1818              
1819             sub new {
1820 0     0   0 my $class = shift;
1821 0         0 my $ui = shift;
1822              
1823 0         0 return bless {ui => $ui, failedStores => {}};
1824             }
1825              
1826 0     0   0 sub ui { shift->{ui} }
1827              
1828             sub rawStoreForUrl {
1829 0     0   0 my $o = shift;
1830 0         0 my $url = shift;
1831              
1832 0 0       0 return if ! $url;
1833             return
1834 0   0     0 CDS::FolderStore->forUrl($url) //
      0        
1835             CDS::HTTPStore->forUrl($url) //
1836             undef;
1837             }
1838              
1839             sub storeForUrl {
1840 0     0   0 my $o = shift;
1841 0         0 my $url = shift;
1842              
1843 0         0 my $store = $o->rawStoreForUrl($url);
1844 0         0 my $progressStore = CDS::UI::ProgressStore->new($store, $url, $o->{ui});
1845 0 0       0 my $cachedStore = defined $o->{cacheStore} ? CDS::ObjectCache->new($progressStore, $o->{cacheStore}) : $progressStore;
1846 0         0 return CDS::ErrorHandlingStore->new($cachedStore, $url, $o);
1847             }
1848              
1849             sub onStoreSuccess {
1850 0     0   0 my $o = shift;
1851 0         0 my $store = shift;
1852 0         0 my $function = shift;
1853              
1854 0         0 delete $o->{failedStores}->{$store->store->id};
1855             }
1856              
1857             sub onStoreError {
1858 0     0   0 my $o = shift;
1859 0         0 my $store = shift;
1860 0         0 my $function = shift;
1861 0         0 my $error = shift;
1862              
1863 0         0 $o->{failedStores}->{$store->store->id} = 1;
1864 0         0 $o->{ui}->error('The store "', $store->{url}, '" reports: ', $error);
1865             }
1866              
1867             sub hasStoreError {
1868 0     0   0 my $o = shift;
1869 0         0 my $store = shift;
1870 0         0 my $function = shift;
1871              
1872 0 0       0 return if ! $o->{failedStores}->{$store->store->id};
1873 0         0 $o->{ui}->error('Ignoring store "', $store->{url}, '", because it previously reported errors.');
1874 0         0 return 1;
1875             }
1876              
1877             sub setCacheStoreUrl {
1878 0     0   0 my $o = shift;
1879 0         0 my $storeUrl = shift;
1880              
1881 0 0 0     0 return if ($storeUrl // '') eq ($o->{cacheStoreUrl} // '');
      0        
1882 0         0 $o->{cacheStoreUrl} = $storeUrl;
1883 0         0 $o->{cacheStore} = $o->rawStoreForUrl($storeUrl);
1884             }
1885              
1886             package CDS::CheckSignatureStore;
1887              
1888             sub new {
1889 0     0   0 my $o = shift;
1890 0         0 my $store = shift;
1891 0         0 my $objects = shift;
1892              
1893 0   0     0 return bless {
1894             store => $store,
1895             id => "Check signature store\n".$store->id,
1896             objects => $objects // {},
1897             };
1898             }
1899              
1900 0     0   0 sub id { shift->{id} }
1901              
1902             sub get {
1903 0     0   0 my $o = shift;
1904 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
1905 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
1906              
1907 0   0     0 my $entry = $o->{objects}->{$hash->bytes} // return $o->{store}->get($hash);
1908 0         0 return $entry->{object};
1909             }
1910              
1911             sub book {
1912 0     0   0 my $o = shift;
1913 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
1914 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
1915              
1916 0         0 return exists $o->{objects}->{$hash->bytes};
1917             }
1918              
1919             sub put {
1920 0     0   0 my $o = shift;
1921 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
1922 0 0 0     0 my $object = shift; die 'wrong type '.ref($object).' for $object' if defined $object && ref $object ne 'CDS::Object';
  0         0  
1923 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
1924              
1925 0         0 $o->{objects}->{$hash->bytes} = {hash => $hash, object => $object};
1926 0         0 return;
1927             }
1928              
1929             sub list {
1930 0     0   0 my $o = shift;
1931 0 0 0     0 my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0         0  
1932 0         0 my $boxLabel = shift;
1933 0         0 my $timeout = shift;
1934 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
1935              
1936 0         0 return 'This store only handles objects.';
1937             }
1938              
1939             sub add {
1940 0     0   0 my $o = shift;
1941 0 0 0     0 my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0         0  
1942 0         0 my $boxLabel = shift;
1943 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
1944 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
1945              
1946 0         0 return 'This store only handles objects.';
1947             }
1948              
1949             sub remove {
1950 0     0   0 my $o = shift;
1951 0 0 0     0 my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0         0  
1952 0         0 my $boxLabel = shift;
1953 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
1954 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
1955              
1956 0         0 return 'This store only handles objects.';
1957             }
1958              
1959             sub modify {
1960 0     0   0 my $o = shift;
1961 0         0 my $modifications = shift;
1962 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
1963              
1964 0         0 return $modifications->executeIndividually($o, $keyPair);
1965             }
1966              
1967             # BEGIN AUTOGENERATED
1968             package CDS::Commands::ActorGroup;
1969              
1970             sub register {
1971 0     0   0 my $class = shift;
1972 0         0 my $cds = shift;
1973 0         0 my $help = shift;
1974              
1975 0         0 my $node000 = CDS::Parser::Node->new(0);
1976 0         0 my $node001 = CDS::Parser::Node->new(0);
1977 0         0 my $node002 = CDS::Parser::Node->new(0);
1978 0         0 my $node003 = CDS::Parser::Node->new(0);
1979 0         0 my $node004 = CDS::Parser::Node->new(0);
1980 0         0 my $node005 = CDS::Parser::Node->new(0);
1981 0         0 my $node006 = CDS::Parser::Node->new(0);
1982 0         0 my $node007 = CDS::Parser::Node->new(0);
1983 0         0 my $node008 = CDS::Parser::Node->new(0);
1984 0         0 my $node009 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
1985 0         0 my $node010 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&show});
1986 0         0 my $node011 = CDS::Parser::Node->new(0);
1987 0         0 my $node012 = CDS::Parser::Node->new(0);
1988 0         0 my $node013 = CDS::Parser::Node->new(0);
1989 0         0 my $node014 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&joinMember});
1990 0         0 my $node015 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&setMember});
1991 0         0 my $node016 = CDS::Parser::Node->new(0);
1992 0         0 $cds->addArrow($node001, 1, 0, 'show');
1993 0         0 $cds->addArrow($node003, 1, 0, 'join');
1994 0         0 $cds->addArrow($node004, 1, 0, 'set');
1995 0         0 $help->addArrow($node000, 1, 0, 'actor');
1996 0         0 $node000->addArrow($node009, 1, 0, 'group');
1997 0         0 $node001->addArrow($node002, 1, 0, 'actor');
1998 0         0 $node002->addArrow($node010, 1, 0, 'group');
1999 0         0 $node003->addArrow($node005, 1, 0, 'member');
2000 0         0 $node004->addArrow($node007, 1, 0, 'member');
2001 0         0 $node005->addDefault($node006);
2002 0         0 $node005->addArrow($node011, 1, 0, 'ACTOR', \&collectActor);
2003 0         0 $node006->addArrow($node006, 1, 0, 'ACCOUNT', \&collectAccount);
2004 0         0 $node006->addArrow($node014, 1, 1, 'ACCOUNT', \&collectAccount);
2005 0         0 $node007->addDefault($node008);
2006 0         0 $node008->addArrow($node008, 1, 0, 'ACTOR', \&collectActor1);
2007 0         0 $node008->addArrow($node013, 1, 0, 'ACTOR', \&collectActor1);
2008 0         0 $node011->addArrow($node012, 1, 0, 'on');
2009 0         0 $node012->addArrow($node014, 1, 0, 'STORE', \&collectStore);
2010 0         0 $node013->addArrow($node015, 1, 0, 'active', \&collectActive);
2011 0         0 $node013->addArrow($node015, 1, 0, 'backup', \&collectBackup);
2012 0         0 $node013->addArrow($node015, 1, 0, 'idle', \&collectIdle);
2013 0         0 $node013->addArrow($node015, 1, 0, 'revoked', \&collectRevoked);
2014 0         0 $node014->addArrow($node016, 1, 0, 'and');
2015 0         0 $node016->addDefault($node005);
2016             }
2017              
2018             sub collectAccount {
2019 0     0   0 my $o = shift;
2020 0         0 my $label = shift;
2021 0         0 my $value = shift;
2022              
2023 0         0 push @{$o->{accountTokens}}, $value;
  0         0  
2024             }
2025              
2026             sub collectActive {
2027 0     0   0 my $o = shift;
2028 0         0 my $label = shift;
2029 0         0 my $value = shift;
2030              
2031 0         0 $o->{status} = 'active';
2032             }
2033              
2034             sub collectActor {
2035 0     0   0 my $o = shift;
2036 0         0 my $label = shift;
2037 0         0 my $value = shift;
2038              
2039 0         0 $o->{actorHash} = $value;
2040             }
2041              
2042             sub collectActor1 {
2043 0     0   0 my $o = shift;
2044 0         0 my $label = shift;
2045 0         0 my $value = shift;
2046              
2047 0         0 push @{$o->{actorHashes}}, $value;
  0         0  
2048             }
2049              
2050             sub collectBackup {
2051 0     0   0 my $o = shift;
2052 0         0 my $label = shift;
2053 0         0 my $value = shift;
2054              
2055 0         0 $o->{status} = 'backup';
2056             }
2057              
2058             sub collectIdle {
2059 0     0   0 my $o = shift;
2060 0         0 my $label = shift;
2061 0         0 my $value = shift;
2062              
2063 0         0 $o->{status} = 'idle';
2064             }
2065              
2066             sub collectRevoked {
2067 0     0   0 my $o = shift;
2068 0         0 my $label = shift;
2069 0         0 my $value = shift;
2070              
2071 0         0 $o->{status} = 'revoked';
2072             }
2073              
2074             sub collectStore {
2075 0     0   0 my $o = shift;
2076 0         0 my $label = shift;
2077 0         0 my $value = shift;
2078              
2079 0         0 push @{$o->{accountTokens}}, CDS::AccountToken->new($value, $o->{actorHash});
  0         0  
2080 0         0 delete $o->{actorHash};
2081             }
2082              
2083             sub new {
2084 0     0   0 my $class = shift;
2085 0         0 my $actor = shift;
2086 0         0 bless {actor => $actor, ui => $actor->ui} }
2087              
2088             # END AUTOGENERATED
2089              
2090             # HTML FOLDER NAME actor-group
2091             # HTML TITLE Actor group
2092             sub help {
2093 0     0   0 my $o = shift;
2094 0         0 my $cmd = shift;
2095              
2096 0         0 my $ui = $o->{ui};
2097 0         0 $ui->space;
2098 0         0 $ui->command('cds show actor group');
2099 0         0 $ui->p('Shows all members of our actor group and the entrusted keys.');
2100 0         0 $ui->space;
2101 0         0 $ui->command('cds join ACCOUNT*');
2102 0         0 $ui->command('cds join ACTOR on STORE');
2103 0         0 $ui->p('Adds a member to our actor group. To complete the association, the new member must join us, too.');
2104 0         0 $ui->space;
2105 0         0 $ui->command('cds set member ACTOR* active');
2106 0         0 $ui->command('cds set member ACTOR* backup');
2107 0         0 $ui->command('cds set member ACTOR* idle');
2108 0         0 $ui->command('cds set member ACTOR* revoked');
2109 0         0 $ui->p('Changes the status of a member to one of the following:');
2110 0         0 $ui->p($ui->bold('Active members'), ' share the group data among themselves, and are advertised to receive messages.');
2111 0         0 $ui->p($ui->bold('Backup members'), ' share the group data (like active members), but are publicly advertised as not processing messages (like idle members). This is suitable for backup actors.');
2112 0         0 $ui->p($ui->bold('Idle members'), ' are part of the group, but advertised as not processing messages. They generally do not have the latest group data, and may have no group data at all. Idle members may reactivate themselves, or get reactivated by any active member of the group.');
2113 0         0 $ui->p($ui->bold('Revoked members'), ' have explicitly been removed from the group, e.g. because their private key (or device) got lost. Revoked members can be reactivated by any active member of the group.');
2114 0         0 $ui->p('Note that changing the status does not start or stop the corresponding actor, but just change how it is regarded by others. The status of each member should reflect its actual behavior.');
2115 0         0 $ui->space;
2116 0         0 $ui->p('After modifying the actor group members, you should "cds announce" yourself to publish the changes.');
2117 0         0 $ui->space;
2118             }
2119              
2120             sub show {
2121 0     0   0 my $o = shift;
2122 0         0 my $cmd = shift;
2123              
2124 0         0 my $hasMembers = 0;
2125 0         0 for my $actorSelector ($o->{actor}->actorGroupSelector->children) {
2126 0         0 my $record = $actorSelector->record;
2127 0   0     0 my $hash = $record->child('hash')->hashValue // next;
2128 0 0       0 next if substr($hash->bytes, 0, length $actorSelector->label) ne $actorSelector->label;
2129 0         0 my $storeUrl = $record->child('store')->textValue;
2130 0         0 my $revisionText = $o->{ui}->niceDateTimeLocal($actorSelector->revision);
2131 0         0 $o->{ui}->line($o->{ui}->gray($revisionText), ' ', $o->coloredType7($actorSelector), ' ', $hash->hex, ' on ', $storeUrl);
2132 0         0 $hasMembers = 1;
2133             }
2134              
2135 0 0       0 return if $hasMembers;
2136 0         0 $o->{ui}->line($o->{ui}->blue('(just you)'));
2137             }
2138              
2139             sub type {
2140 0     0   0 my $o = shift;
2141 0 0 0     0 my $actorSelector = shift; die 'wrong type '.ref($actorSelector).' for $actorSelector' if defined $actorSelector && ref $actorSelector ne 'CDS::Selector';
  0         0  
2142              
2143 0         0 my $groupData = $actorSelector->child('group data')->isSet;
2144 0         0 my $active = $actorSelector->child('active')->isSet;
2145 0         0 my $revoked = $actorSelector->child('revoked')->isSet;
2146             return
2147 0 0 0     0 $revoked ? 'revoked' :
    0          
    0          
    0          
2148             $active && $groupData ? 'active' :
2149             $groupData ? 'backup' :
2150             $active ? 'weird' :
2151             'idle';
2152             }
2153              
2154             sub coloredType7 {
2155 0     0   0 my $o = shift;
2156 0 0 0     0 my $actorSelector = shift; die 'wrong type '.ref($actorSelector).' for $actorSelector' if defined $actorSelector && ref $actorSelector ne 'CDS::Selector';
  0         0  
2157              
2158 0         0 my $groupData = $actorSelector->child('group data')->isSet;
2159 0         0 my $active = $actorSelector->child('active')->isSet;
2160 0         0 my $revoked = $actorSelector->child('revoked')->isSet;
2161             return
2162             $revoked ? $o->{ui}->red('revoked') :
2163             $active && $groupData ? $o->{ui}->green('active ') :
2164             $groupData ? $o->{ui}->blue('backup ') :
2165             $active ? $o->{ui}->orange('weird ') :
2166 0 0 0     0 $o->{ui}->gray('idle ');
    0          
    0          
    0          
2167             }
2168              
2169             sub joinMember {
2170 0     0   0 my $o = shift;
2171 0         0 my $cmd = shift;
2172              
2173 0         0 $o->{accountTokens} = [];
2174 0         0 $cmd->collect($o);
2175              
2176 0         0 my $selector = $o->{actor}->actorGroupSelector;
2177 0         0 for my $accountToken (@{$o->{accountTokens}}) {
  0         0  
2178 0         0 my $actorHash = $accountToken->actorHash;
2179              
2180             # Get the public key
2181 0         0 my ($publicKey, $invalidReason, $storeError) = $o->{actor}->keyPair->getPublicKey($actorHash, $accountToken->cliStore);
2182 0 0       0 if (defined $storeError) {
2183 0         0 $o->{ui}->pRed('Unable to get the public key of ', $actorHash->hex, ' from ', $accountToken->cliStore->url, ': ', $storeError);
2184 0         0 next;
2185             }
2186              
2187 0 0       0 if (defined $invalidReason) {
2188 0         0 $o->{ui}->pRed('Unable to get the public key of ', $actorHash->hex, ' from ', $accountToken->cliStore->url, ': ', $invalidReason);
2189 0         0 next;
2190             }
2191              
2192             # Add or update this member
2193 0         0 my $label = substr($actorHash->bytes, 0, 16);
2194 0         0 my $actorSelector = $selector->child($label);
2195 0         0 my $wasMember = $actorSelector->isSet;
2196              
2197 0         0 my $record = CDS::Record->new;
2198 0         0 $record->add('hash')->addHash($actorHash);
2199 0         0 $record->add('store')->addText($accountToken->cliStore->url);
2200 0         0 $actorSelector->set($record);
2201 0         0 $actorSelector->addObject($publicKey->hash, $publicKey->object);
2202              
2203 0 0       0 $o->{ui}->pGreen('Updated ', $o->type($actorSelector), ' member ', $actorHash->hex, '.') if $wasMember;
2204 0 0       0 $o->{ui}->pGreen('Added ', $actorHash->hex, ' as ', $o->type($actorSelector), ' member of the actor group.') if ! $wasMember;
2205             }
2206              
2207             # Save
2208 0         0 $o->{actor}->saveOrShowError;
2209             }
2210              
2211             sub setFlag {
2212 0     0   0 my $o = shift;
2213 0 0 0     0 my $actorSelector = shift; die 'wrong type '.ref($actorSelector).' for $actorSelector' if defined $actorSelector && ref $actorSelector ne 'CDS::Selector';
  0         0  
2214 0         0 my $label = shift;
2215 0         0 my $value = shift;
2216              
2217 0         0 my $child = $actorSelector->child($label);
2218 0 0       0 if ($value) {
2219 0         0 $child->setBoolean(1);
2220             } else {
2221 0         0 $child->clear;
2222             }
2223             }
2224              
2225             sub setMember {
2226 0     0   0 my $o = shift;
2227 0         0 my $cmd = shift;
2228              
2229 0         0 $o->{actorHashes} = [];
2230 0         0 $cmd->collect($o);
2231              
2232 0         0 my $selector = $o->{actor}->actorGroupSelector;
2233 0         0 for my $actorHash (@{$o->{actorHashes}}) {
  0         0  
2234 0         0 my $label = substr($actorHash->bytes, 0, 16);
2235 0         0 my $actorSelector = $selector->child($label);
2236              
2237 0         0 my $record = $actorSelector->record;
2238 0         0 my $hash = $record->child('hash')->hashValue;
2239 0 0       0 if (! $hash) {
2240 0         0 $o->{ui}->pRed($actorHash->hex, ' is not a member of our actor group.');
2241 0         0 next;
2242             }
2243              
2244 0   0     0 $o->setFlag($actorSelector, 'group data', $o->{status} eq 'active' || $o->{status} eq 'backup');
2245 0         0 $o->setFlag($actorSelector, 'active', $o->{status} eq 'active');
2246 0         0 $o->setFlag($actorSelector, 'revoked', $o->{status} eq 'revoked');
2247 0         0 $o->{ui}->pGreen($actorHash->hex, ' is now ', $o->type($actorSelector), '.');
2248             }
2249              
2250             # Save
2251 0         0 $o->{actor}->saveOrShowError;
2252             }
2253              
2254             # BEGIN AUTOGENERATED
2255             package CDS::Commands::Announce;
2256              
2257             sub register {
2258 0     0   0 my $class = shift;
2259 0         0 my $cds = shift;
2260 0         0 my $help = shift;
2261              
2262 0         0 my $node000 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
2263 0         0 my $node001 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&announceMe});
2264 0         0 my $node002 = CDS::Parser::Node->new(1);
2265 0         0 my $node003 = CDS::Parser::Node->new(0);
2266 0         0 my $node004 = CDS::Parser::Node->new(0);
2267 0         0 my $node005 = CDS::Parser::Node->new(0);
2268 0         0 my $node006 = CDS::Parser::Node->new(0);
2269 0         0 my $node007 = CDS::Parser::Node->new(0);
2270 0         0 my $node008 = CDS::Parser::Node->new(0);
2271 0         0 my $node009 = CDS::Parser::Node->new(0);
2272 0         0 my $node010 = CDS::Parser::Node->new(0);
2273 0         0 my $node011 = CDS::Parser::Node->new(0);
2274 0         0 my $node012 = CDS::Parser::Node->new(0);
2275 0         0 my $node013 = CDS::Parser::Node->new(1);
2276 0         0 my $node014 = CDS::Parser::Node->new(0);
2277 0         0 my $node015 = CDS::Parser::Node->new(0);
2278 0         0 my $node016 = CDS::Parser::Node->new(0);
2279 0         0 my $node017 = CDS::Parser::Node->new(0, {constructor => \&new, function => \&announceKeyPair});
2280 0         0 $cds->addArrow($node001, 1, 0, 'announce');
2281 0         0 $cds->addArrow($node002, 1, 0, 'announce');
2282 0         0 $help->addArrow($node000, 1, 0, 'announce');
2283 0         0 $node002->addArrow($node003, 1, 0, 'KEYPAIR', \&collectKeypair);
2284 0         0 $node003->addArrow($node004, 1, 0, 'on');
2285 0         0 $node004->addArrow($node005, 1, 0, 'STORE', \&collectStore);
2286 0         0 $node005->addArrow($node006, 1, 0, 'without');
2287 0         0 $node005->addArrow($node007, 1, 0, 'with');
2288 0         0 $node005->addDefault($node017);
2289 0         0 $node006->addArrow($node006, 1, 0, 'ACTOR', \&collectActor);
2290 0         0 $node006->addArrow($node017, 1, 0, 'ACTOR', \&collectActor);
2291 0         0 $node007->addArrow($node008, 1, 0, 'active', \&collectActive);
2292 0         0 $node007->addArrow($node008, 1, 0, 'entrusted', \&collectEntrusted);
2293 0         0 $node007->addArrow($node008, 1, 0, 'idle', \&collectIdle);
2294 0         0 $node007->addArrow($node008, 1, 0, 'revoked', \&collectRevoked);
2295 0         0 $node008->addDefault($node009);
2296 0         0 $node008->addDefault($node010);
2297 0         0 $node009->addArrow($node009, 1, 0, 'ACCOUNT', \&collectAccount);
2298 0         0 $node009->addArrow($node013, 1, 1, 'ACCOUNT', \&collectAccount);
2299 0         0 $node010->addArrow($node010, 1, 0, 'ACTOR', \&collectActor1);
2300 0         0 $node010->addArrow($node011, 1, 0, 'ACTOR', \&collectActor1);
2301 0         0 $node011->addArrow($node012, 1, 0, 'on');
2302 0         0 $node012->addArrow($node013, 1, 0, 'STORE', \&collectStore1);
2303 0         0 $node013->addArrow($node014, 1, 0, 'but');
2304 0         0 $node013->addArrow($node016, 1, 0, 'and');
2305 0         0 $node013->addDefault($node017);
2306 0         0 $node014->addArrow($node015, 1, 0, 'without');
2307 0         0 $node015->addArrow($node015, 1, 0, 'ACTOR', \&collectActor);
2308 0         0 $node015->addArrow($node017, 1, 0, 'ACTOR', \&collectActor);
2309 0         0 $node016->addDefault($node007);
2310             }
2311              
2312             sub collectAccount {
2313 0     0   0 my $o = shift;
2314 0         0 my $label = shift;
2315 0         0 my $value = shift;
2316              
2317 0         0 push @{$o->{with}}, {status => $o->{status}, accountToken => $value};
  0         0  
2318             }
2319              
2320             sub collectActive {
2321 0     0   0 my $o = shift;
2322 0         0 my $label = shift;
2323 0         0 my $value = shift;
2324              
2325 0         0 $o->{status} = 'active';
2326             }
2327              
2328             sub collectActor {
2329 0     0   0 my $o = shift;
2330 0         0 my $label = shift;
2331 0         0 my $value = shift;
2332              
2333 0         0 $o->{without}->{$value->bytes} = $value;
2334             }
2335              
2336             sub collectActor1 {
2337 0     0   0 my $o = shift;
2338 0         0 my $label = shift;
2339 0         0 my $value = shift;
2340              
2341 0         0 push @{$o->{actorHashes}}, $value;
  0         0  
2342             }
2343              
2344             sub collectEntrusted {
2345 0     0   0 my $o = shift;
2346 0         0 my $label = shift;
2347 0         0 my $value = shift;
2348              
2349 0         0 $o->{status} = 'entrusted';
2350             }
2351              
2352             sub collectIdle {
2353 0     0   0 my $o = shift;
2354 0         0 my $label = shift;
2355 0         0 my $value = shift;
2356              
2357 0         0 $o->{status} = 'idle';
2358             }
2359              
2360             sub collectKeypair {
2361 0     0   0 my $o = shift;
2362 0         0 my $label = shift;
2363 0         0 my $value = shift;
2364              
2365 0         0 $o->{keyPairToken} = $value;
2366             }
2367              
2368             sub collectRevoked {
2369 0     0   0 my $o = shift;
2370 0         0 my $label = shift;
2371 0         0 my $value = shift;
2372              
2373 0         0 $o->{status} = 'revoked';
2374             }
2375              
2376             sub collectStore {
2377 0     0   0 my $o = shift;
2378 0         0 my $label = shift;
2379 0         0 my $value = shift;
2380              
2381 0         0 $o->{store} = $value;
2382             }
2383              
2384             sub collectStore1 {
2385 0     0   0 my $o = shift;
2386 0         0 my $label = shift;
2387 0         0 my $value = shift;
2388              
2389 0         0 for my $actorHash (@{$o->{actorHashes}}) {
  0         0  
2390 0         0 my $accountToken = CDS::AccountToken->new($value, $actorHash);
2391 0         0 push @{$o->{with}}, {status => $o->{status}, accountToken => $accountToken};
  0         0  
2392             }
2393              
2394 0         0 $o->{actorHashes} = [];
2395             }
2396              
2397             sub new {
2398 0     0   0 my $class = shift;
2399 0         0 my $actor = shift;
2400 0         0 bless {actor => $actor, ui => $actor->ui} }
2401              
2402             # END AUTOGENERATED
2403              
2404             # HTML FOLDER NAME announce
2405             # HTML TITLE Announce
2406             sub help {
2407 0     0   0 my $o = shift;
2408 0         0 my $cmd = shift;
2409              
2410 0         0 my $ui = $o->{ui};
2411 0         0 $ui->space;
2412 0         0 $ui->command('cds announce');
2413 0         0 $ui->p('Announces yourself on your accounts.');
2414 0         0 $ui->space;
2415 0         0 $ui->command('cds announce KEYPAIR on STORE');
2416 0         0 $ui->command('… with (active|idle|revoked|entrusted) ACCOUNT*');
2417 0         0 $ui->command('… with (active|idle|revoked|entrusted) ACTOR* on STORE');
2418 0         0 $ui->command('… without ACTOR*');
2419 0         0 $ui->command('… with … and … and … but without …');
2420 0         0 $ui->p('Updates the public card of the indicated key pair on the indicated store. The indicated accounts are added or removed from the actor group on the card.');
2421 0         0 $ui->p('If no card exists, a minimalistic card is created.');
2422 0         0 $ui->p('Use this with care, as the generated card may not be compliant with the card produced by the actor.');
2423 0         0 $ui->space;
2424             }
2425              
2426             sub announceMe {
2427 0     0   0 my $o = shift;
2428 0         0 my $cmd = shift;
2429              
2430 0         0 $o->announceOnStore($o->{actor}->storageStore);
2431 0 0       0 $o->announceOnStore($o->{actor}->messagingStore) if $o->{actor}->messagingStore->id ne $o->{actor}->storageStore->id;
2432 0         0 $o->{ui}->space;
2433             }
2434              
2435             sub announceOnStore {
2436 0     0   0 my $o = shift;
2437 0         0 my $store = shift;
2438              
2439 0         0 $o->{ui}->space;
2440 0         0 $o->{ui}->title($store->url);
2441 0         0 my ($envelopeHash, $cardHash, $invalidReason, $storeError) = $o->{actor}->announce($store);
2442 0 0       0 return if defined $storeError;
2443 0 0       0 return $o->{ui}->pRed($invalidReason) if defined $invalidReason;
2444 0         0 $o->{ui}->pGreen('Announced');
2445             }
2446              
2447             sub announceKeyPair {
2448 0     0   0 my $o = shift;
2449 0         0 my $cmd = shift;
2450              
2451 0         0 $o->{actors} = [];
2452 0         0 $o->{with} = [];
2453 0         0 $o->{without} = {};
2454 0         0 $o->{now} = CDS->now;
2455 0         0 $cmd->collect($o);
2456              
2457             # List
2458 0         0 $o->{keyPair} = $o->{keyPairToken}->keyPair;
2459 0         0 my ($hashes, $listError) = $o->{store}->list($o->{keyPair}->publicKey->hash, 'public', 0, $o->{keyPair});
2460 0 0       0 return if defined $listError;
2461              
2462             # Check if there are more than one cards
2463 0 0       0 if (scalar @$hashes > 1) {
2464 0         0 $o->{ui}->space;
2465 0         0 $o->{ui}->p('This account contains more than one public card:');
2466 0         0 $o->{ui}->pushIndent;
2467 0         0 for my $hash (@$hashes) {
2468 0         0 $o->{ui}->line($o->{ui}->gold('cds show card ', $hash->hex, ' on ', $o->{storeUrl}));
2469             }
2470 0         0 $o->{ui}->popIndent;
2471 0         0 $o->{ui}->p('Remove all but the most recent card. Cards can be removed as follows:');
2472 0         0 my $keyPairReference = $o->{actor}->blueKeyPairReference($o->{keyPairToken});
2473 0         0 $o->{ui}->line($o->{ui}->gold('cds remove ', 'HASH', ' on ', $o->{storeUrl}, ' using ', $keyPairReference));
2474 0         0 $o->{ui}->space;
2475 0         0 return;
2476             }
2477              
2478             # Read the card
2479 0 0       0 my $cardRecord = scalar @$hashes ? $o->readCard($hashes->[0]) : CDS::Record->new;
2480 0 0       0 return if ! $cardRecord;
2481              
2482             # Parse
2483 0         0 my $builder = CDS::ActorGroupBuilder->new;
2484 0         0 $builder->parse($cardRecord, 0);
2485              
2486             # Apply the changes
2487 0         0 for my $change (@{$o->{with}}) {
  0         0  
2488 0 0       0 if ($change->{status} eq 'entrusted') {
2489 0         0 $builder->addEntrustedActor($change->{accountToken}->cliStore->url, $change->{accountToken}->actorHash);
2490 0         0 $builder->{entrustedActorsRevision} = $o->{now};
2491             } else {
2492 0         0 $builder->addMember($change->{accountToken}->cliStore->url, $change->{accountToken}->actorHash, $o->{now}, $change->{status});
2493             }
2494             }
2495              
2496 0         0 for my $hash (values %{$o->{without}}) {
  0         0  
2497 0         0 $builder->removeEntrustedActor($hash)
2498             }
2499              
2500 0         0 for my $member ($builder->members) {
2501 0 0       0 next if ! $o->{without}->{$member->hash->bytes};
2502 0         0 $builder->removeMember($member->storeUrl, $member->hash);
2503             }
2504              
2505             # Write the new card
2506 0         0 my $newCard = $builder->toRecord(0);
2507 0         0 $newCard->add('public key')->addHash($o->{keyPair}->publicKey->hash);
2508              
2509 0         0 for my $child ($cardRecord->children) {
2510 0 0       0 if ($child->bytes eq 'actor group') {
    0          
    0          
2511             } elsif ($child->bytes eq 'entrusted actors') {
2512             } elsif ($child->bytes eq 'public key') {
2513             } else {
2514 0         0 $newCard->addRecord($child);
2515             }
2516             }
2517              
2518 0         0 $o->announce($newCard, $hashes);
2519             }
2520              
2521             sub readCard {
2522 0     0   0 my $o = shift;
2523 0 0 0     0 my $envelopeHash = shift; die 'wrong type '.ref($envelopeHash).' for $envelopeHash' if defined $envelopeHash && ref $envelopeHash ne 'CDS::Hash';
  0         0  
2524              
2525             # Open the envelope
2526 0         0 my ($object, $storeError) = $o->{store}->get($envelopeHash, $o->{keyPair});
2527 0 0       0 return if defined $storeError;
2528 0 0       0 return $o->{ui}->error('Envelope object ', $envelopeHash->hex, ' not found.') if ! $object;
2529              
2530 0   0     0 my $envelope = CDS::Record->fromObject($object) // return $o->{ui}->error($envelopeHash->hex, ' is not a record.');
2531 0   0     0 my $cardHash = $envelope->child('content')->hashValue // return $o->{ui}->error($envelopeHash->hex, ' is not a valid envelope, because it has no content hash.');
2532 0 0       0 return $o->{ui}->error($envelopeHash->hex, ' has an invalid signature.') if ! CDS->verifyEnvelopeSignature($envelope, $o->{keyPair}->publicKey, $cardHash);
2533              
2534             # Read the card
2535 0         0 my ($cardObject, $storeError1) = $o->{store}->get($cardHash, $o->{keyPair});
2536 0 0       0 return if defined $storeError1;
2537 0 0       0 return $o->{ui}->error('Card object ', $cardHash->hex, ' not found.') if ! $cardObject;
2538              
2539 0   0     0 return CDS::Record->fromObject($cardObject) // return $o->{ui}->error($cardHash->hex, ' is not a record.');
2540             }
2541              
2542             sub applyChanges {
2543 0     0   0 my $o = shift;
2544 0 0 0     0 my $actorGroup = shift; die 'wrong type '.ref($actorGroup).' for $actorGroup' if defined $actorGroup && ref $actorGroup ne 'CDS::ActorGroup';
  0         0  
2545 0         0 my $status = shift;
2546 0         0 my $accounts = shift;
2547              
2548 0         0 for my $account (@$accounts) {
2549 0         0 $actorGroup->{$account->url} = {storeUrl => $account->cliStore->url, actorHash => $account->actorHash, revision => $o->{now}, status => $status};
2550             }
2551             }
2552              
2553             sub announce {
2554 0     0   0 my $o = shift;
2555 0         0 my $card = shift;
2556 0         0 my $sourceHashes = shift;
2557              
2558 0         0 my $inMemoryStore = CDS::InMemoryStore->create;
2559              
2560             # Serialize the card
2561 0         0 my $cardObject = $card->toObject;
2562 0         0 my $cardHash = $cardObject->calculateHash;
2563 0         0 $inMemoryStore->put($cardHash, $cardObject);
2564 0         0 $inMemoryStore->put($o->{keyPair}->publicKey->hash, $o->{keyPair}->publicKey->object);
2565              
2566             # Prepare the public envelope
2567 0         0 my $envelopeObject = $o->{keyPair}->createPublicEnvelope($cardHash)->toObject;
2568 0         0 my $envelopeHash = $envelopeObject->calculateHash;
2569 0         0 $inMemoryStore->put($envelopeHash, $envelopeObject);
2570              
2571             # Transfer
2572 0         0 my ($missingHash, $failedStore, $storeError) = $o->{keyPair}->transfer([$envelopeHash], $inMemoryStore, $o->{store});
2573 0 0       0 return if $storeError;
2574 0 0       0 return $o->{ui}->pRed('Object ', $missingHash, ' is missing.') if $missingHash;
2575              
2576             # Modify
2577 0         0 my $modifications = CDS::StoreModifications->new;
2578 0         0 $modifications->add($o->{keyPair}->publicKey->hash, 'public', $envelopeHash);
2579 0         0 for my $hash (@$sourceHashes) {
2580 0         0 $modifications->remove($o->{keyPair}->publicKey->hash, 'public', $hash);
2581             }
2582              
2583 0         0 my $modifyError = $o->{store}->modify($modifications, $o->{keyPair});
2584 0 0       0 return if $modifyError;
2585              
2586 0         0 $o->{ui}->pGreen('Announced on ', $o->{store}->url, '.');
2587             }
2588              
2589             # BEGIN AUTOGENERATED
2590             package CDS::Commands::Book;
2591              
2592             sub register {
2593 0     0   0 my $class = shift;
2594 0         0 my $cds = shift;
2595 0         0 my $help = shift;
2596              
2597 0         0 my $node000 = CDS::Parser::Node->new(0);
2598 0         0 my $node001 = CDS::Parser::Node->new(0);
2599 0         0 my $node002 = CDS::Parser::Node->new(0);
2600 0         0 my $node003 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
2601 0         0 my $node004 = CDS::Parser::Node->new(0);
2602 0         0 my $node005 = CDS::Parser::Node->new(0);
2603 0         0 my $node006 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&book});
2604 0         0 $cds->addArrow($node000, 1, 0, 'book');
2605 0         0 $cds->addArrow($node001, 1, 0, 'book');
2606 0         0 $cds->addArrow($node002, 1, 0, 'book');
2607 0         0 $help->addArrow($node003, 1, 0, 'book');
2608 0         0 $node000->addArrow($node000, 1, 0, 'HASH', \&collectHash);
2609 0         0 $node000->addArrow($node004, 1, 0, 'HASH', \&collectHash);
2610 0         0 $node001->addArrow($node001, 1, 0, 'OBJECT', \&collectObject);
2611 0         0 $node001->addArrow($node006, 1, 0, 'OBJECT', \&collectObject);
2612 0         0 $node002->addArrow($node002, 1, 0, 'HASH', \&collectHash);
2613 0         0 $node002->addArrow($node006, 1, 0, 'HASH', \&collectHash);
2614 0         0 $node004->addArrow($node005, 1, 0, 'on');
2615 0         0 $node005->addArrow($node005, 1, 0, 'STORE', \&collectStore);
2616 0         0 $node005->addArrow($node006, 1, 0, 'STORE', \&collectStore);
2617             }
2618              
2619             sub collectHash {
2620 0     0   0 my $o = shift;
2621 0         0 my $label = shift;
2622 0         0 my $value = shift;
2623              
2624 0         0 push @{$o->{hashes}}, $value;
  0         0  
2625             }
2626              
2627             sub collectObject {
2628 0     0   0 my $o = shift;
2629 0         0 my $label = shift;
2630 0         0 my $value = shift;
2631              
2632 0         0 push @{$o->{objectTokens}}, $value;
  0         0  
2633             }
2634              
2635             sub collectStore {
2636 0     0   0 my $o = shift;
2637 0         0 my $label = shift;
2638 0         0 my $value = shift;
2639              
2640 0         0 push @{$o->{stores}}, $value;
  0         0  
2641             }
2642              
2643             sub new {
2644 0     0   0 my $class = shift;
2645 0         0 my $actor = shift;
2646 0         0 bless {actor => $actor, ui => $actor->ui} }
2647              
2648             # END AUTOGENERATED
2649              
2650             # HTML FOLDER NAME store-book
2651             # HTML TITLE Book
2652             sub help {
2653 0     0   0 my $o = shift;
2654 0         0 my $cmd = shift;
2655              
2656 0         0 my $ui = $o->{ui};
2657 0         0 $ui->space;
2658 0         0 $ui->command('cds book OBJECT*');
2659 0         0 $ui->command('cds book HASH* on STORE*');
2660 0         0 $ui->p('Books all indicated objects and reports whether booking as successful.');
2661 0         0 $ui->space;
2662 0         0 $ui->command('cds book HASH*');
2663 0         0 $ui->p('As above, but uses the selected store.');
2664 0         0 $ui->space;
2665             }
2666              
2667             sub book {
2668 0     0   0 my $o = shift;
2669 0         0 my $cmd = shift;
2670              
2671 0         0 $o->{keyPair} = $o->{actor}->preferredKeyPairToken->keyPair;
2672 0         0 $o->{hashes} = [];
2673 0         0 $o->{stores} = [];
2674 0         0 $o->{objectTokens} = [];
2675 0         0 $cmd->collect($o);
2676              
2677             # Use the selected store
2678 0 0       0 push @{$o->{stores}}, $o->{actor}->preferredStore if ! scalar @{$o->{stores}};
  0         0  
  0         0  
2679              
2680             # Book all hashes on all stores
2681 0         0 my %triedStores;
2682 0         0 for my $store (@{$o->{stores}}) {
  0         0  
2683 0 0       0 next if $triedStores{$store->url};
2684 0         0 $triedStores{$store->url} = 1;
2685 0         0 for my $hash (@{$o->{hashes}}) {
  0         0  
2686 0         0 $o->process($store, $hash);
2687             }
2688             }
2689              
2690             # Book the direct object references
2691 0         0 for my $objectToken (@{$o->{objectTokens}}) {
  0         0  
2692 0         0 $o->process($objectToken->cliStore, $objectToken->hash);
2693             }
2694              
2695             # Warn the user if no key pair is selected
2696 0 0       0 return if ! $o->{hasErrors};
2697 0 0       0 return if $o->{keyPair};
2698 0         0 $o->{ui}->space;
2699 0         0 $o->{ui}->warning('Since no key pair is selected, the bookings were requested without signature. Stores are more likely to accept signed bookings. To add a signature, select a key pair using "cds use …", or create your key pair using "cds create my key pair".');
2700             }
2701              
2702             sub process {
2703 0     0   0 my $o = shift;
2704 0         0 my $store = shift;
2705 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
2706              
2707             # Upload the object
2708 0         0 my $success = $store->book($hash, $o->{keyPair});
2709 0 0       0 if ($success) {
2710 0         0 $o->{ui}->line($o->{ui}->green('OK '), $hash->hex, ' on ', $store->url);
2711             } else {
2712 0         0 $o->{ui}->line($o->{ui}->red('not found '), $hash->hex, ' on ', $store->url);
2713 0         0 $o->{hasErrors} = 1;
2714             }
2715             }
2716              
2717             # BEGIN AUTOGENERATED
2718             package CDS::Commands::CheckKeyPair;
2719              
2720             sub register {
2721 0     0   0 my $class = shift;
2722 0         0 my $cds = shift;
2723 0         0 my $help = shift;
2724              
2725 0         0 my $node000 = CDS::Parser::Node->new(0);
2726 0         0 my $node001 = CDS::Parser::Node->new(0);
2727 0         0 my $node002 = CDS::Parser::Node->new(0);
2728 0         0 my $node003 = CDS::Parser::Node->new(0);
2729 0         0 my $node004 = CDS::Parser::Node->new(0);
2730 0         0 my $node005 = CDS::Parser::Node->new(0);
2731 0         0 my $node006 = CDS::Parser::Node->new(0);
2732 0         0 my $node007 = CDS::Parser::Node->new(0);
2733 0         0 my $node008 = CDS::Parser::Node->new(0);
2734 0         0 my $node009 = CDS::Parser::Node->new(0);
2735 0         0 my $node010 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
2736 0         0 my $node011 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&checkKeyPair});
2737 0         0 $cds->addArrow($node004, 1, 0, 'check');
2738 0         0 $cds->addArrow($node005, 1, 0, 'fix');
2739 0         0 $help->addArrow($node000, 1, 0, 'check');
2740 0         0 $help->addArrow($node001, 1, 0, 'fix');
2741 0         0 $node000->addArrow($node002, 1, 0, 'key');
2742 0         0 $node001->addArrow($node003, 1, 0, 'key');
2743 0         0 $node002->addArrow($node010, 1, 0, 'pair');
2744 0         0 $node003->addArrow($node010, 1, 0, 'pair');
2745 0         0 $node004->addArrow($node006, 1, 0, 'key');
2746 0         0 $node005->addArrow($node007, 1, 0, 'key');
2747 0         0 $node006->addArrow($node008, 1, 0, 'pair');
2748 0         0 $node007->addArrow($node009, 1, 0, 'pair');
2749 0         0 $node008->addArrow($node011, 1, 0, 'FILE', \&collectFile);
2750 0         0 $node009->addArrow($node011, 1, 0, 'FILE', \&collectFile1);
2751             }
2752              
2753             sub collectFile {
2754 0     0   0 my $o = shift;
2755 0         0 my $label = shift;
2756 0         0 my $value = shift;
2757              
2758 0         0 $o->{file} = $value;
2759             }
2760              
2761             sub collectFile1 {
2762 0     0   0 my $o = shift;
2763 0         0 my $label = shift;
2764 0         0 my $value = shift;
2765              
2766 0         0 $o->{file} = $value;
2767 0         0 $o->{fix} = 1;
2768             }
2769              
2770             sub new {
2771 0     0   0 my $class = shift;
2772 0         0 my $actor = shift;
2773 0         0 bless {actor => $actor, ui => $actor->ui} }
2774              
2775             # END AUTOGENERATED
2776              
2777             # HTML FOLDER NAME check-key-pair
2778             # HTML TITLE Check key pair
2779             sub help {
2780 0     0   0 my $o = shift;
2781 0         0 my $cmd = shift;
2782              
2783 0         0 my $ui = $o->{ui};
2784 0         0 $ui->space;
2785 0         0 $ui->command('cds check key pair FILE');
2786 0         0 $ui->p('Checks if the key pair FILE is complete, i.e. that a valid private key and a matching public key exists.');
2787 0         0 $ui->space;
2788             }
2789              
2790             sub checkKeyPair {
2791 0     0   0 my $o = shift;
2792 0         0 my $cmd = shift;
2793              
2794 0         0 $cmd->collect($o);
2795              
2796             # Check if we have a complete private key
2797 0   0     0 my $bytes = CDS->readBytesFromFile($o->{file}) // return $o->{ui}->error('The file "', $o->{file}, '" cannot be read.');
2798 0         0 my $record = CDS::Record->fromObject(CDS::Object->fromBytes($bytes));
2799              
2800 0         0 my $rsaKey = $record->child('rsa key');
2801 0         0 my $e = $rsaKey->child('e')->bytesValue;
2802 0 0       0 return $o->{ui}->error('The exponent "e" of the private key is missing.') if ! length $e;
2803 0         0 my $p = $rsaKey->child('p')->bytesValue;
2804 0 0       0 return $o->{ui}->error('The prime "p" of the private key is missing.') if ! length $p;
2805 0         0 my $q = $rsaKey->child('q')->bytesValue;
2806 0 0       0 return $o->{ui}->error('The prime "q" of the private key is missing.') if ! length $q;
2807 0         0 $o->{ui}->pGreen('The private key is complete.');
2808              
2809             # Derive the public key
2810 0         0 my $privateKey = CDS::C::privateKeyNew($e, $p, $q);
2811 0         0 my $publicKey = CDS::C::publicKeyFromPrivateKey($privateKey);
2812 0         0 my $n = CDS::C::publicKeyN($publicKey);
2813              
2814             # Check if we have a matching public key
2815 0         0 my $publicKeyObjectBytes = $record->child('public key object')->bytesValue;
2816 0 0       0 return $o->{ui}->error('The public key is missing.') if ! length $publicKeyObjectBytes;
2817 0   0     0 $o->{publicKeyObject} = CDS::Object->fromBytes($publicKeyObjectBytes) // return $o->{ui}->error('The public key is is not a valid Condensation object.');
2818 0         0 $o->{publicKeyHash} = $o->{publicKeyObject}->calculateHash;
2819 0         0 my $publicKeyRecord = CDS::Record->fromObject($o->{publicKeyObject});
2820 0 0       0 return $o->{ui}->error('The public key is not a valid record.') if ! $publicKeyRecord;
2821 0         0 my $publicN = $publicKeyRecord->child('n')->bytesValue;
2822 0 0       0 return $o->{ui}->error('The modulus "n" of the public key is missing.') if ! length $publicN;
2823 0   0     0 my $publicE = $publicKeyRecord->child('e')->bytesValue // $o->{ui}->error('The public key is incomplete.');
2824 0 0       0 return $o->{ui}->error('The exponent "e" of the public key is missing.') if ! length $publicE;
2825 0 0       0 return $o->{ui}->error('The exponent "e" of the public key does not match the exponent "e" of the private key.') if $publicE ne $e;
2826 0 0       0 return $o->{ui}->error('The modulus "n" of the public key does not correspond to the primes "p" and "q" of the private key.') if $publicN ne $n;
2827 0         0 $o->{ui}->pGreen('The public key ', $o->{publicKeyHash}->hex, ' is complete.');
2828              
2829             # At this point, the configuration looks good, and we can load the key pair
2830 0   0     0 CDS::KeyPair->fromRecord($record) // $o->{ui}->error('Your key pair looks complete, but could not be loaded.');
2831             }
2832              
2833             # BEGIN AUTOGENERATED
2834             package CDS::Commands::CollectGarbage;
2835              
2836             sub register {
2837 0     0   0 my $class = shift;
2838 0         0 my $cds = shift;
2839 0         0 my $help = shift;
2840              
2841 0         0 my $node000 = CDS::Parser::Node->new(0);
2842 0         0 my $node001 = CDS::Parser::Node->new(0);
2843 0         0 my $node002 = CDS::Parser::Node->new(0);
2844 0         0 my $node003 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
2845 0         0 my $node004 = CDS::Parser::Node->new(0, {constructor => \&new, function => \&collectGarbage});
2846 0         0 my $node005 = CDS::Parser::Node->new(0);
2847 0         0 my $node006 = CDS::Parser::Node->new(0, {constructor => \&new, function => \&reportGarbage});
2848 0         0 my $node007 = CDS::Parser::Node->new(0);
2849 0         0 my $node008 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&collectGarbage});
2850 0         0 my $node009 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&reportGarbage});
2851 0         0 $cds->addArrow($node001, 1, 0, 'report');
2852 0         0 $cds->addArrow($node002, 1, 0, 'collect');
2853 0         0 $help->addArrow($node000, 1, 0, 'collect');
2854 0         0 $node000->addArrow($node003, 1, 0, 'garbage');
2855 0         0 $node001->addArrow($node006, 1, 0, 'garbage');
2856 0         0 $node002->addArrow($node004, 1, 0, 'garbage');
2857 0         0 $node004->addArrow($node005, 1, 0, 'of');
2858 0         0 $node004->addDefault($node008);
2859 0         0 $node005->addArrow($node008, 1, 0, 'STORE', \&collectStore);
2860 0         0 $node006->addArrow($node007, 1, 0, 'of');
2861 0         0 $node006->addDefault($node009);
2862 0         0 $node007->addArrow($node009, 1, 0, 'STORE', \&collectStore);
2863             }
2864              
2865             sub collectStore {
2866 0     0   0 my $o = shift;
2867 0         0 my $label = shift;
2868 0         0 my $value = shift;
2869              
2870 0         0 $o->{store} = $value;
2871             }
2872              
2873             sub new {
2874 0     0   0 my $class = shift;
2875 0         0 my $actor = shift;
2876 0         0 bless {actor => $actor, ui => $actor->ui} }
2877              
2878             # END AUTOGENERATED
2879              
2880             # HTML FOLDER NAME collect-garbage
2881             # HTML TITLE Garbage collection
2882             sub help {
2883 0     0   0 my $o = shift;
2884 0         0 my $cmd = shift;
2885              
2886 0         0 my $ui = $o->{ui};
2887 0         0 $ui->space;
2888 0         0 $ui->command('cds collect garbage [of STORE]');
2889 0         0 $ui->p('Runs garbage collection. STORE must be a folder store. Objects not in use, and older than 1 day are removed from the store.');
2890 0         0 $ui->p('If no store is provided, garbage collection is run on the selected store, or the actor\'s storage store.');
2891 0         0 $ui->space;
2892 0         0 $ui->p('The store must not be written to while garbage collection is running. Objects booked during garbage collection may get deleted, and leave the store in a corrupt state. Reading from the store is fine.');
2893 0         0 $ui->space;
2894 0         0 $ui->command('cds report garbage [of STORE]');
2895 0         0 $ui->p('As above, but reports obsolete objects rather than deleting them. A protocol (shell script) is written to ".garbage" in the store folder.');
2896 0         0 $ui->space;
2897             }
2898              
2899             sub collectGarbage {
2900 0     0   0 my $o = shift;
2901 0         0 my $cmd = shift;
2902              
2903 0         0 $cmd->collect($o);
2904 0         0 $o->run(CDS::Commands::CollectGarbage::Delete->new($o->{ui}));
2905             }
2906              
2907             sub wrapUpDeletion {
2908 0     0   0 my $o = shift;
2909             }
2910              
2911             sub reportGarbage {
2912 0     0   0 my $o = shift;
2913 0         0 my $cmd = shift;
2914              
2915 0         0 $cmd->collect($o);
2916 0         0 $o->run(CDS::Commands::CollectGarbage::Report->new($o->{ui}));
2917 0         0 $o->{ui}->space;
2918             }
2919              
2920             # Creates a folder with the selected permissions.
2921             sub run {
2922 0     0   0 my $o = shift;
2923 0         0 my $handler = shift;
2924              
2925             # Prepare
2926 0   0     0 my $store = $o->{store} // $o->{actor}->selectedStore // $o->{actor}->storageStore;
      0        
2927 0   0     0 my $folderStore = CDS::FolderStore->forUrl($store->url) // return $o->{ui}->error('"', $store->url, '" is not a folder store.');
2928 0   0     0 $handler->initialize($folderStore) // return;
2929              
2930 0         0 $o->{storeFolder} = $folderStore->folder;
2931 0         0 $o->{accountsFolder} = $folderStore->folder.'/accounts';
2932 0         0 $o->{objectsFolder} = $folderStore->folder.'/objects';
2933 0         0 my $dateLimit = time - 86400;
2934 0         0 my $envelopeExpirationLimit = time * 1000;
2935              
2936             # Read the tree index
2937 0         0 $o->readIndex;
2938              
2939             # Process all accounts
2940 0         0 $o->{ui}->space;
2941 0         0 $o->{ui}->title($o->{ui}->left(64, 'Accounts'), ' ', $o->{ui}->right(10, 'messages'), ' ', $o->{ui}->right(10, 'private'), ' ', $o->{ui}->right(10, 'public'), ' ', 'last modification');
2942 0         0 $o->startProgress('accounts');
2943 0         0 $o->{usedHashes} = {};
2944 0         0 $o->{missingObjects} = {};
2945 0         0 $o->{brokenOrigins} = {};
2946 0         0 my $countAccounts = 0;
2947 0         0 my $countKeptEnvelopes = 0;
2948 0         0 my $countDeletedEnvelopes = 0;
2949 0         0 for my $accountHash (sort { $$a cmp $$b } $folderStore->accounts) {
  0         0  
2950             # This would be the private key, but we don't use it right now
2951 0         0 $o->{usedHashes}->{$accountHash->hex} = 1;
2952              
2953 0         0 my $newestDate = 0;
2954 0         0 my %sizeByBox;
2955 0         0 my $accountFolder = $o->{accountsFolder}.'/'.$accountHash->hex;
2956 0         0 foreach my $boxLabel (CDS->listFolder($accountFolder)) {
2957 0 0       0 next if $boxLabel =~ /^\./;
2958 0         0 my $boxFolder = $accountFolder.'/'.$boxLabel;
2959 0         0 my $date = &lastModified($boxFolder);
2960 0 0       0 $newestDate = $date if $newestDate < $date;
2961 0         0 my $size = 0;
2962 0         0 foreach my $filename (CDS->listFolder($boxFolder)) {
2963 0 0       0 next if $filename =~ /^\./;
2964 0         0 my $hash = pack('H*', $filename);
2965 0         0 my $file = $boxFolder.'/'.$filename;
2966              
2967 0         0 my $timestamp = $o->envelopeExpiration($hash, $boxFolder);
2968 0 0 0     0 if ($timestamp > 0 && $timestamp < $envelopeExpirationLimit) {
2969 0         0 $countDeletedEnvelopes += 1;
2970 0   0     0 $handler->deleteEnvelope($file) // return;
2971 0         0 next;
2972             }
2973              
2974 0         0 $countKeptEnvelopes += 1;
2975 0         0 my $date = &lastModified($file);
2976 0 0       0 $newestDate = $date if $newestDate < $date;
2977 0         0 $size += $o->traverse($hash, $boxFolder);
2978             }
2979 0         0 $sizeByBox{$boxLabel} = $size;
2980             }
2981              
2982             $o->{ui}->line($accountHash->hex, ' ',
2983             $o->{ui}->right(10, $o->{ui}->niceFileSize($sizeByBox{'messages'} || 0)), ' ',
2984             $o->{ui}->right(10, $o->{ui}->niceFileSize($sizeByBox{'private'} || 0)), ' ',
2985             $o->{ui}->right(10, $o->{ui}->niceFileSize($sizeByBox{'public'} || 0)), ' ',
2986 0 0 0     0 $newestDate == 0 ? 'never' : $o->{ui}->niceDateTime($newestDate * 1000));
      0        
      0        
2987              
2988 0         0 $countAccounts += 1;
2989             }
2990              
2991 0         0 $o->{ui}->line($countAccounts, ' accounts traversed');
2992 0         0 $o->{ui}->space;
2993              
2994             # Mark all objects that are younger than 1 day (so that objects being uploaded right now but not linked yet remain)
2995 0         0 $o->{ui}->title('Objects');
2996 0         0 $o->startProgress('objects');
2997              
2998 0         0 my %objects;
2999 0         0 my @topFolders = sort grep {$_ !~ /^\./} CDS->listFolder($o->{objectsFolder});
  0         0  
3000 0         0 foreach my $topFolder (@topFolders) {
3001 0         0 my @files = sort grep {$_ !~ /^\./} CDS->listFolder($o->{objectsFolder}.'/'.$topFolder);
  0         0  
3002 0         0 foreach my $filename (@files) {
3003 0         0 $o->incrementProgress;
3004 0         0 my $hash = pack 'H*', $topFolder.$filename;
3005 0         0 my @s = stat $o->{objectsFolder}.'/'.$topFolder.'/'.$filename;
3006 0         0 $objects{$hash} = $s[7];
3007 0 0       0 next if $s[9] < $dateLimit;
3008 0         0 $o->traverse($hash, 'recent object');
3009             }
3010             }
3011              
3012 0         0 $o->{ui}->line(scalar keys %objects, ' objects traversed');
3013 0         0 $o->{ui}->space;
3014              
3015             # Delete all unmarked objects, and add the marked objects to the new tree index
3016 0         0 my $index = CDS::Record->new;
3017 0         0 my $countKeptObjects = 0;
3018 0         0 my $sizeKeptObjects = 0;
3019 0         0 my $countDeletedObjects = 0;
3020 0         0 my $sizeDeletedObjects = 0;
3021              
3022 0         0 $handler->startDeletion;
3023 0         0 $o->startProgress('delete-objects');
3024 0         0 for my $hash (keys %objects) {
3025 0         0 my $size = $objects{$hash};
3026 0 0       0 if (exists $o->{usedHashes}->{$hash}) {
3027 0         0 $countKeptObjects += 1;
3028 0         0 $sizeKeptObjects += $size;
3029 0         0 my $entry = $o->{index}->{$hash};
3030 0 0       0 $index->addRecord($entry) if $entry;
3031             } else {
3032 0         0 $o->incrementProgress;
3033 0         0 $countDeletedObjects += 1;
3034 0         0 $sizeDeletedObjects += $size;
3035 0         0 my $hashHex = unpack 'H*', $hash;
3036 0         0 my $file = $o->{objectsFolder}.'/'.substr($hashHex, 0, 2).'/'.substr($hashHex, 2);
3037 0   0     0 $handler->deleteObject($file) // return;
3038             }
3039             }
3040              
3041             # Write the new tree index
3042 0         0 CDS->writeBytesToFile($o->{storeFolder}.'/.index-new', $index->toObject->bytes);
3043 0         0 rename $o->{storeFolder}.'/.index-new', $o->{storeFolder}.'/.index';
3044              
3045             # Show what has been done
3046 0         0 $o->{ui}->space;
3047 0         0 $o->{ui}->line($countDeletedEnvelopes, ' ', $handler->{deletedEnvelopesText});
3048 0         0 $o->{ui}->line($countKeptEnvelopes, ' ', $handler->{keptEnvelopesText});
3049 0         0 my $line1 = $countDeletedObjects.' '.$handler->{deletedObjectsText};
3050 0         0 my $line2 = $countKeptObjects.' '.$handler->{keptObjectsText};
3051 0         0 my $maxLength = CDS->max(length $line1, length $line2);
3052 0         0 $o->{ui}->line($o->{ui}->left($maxLength, $line1), ' ', $o->{ui}->gray($o->{ui}->niceFileSize($sizeDeletedObjects)));
3053 0         0 $o->{ui}->line($o->{ui}->left($maxLength, $line2), ' ', $o->{ui}->gray($o->{ui}->niceFileSize($sizeKeptObjects)));
3054 0         0 $o->{ui}->space;
3055 0         0 $handler->wrapUp;
3056              
3057 0         0 my $missing = scalar keys %{$o->{missingObjects}};
  0         0  
3058 0 0       0 if ($missing) {
3059 0         0 $o->{ui}->warning($missing, ' objects are referenced from other objects, but missing:');
3060              
3061 0         0 my $count = 0;
3062 0         0 for my $hashBytes (sort keys %{$o->{missingObjects}}) {
  0         0  
3063 0         0 $o->{ui}->warning(' ', unpack('H*', $hashBytes));
3064              
3065 0         0 $count += 1;
3066 0 0 0     0 if ($missing > 10 && $count > 5) {
3067 0         0 $o->{ui}->warning(' …');
3068 0         0 last;
3069             }
3070             }
3071              
3072 0         0 $o->{ui}->space;
3073 0         0 $o->{ui}->warning('The missing objects are from the following origins:');
3074 0         0 for my $origin (sort keys %{$o->{brokenOrigins}}) {
  0         0  
3075 0         0 $o->{ui}->line(' ', $o->{ui}->orange($origin));
3076             }
3077              
3078 0         0 $o->{ui}->space;
3079             }
3080             }
3081              
3082             sub traverse {
3083 0     0   0 my $o = shift;
3084 0         0 my $hashBytes = shift;
3085 0         0 my $origin = shift;
3086              
3087 0 0       0 return $o->{usedHashes}->{$hashBytes} if exists $o->{usedHashes}->{$hashBytes};
3088              
3089             # Get index information about the object
3090 0   0     0 my $record = $o->index($hashBytes, $origin) // return 0;
3091 0         0 my $size = $record->nthChild(0)->asInteger;
3092              
3093             # Process children
3094 0         0 my $pos = 0;
3095 0         0 my $hashes = $record->nthChild(1)->bytes;
3096 0         0 while ($pos < length $hashes) {
3097 0         0 $size += $o->traverse(substr($hashes, $pos, 32), $origin);
3098 0         0 $pos += 32;
3099             }
3100              
3101             # Keep the size for future use
3102 0         0 $o->{usedHashes}->{$hashBytes} = $size;
3103 0         0 return $size;
3104             }
3105              
3106             sub readIndex {
3107 0     0   0 my $o = shift;
3108              
3109 0         0 $o->{index} = {};
3110 0         0 my $file = $o->{storeFolder}.'/.index';
3111 0   0     0 my $record = CDS::Record->fromObject(CDS::Object->fromBytes(CDS->readBytesFromFile($file))) // return;
3112 0         0 for my $child ($record->children) {
3113 0         0 $o->{index}->{$child->bytes} = $child;
3114             }
3115             }
3116              
3117             sub index {
3118 0     0   0 my $o = shift;
3119 0         0 my $hashBytes = shift;
3120 0         0 my $origin = shift;
3121              
3122 0         0 $o->incrementProgress;
3123              
3124             # Report a known result
3125 0 0       0 if ($o->{missingObjects}->{$hashBytes}) {
3126 0         0 $o->{brokenOrigins}->{$origin} = 1;
3127 0         0 return;
3128             }
3129              
3130 0 0       0 return $o->{index}->{$hashBytes} if exists $o->{index}->{$hashBytes};
3131              
3132             # Object file
3133 0         0 my $hashHex = unpack 'H*', $hashBytes;
3134 0         0 my $file = $o->{objectsFolder}.'/'.substr($hashHex, 0, 2).'/'.substr($hashHex, 2);
3135              
3136             # Size and existence
3137 0         0 my @s = stat $file;
3138 0 0       0 if (! scalar @s) {
3139 0         0 $o->{missingObjects}->{$hashBytes} = 1;
3140 0         0 $o->{brokenOrigins}->{$origin} = 1;
3141 0         0 return;
3142             }
3143 0         0 my $size = $s[7];
3144 0 0       0 return $o->{ui}->error('Unexpected: object ', $hashHex, ' has ', $size, ' bytes') if $size < 4;
3145              
3146             # Read header
3147 0         0 open O, '<', $file;
3148 0         0 read O, my $buffer, 4;
3149 0         0 my $links = unpack 'L>', $buffer;
3150 0 0       0 return $o->{ui}->error('Unexpected: object ', $hashHex, ' has ', $links, ' references') if $links > 160000;
3151 0 0       0 return $o->{ui}->error('Unexpected: object ', $hashHex, ' is too small for ', $links, ' references') if 4 + $links * 32 > $s[7];
3152 0         0 my $hashes = '';
3153 0 0       0 read O, $hashes, $links * 32 if $links > 0;
3154 0         0 close O;
3155              
3156 0 0       0 return $o->{ui}->error('Incomplete read: ', length $hashes, ' out of ', $links * 32, ' bytes received.') if length $hashes != $links * 32;
3157              
3158 0         0 my $record = CDS::Record->new($hashBytes);
3159 0         0 $record->addInteger($size);
3160 0         0 $record->add($hashes);
3161 0         0 return $o->{index}->{$hashBytes} = $record;
3162             }
3163              
3164             sub envelopeExpiration {
3165 0     0   0 my $o = shift;
3166 0         0 my $hashBytes = shift;
3167 0         0 my $origin = shift;
3168              
3169 0   0     0 my $entry = $o->index($hashBytes, $origin) // return 0;
3170 0 0       0 return $entry->nthChild(2)->asInteger if scalar $entry->children > 2;
3171              
3172             # Object file
3173 0         0 my $hashHex = unpack 'H*', $hashBytes;
3174 0         0 my $file = $o->{objectsFolder}.'/'.substr($hashHex, 0, 2).'/'.substr($hashHex, 2);
3175 0         0 my $record = CDS::Record->fromObject(CDS::Object->fromBytes(CDS->readBytesFromFile($file)));
3176 0         0 my $expires = $record->child('expires')->integerValue;
3177 0         0 $entry->addInteger($expires);
3178 0         0 return $expires;
3179             }
3180              
3181             sub startProgress {
3182 0     0   0 my $o = shift;
3183 0         0 my $title = shift;
3184              
3185 0         0 $o->{progress} = 0;
3186 0         0 $o->{progressTitle} = $title;
3187 0         0 $o->{ui}->progress($o->{progress}, ' ', $o->{progressTitle});
3188             }
3189              
3190             sub incrementProgress {
3191 0     0   0 my $o = shift;
3192              
3193 0         0 $o->{progress} += 1;
3194 0 0       0 return if $o->{progress} % 100;
3195 0         0 $o->{ui}->progress($o->{progress}, ' ', $o->{progressTitle});
3196             }
3197              
3198             sub lastModified {
3199 0     0   0 my $file = shift;
3200              
3201 0         0 my @s = stat $file;
3202 0 0       0 return scalar @s ? $s[9] : 0;
3203             }
3204              
3205             package CDS::Commands::CollectGarbage::Delete;
3206              
3207             sub new {
3208 0     0   0 my $class = shift;
3209 0         0 my $ui = shift;
3210              
3211 0         0 return bless {
3212             ui => $ui,
3213             deletedEnvelopesText => 'expired envelopes deleted',
3214             keptEnvelopesText => 'envelopes kept',
3215             deletedObjectsText => 'objects deleted',
3216             keptObjectsText => 'objects kept',
3217             };
3218             }
3219              
3220             sub initialize {
3221 0     0   0 my $o = shift;
3222 0         0 my $folder = shift;
3223 0         0 1 }
3224              
3225             sub startDeletion {
3226 0     0   0 my $o = shift;
3227              
3228 0         0 $o->{ui}->title('Deleting obsolete objects');
3229             }
3230              
3231             sub deleteEnvelope {
3232 0     0   0 my $o = shift;
3233 0         0 my $file = shift;
3234 0         0 $o->deleteObject($file) }
3235              
3236             sub deleteObject {
3237 0     0   0 my $o = shift;
3238 0         0 my $file = shift;
3239              
3240 0   0     0 unlink $file // return $o->{ui}->error('Unable to delete "', $file, '". Giving up …');
3241 0         0 return 1;
3242             }
3243              
3244             sub wrapUp {
3245 0     0   0 my $o = shift;
3246             }
3247              
3248             package CDS::Commands::CollectGarbage::Report;
3249              
3250             sub new {
3251 0     0   0 my $class = shift;
3252 0         0 my $ui = shift;
3253              
3254 0         0 return bless {
3255             ui => $ui,
3256             countReported => 0,
3257             deletedEnvelopesText => 'envelopes have expired',
3258             keptEnvelopesText => 'envelopes are in use',
3259             deletedObjectsText => 'objects can be deleted',
3260             keptObjectsText => 'objects are in use',
3261             };
3262             }
3263              
3264             sub initialize {
3265 0     0   0 my $o = shift;
3266 0         0 my $folderStore = shift;
3267              
3268 0         0 $o->{file} = $folderStore->folder.'/.garbage';
3269 0 0       0 open($o->{fh}, '>', $o->{file}) || return $o->{ui}->error('Failed to open ', $o->{file}, ' for writing.');
3270 0         0 return 1;
3271             }
3272              
3273             sub startDeletion {
3274 0     0   0 my $o = shift;
3275              
3276 0         0 $o->{ui}->title('Deleting obsolete objects');
3277             }
3278              
3279             sub deleteEnvelope {
3280 0     0   0 my $o = shift;
3281 0         0 my $file = shift;
3282 0         0 $o->deleteObject($file) }
3283              
3284             sub deleteObject {
3285 0     0   0 my $o = shift;
3286 0         0 my $file = shift;
3287              
3288 0         0 my $fh = $o->{fh};
3289 0         0 print $fh 'rm ', $file, "\n";
3290 0         0 $o->{countReported} += 1;
3291 0 0       0 print $fh 'echo ', $o->{countReported}, ' files deleted', "\n" if $o->{countReported} % 100 == 0;
3292 0         0 return 1;
3293             }
3294              
3295             sub wrapUp {
3296 0     0   0 my $o = shift;
3297              
3298 0         0 close $o->{fh};
3299 0 0       0 if ($o->{countReported} == 0) {
3300 0         0 unlink $o->{file};
3301             } else {
3302 0         0 $o->{ui}->space;
3303 0         0 $o->{ui}->p('The report was written to ', $o->{file}, '.');
3304 0         0 $o->{ui}->space;
3305             }
3306             }
3307              
3308             # BEGIN AUTOGENERATED
3309             package CDS::Commands::CreateKeyPair;
3310              
3311             sub register {
3312 0     0   0 my $class = shift;
3313 0         0 my $cds = shift;
3314 0         0 my $help = shift;
3315              
3316 0         0 my $node000 = CDS::Parser::Node->new(0);
3317 0         0 my $node001 = CDS::Parser::Node->new(0);
3318 0         0 my $node002 = CDS::Parser::Node->new(0);
3319 0         0 my $node003 = CDS::Parser::Node->new(0);
3320 0         0 my $node004 = CDS::Parser::Node->new(0);
3321 0         0 my $node005 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
3322 0         0 my $node006 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&createKeyPair});
3323 0         0 $cds->addArrow($node002, 1, 0, 'create');
3324 0         0 $help->addArrow($node000, 1, 0, 'create');
3325 0         0 $node000->addArrow($node001, 1, 0, 'key');
3326 0         0 $node001->addArrow($node005, 1, 0, 'pair');
3327 0         0 $node002->addArrow($node003, 1, 0, 'key');
3328 0         0 $node003->addArrow($node004, 1, 0, 'pair');
3329 0         0 $node004->addArrow($node006, 1, 0, 'FILENAME', \&collectFilename);
3330             }
3331              
3332             sub collectFilename {
3333 0     0   0 my $o = shift;
3334 0         0 my $label = shift;
3335 0         0 my $value = shift;
3336              
3337 0         0 $o->{filename} = $value;
3338             }
3339              
3340             sub new {
3341 0     0   0 my $class = shift;
3342 0         0 my $actor = shift;
3343 0         0 bless {actor => $actor, ui => $actor->ui} }
3344              
3345             # END AUTOGENERATED
3346              
3347             # HTML FOLDER NAME create-key-pair
3348             # HTML TITLE Create key pair
3349             sub help {
3350 0     0   0 my $o = shift;
3351 0         0 my $cmd = shift;
3352              
3353 0         0 my $ui = $o->{ui};
3354 0         0 $ui->space;
3355 0         0 $ui->command('cds create key pair FILENAME');
3356 0         0 $ui->p('Generates a key pair, and writes it to FILENAME.');
3357 0         0 $ui->space;
3358 0         0 $ui->title('Related commands');
3359 0         0 $ui->line(' cds select …');
3360 0         0 $ui->line(' cds use …');
3361 0         0 $ui->line(' cds entrust …');
3362 0         0 $ui->line(' cds drop …');
3363 0         0 $ui->space;
3364             }
3365              
3366             sub createKeyPair {
3367 0     0   0 my $o = shift;
3368 0         0 my $cmd = shift;
3369              
3370 0         0 $cmd->collect($o);
3371 0 0       0 return $o->{ui}->error('The file "', $o->{filename}, '" exists.') if -e $o->{filename};
3372 0         0 my $keyPair = CDS::KeyPair->generate;
3373 0   0     0 $keyPair->writeToFile($o->{filename}) // return $o->{ui}->error('Failed to write the key pair file "', $o->{filename}, '".');
3374 0         0 $o->{ui}->pGreen('Key pair "', $o->{filename}, '" created.');
3375             }
3376              
3377             # BEGIN AUTOGENERATED
3378             package CDS::Commands::Curl;
3379              
3380             sub register {
3381 0     0   0 my $class = shift;
3382 0         0 my $cds = shift;
3383 0         0 my $help = shift;
3384              
3385 0         0 my $node000 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
3386 0         0 my $node001 = CDS::Parser::Node->new(1);
3387 0         0 my $node002 = CDS::Parser::Node->new(0);
3388 0         0 my $node003 = CDS::Parser::Node->new(0);
3389 0         0 my $node004 = CDS::Parser::Node->new(0);
3390 0         0 my $node005 = CDS::Parser::Node->new(0);
3391 0         0 my $node006 = CDS::Parser::Node->new(0);
3392 0         0 my $node007 = CDS::Parser::Node->new(0);
3393 0         0 my $node008 = CDS::Parser::Node->new(0);
3394 0         0 my $node009 = CDS::Parser::Node->new(0);
3395 0         0 my $node010 = CDS::Parser::Node->new(0);
3396 0         0 my $node011 = CDS::Parser::Node->new(0);
3397 0         0 my $node012 = CDS::Parser::Node->new(0);
3398 0         0 my $node013 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&curlGet});
3399 0         0 my $node014 = CDS::Parser::Node->new(0);
3400 0         0 my $node015 = CDS::Parser::Node->new(0);
3401 0         0 my $node016 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&curlPut});
3402 0         0 my $node017 = CDS::Parser::Node->new(0);
3403 0         0 my $node018 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&curlBook});
3404 0         0 my $node019 = CDS::Parser::Node->new(0);
3405 0         0 my $node020 = CDS::Parser::Node->new(0);
3406 0         0 my $node021 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&curlList});
3407 0         0 my $node022 = CDS::Parser::Node->new(0);
3408 0         0 my $node023 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&curlGet});
3409 0         0 my $node024 = CDS::Parser::Node->new(0);
3410 0         0 my $node025 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&curlPut});
3411 0         0 my $node026 = CDS::Parser::Node->new(0);
3412 0         0 my $node027 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&curlBook});
3413 0         0 my $node028 = CDS::Parser::Node->new(0);
3414 0         0 my $node029 = CDS::Parser::Node->new(1);
3415 0         0 my $node030 = CDS::Parser::Node->new(0);
3416 0         0 my $node031 = CDS::Parser::Node->new(0);
3417 0         0 my $node032 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&curlList});
3418 0         0 my $node033 = CDS::Parser::Node->new(0);
3419 0         0 my $node034 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&curlGet});
3420 0         0 my $node035 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&curlPut});
3421 0         0 my $node036 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&curlBook});
3422 0         0 my $node037 = CDS::Parser::Node->new(1);
3423 0         0 my $node038 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&curlList});
3424 0         0 my $node039 = CDS::Parser::Node->new(0);
3425 0         0 my $node040 = CDS::Parser::Node->new(0);
3426 0         0 my $node041 = CDS::Parser::Node->new(0);
3427 0         0 my $node042 = CDS::Parser::Node->new(0);
3428 0         0 my $node043 = CDS::Parser::Node->new(0);
3429 0         0 my $node044 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&curlList});
3430 0         0 my $node045 = CDS::Parser::Node->new(1);
3431 0         0 my $node046 = CDS::Parser::Node->new(0);
3432 0         0 my $node047 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&curlModify});
3433 0         0 my $node048 = CDS::Parser::Node->new(0);
3434 0         0 my $node049 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&curlModify});
3435 0         0 my $node050 = CDS::Parser::Node->new(0);
3436 0         0 my $node051 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&curlModify});
3437 0         0 $cds->addArrow($node001, 1, 0, 'curl');
3438 0         0 $help->addArrow($node000, 1, 0, 'curl');
3439 0         0 $node001->addArrow($node002, 1, 0, 'get');
3440 0         0 $node001->addArrow($node003, 1, 0, 'put');
3441 0         0 $node001->addArrow($node004, 1, 0, 'book');
3442 0         0 $node001->addArrow($node005, 1, 0, 'get');
3443 0         0 $node001->addArrow($node006, 1, 0, 'book');
3444 0         0 $node001->addArrow($node007, 1, 0, 'list');
3445 0         0 $node001->addArrow($node007, 1, 0, 'watch', \&collectWatch);
3446 0         0 $node001->addDefault($node011);
3447 0         0 $node002->addArrow($node013, 1, 0, 'HASH', \&collectHash);
3448 0         0 $node003->addArrow($node016, 1, 0, 'FILE', \&collectFile);
3449 0         0 $node004->addArrow($node018, 1, 0, 'HASH', \&collectHash);
3450 0         0 $node005->addArrow($node023, 1, 0, 'OBJECT', \&collectObject);
3451 0         0 $node006->addArrow($node027, 1, 0, 'OBJECT', \&collectObject);
3452 0         0 $node007->addArrow($node008, 1, 0, 'message');
3453 0         0 $node007->addArrow($node009, 1, 0, 'private');
3454 0         0 $node007->addArrow($node010, 1, 0, 'public');
3455 0         0 $node007->addArrow($node021, 0, 0, 'messages', \&collectMessages);
3456 0         0 $node007->addArrow($node021, 0, 0, 'private', \&collectPrivate);
3457 0         0 $node007->addArrow($node021, 0, 0, 'public', \&collectPublic);
3458 0         0 $node008->addArrow($node021, 1, 0, 'box', \&collectMessages);
3459 0         0 $node009->addArrow($node021, 1, 0, 'box', \&collectPrivate);
3460 0         0 $node010->addArrow($node021, 1, 0, 'box', \&collectPublic);
3461 0         0 $node011->addArrow($node012, 1, 0, 'remove');
3462 0         0 $node011->addArrow($node020, 1, 0, 'add');
3463 0         0 $node012->addArrow($node012, 1, 0, 'HASH', \&collectHash1);
3464 0         0 $node012->addArrow($node037, 1, 0, 'HASH', \&collectHash1);
3465 0         0 $node013->addArrow($node014, 1, 0, 'from');
3466 0         0 $node013->addArrow($node015, 0, 0, 'on');
3467 0         0 $node013->addDefault($node023);
3468 0         0 $node014->addArrow($node023, 1, 0, 'STORE', \&collectStore);
3469 0         0 $node015->addArrow($node023, 0, 0, 'STORE', \&collectStore);
3470 0         0 $node016->addArrow($node017, 1, 0, 'onto');
3471 0         0 $node016->addDefault($node025);
3472 0         0 $node017->addArrow($node025, 1, 0, 'STORE', \&collectStore);
3473 0         0 $node018->addArrow($node019, 1, 0, 'on');
3474 0         0 $node018->addDefault($node027);
3475 0         0 $node019->addArrow($node027, 1, 0, 'STORE', \&collectStore);
3476 0         0 $node020->addArrow($node029, 1, 0, 'FILE', \&collectFile1);
3477 0         0 $node020->addArrow($node029, 1, 0, 'HASH', \&collectHash2);
3478 0         0 $node021->addArrow($node022, 1, 0, 'of');
3479 0         0 $node022->addArrow($node032, 1, 0, 'ACTOR', \&collectActor);
3480 0         0 $node023->addArrow($node024, 1, 0, 'using');
3481 0         0 $node024->addArrow($node034, 1, 0, 'KEYPAIR', \&collectKeypair);
3482 0         0 $node025->addArrow($node026, 1, 0, 'using');
3483 0         0 $node026->addArrow($node035, 1, 0, 'KEYPAIR', \&collectKeypair);
3484 0         0 $node027->addArrow($node028, 1, 0, 'using');
3485 0         0 $node028->addArrow($node036, 1, 0, 'KEYPAIR', \&collectKeypair);
3486 0         0 $node029->addDefault($node020);
3487 0         0 $node029->addArrow($node030, 1, 0, 'and');
3488 0         0 $node029->addArrow($node040, 1, 0, 'to');
3489 0         0 $node030->addArrow($node031, 1, 0, 'remove');
3490 0         0 $node031->addArrow($node031, 1, 0, 'HASH', \&collectHash1);
3491 0         0 $node031->addArrow($node037, 1, 0, 'HASH', \&collectHash1);
3492 0         0 $node032->addArrow($node033, 1, 0, 'on');
3493 0         0 $node033->addArrow($node038, 1, 0, 'STORE', \&collectStore);
3494 0         0 $node037->addArrow($node040, 1, 0, 'from');
3495 0         0 $node038->addArrow($node039, 1, 0, 'using');
3496 0         0 $node039->addArrow($node044, 1, 0, 'KEYPAIR', \&collectKeypair);
3497 0         0 $node040->addArrow($node041, 1, 0, 'message');
3498 0         0 $node040->addArrow($node042, 1, 0, 'private');
3499 0         0 $node040->addArrow($node043, 1, 0, 'public');
3500 0         0 $node040->addArrow($node045, 0, 0, 'messages', \&collectMessages1);
3501 0         0 $node040->addArrow($node045, 0, 0, 'private', \&collectPrivate1);
3502 0         0 $node040->addArrow($node045, 0, 0, 'public', \&collectPublic1);
3503 0         0 $node041->addArrow($node045, 1, 0, 'box', \&collectMessages1);
3504 0         0 $node042->addArrow($node045, 1, 0, 'box', \&collectPrivate1);
3505 0         0 $node043->addArrow($node045, 1, 0, 'box', \&collectPublic1);
3506 0         0 $node045->addArrow($node046, 1, 0, 'of');
3507 0         0 $node045->addDefault($node047);
3508 0         0 $node046->addArrow($node047, 1, 0, 'ACTOR', \&collectActor1);
3509 0         0 $node047->addArrow($node011, 1, 0, 'and', \&collectAnd);
3510 0         0 $node047->addArrow($node048, 1, 0, 'on');
3511 0         0 $node048->addArrow($node049, 1, 0, 'STORE', \&collectStore);
3512 0         0 $node049->addArrow($node050, 1, 0, 'using');
3513 0         0 $node050->addArrow($node051, 1, 0, 'KEYPAIR', \&collectKeypair);
3514             }
3515              
3516             sub collectActor {
3517 0     0   0 my $o = shift;
3518 0         0 my $label = shift;
3519 0         0 my $value = shift;
3520              
3521 0         0 $o->{actorHash} = $value;
3522             }
3523              
3524             sub collectActor1 {
3525 0     0   0 my $o = shift;
3526 0         0 my $label = shift;
3527 0         0 my $value = shift;
3528              
3529 0         0 $o->{currentBatch}->{actorHash} = $value;
3530             }
3531              
3532             sub collectAnd {
3533 0     0   0 my $o = shift;
3534 0         0 my $label = shift;
3535 0         0 my $value = shift;
3536              
3537 0         0 push @{$o->{batches}}, $o->{currentBatch};
  0         0  
3538             $o->{currentBatch} = {
3539 0         0 addHashes => [],
3540             addEnvelopes => [],
3541             removeHashes => []
3542             };
3543             }
3544              
3545             sub collectFile {
3546 0     0   0 my $o = shift;
3547 0         0 my $label = shift;
3548 0         0 my $value = shift;
3549              
3550 0         0 $o->{file} = $value;
3551             }
3552              
3553             sub collectFile1 {
3554 0     0   0 my $o = shift;
3555 0         0 my $label = shift;
3556 0         0 my $value = shift;
3557              
3558 0         0 push @{$o->{currentBatch}->{addFiles}}, $value;
  0         0  
3559             }
3560              
3561             sub collectHash {
3562 0     0   0 my $o = shift;
3563 0         0 my $label = shift;
3564 0         0 my $value = shift;
3565              
3566 0         0 $o->{hash} = $value;
3567             }
3568              
3569             sub collectHash1 {
3570 0     0   0 my $o = shift;
3571 0         0 my $label = shift;
3572 0         0 my $value = shift;
3573              
3574 0         0 push @{$o->{currentBatch}->{removeHashes}}, $value;
  0         0  
3575             }
3576              
3577             sub collectHash2 {
3578 0     0   0 my $o = shift;
3579 0         0 my $label = shift;
3580 0         0 my $value = shift;
3581              
3582 0         0 push @{$o->{currentBatch}->{addHashes}}, $value;
  0         0  
3583             }
3584              
3585             sub collectKeypair {
3586 0     0   0 my $o = shift;
3587 0         0 my $label = shift;
3588 0         0 my $value = shift;
3589              
3590 0         0 $o->{keyPairToken} = $value;
3591             }
3592              
3593             sub collectMessages {
3594 0     0   0 my $o = shift;
3595 0         0 my $label = shift;
3596 0         0 my $value = shift;
3597              
3598 0         0 $o->{boxLabel} = 'messages';
3599             }
3600              
3601             sub collectMessages1 {
3602 0     0   0 my $o = shift;
3603 0         0 my $label = shift;
3604 0         0 my $value = shift;
3605              
3606 0         0 $o->{currentBatch}->{boxLabel} = 'messages';
3607             }
3608              
3609             sub collectObject {
3610 0     0   0 my $o = shift;
3611 0         0 my $label = shift;
3612 0         0 my $value = shift;
3613              
3614 0         0 $o->{hash} = $value->hash;
3615 0         0 $o->{store} = $value->cliStore;
3616             }
3617              
3618             sub collectPrivate {
3619 0     0   0 my $o = shift;
3620 0         0 my $label = shift;
3621 0         0 my $value = shift;
3622              
3623 0         0 $o->{boxLabel} = 'private';
3624             }
3625              
3626             sub collectPrivate1 {
3627 0     0   0 my $o = shift;
3628 0         0 my $label = shift;
3629 0         0 my $value = shift;
3630              
3631 0         0 $o->{currentBatch}->{boxLabel} = 'private';
3632             }
3633              
3634             sub collectPublic {
3635 0     0   0 my $o = shift;
3636 0         0 my $label = shift;
3637 0         0 my $value = shift;
3638              
3639 0         0 $o->{boxLabel} = 'public';
3640             }
3641              
3642             sub collectPublic1 {
3643 0     0   0 my $o = shift;
3644 0         0 my $label = shift;
3645 0         0 my $value = shift;
3646              
3647 0         0 $o->{currentBatch}->{boxLabel} = 'public';
3648             }
3649              
3650             sub collectStore {
3651 0     0   0 my $o = shift;
3652 0         0 my $label = shift;
3653 0         0 my $value = shift;
3654              
3655 0         0 $o->{store} = $value;
3656             }
3657              
3658             sub collectWatch {
3659 0     0   0 my $o = shift;
3660 0         0 my $label = shift;
3661 0         0 my $value = shift;
3662              
3663 0         0 $o->{watchTimeout} = 60000;
3664             }
3665              
3666             sub new {
3667 0     0   0 my $class = shift;
3668 0         0 my $actor = shift;
3669 0         0 bless {actor => $actor, ui => $actor->ui} }
3670              
3671             # END AUTOGENERATED
3672              
3673             # HTML FOLDER NAME curl
3674             # HTML TITLE Curl
3675             sub help {
3676 0     0   0 my $o = shift;
3677 0         0 my $cmd = shift;
3678              
3679 0         0 my $ui = $o->{ui};
3680 0         0 $ui->space;
3681 0         0 $ui->p($ui->blue('cds curl'), ' prepares and executes a CURL command line for a HTTP store request. This is helpful for debugging a HTTP store implementation. Outside of low-level debugging, it is more convenient to use the "cds get|put|list|add|remove …" commands, which are richer in functionality, and work on all stores.');
3682 0         0 $ui->space;
3683 0         0 $ui->command('cds curl get OBJECT');
3684 0         0 $ui->command('cds curl get HASH [from|on STORE]');
3685 0         0 $ui->p('Downloads an object with a GET request on an object store.');
3686 0         0 $ui->space;
3687 0         0 $ui->command('cds curl put FILE [onto STORE]');
3688 0         0 $ui->p('Uploads an object with a PUT request on an object store.');
3689 0         0 $ui->space;
3690 0         0 $ui->command('cds curl book OBJECT');
3691 0         0 $ui->command('cds curl book HASH [on STORE]');
3692 0         0 $ui->p('Books an object with a POST request on an object store.');
3693 0         0 $ui->space;
3694 0         0 $ui->command('cds curl list message box of ACTOR [on STORE]');
3695 0         0 $ui->command('cds curl list private box of ACTOR [on STORE]');
3696 0         0 $ui->command('cds curl list public box of ACTOR [on STORE]');
3697 0         0 $ui->p('Lists the indicated box with a GET request on an account store.');
3698 0         0 $ui->space;
3699 0         0 $ui->command('cds curl watch message box of ACTOR [on STORE]');
3700 0         0 $ui->command('cds curl watch private box of ACTOR [on STORE]');
3701 0         0 $ui->command('cds curl watch public box of ACTOR [on STORE]');
3702 0         0 $ui->p('As above, but with a watch timeout of 60 second.');
3703 0         0 $ui->space;
3704 0         0 $ui->command('cds curl add (FILE|HASH)* to (message|private|public) box of ACTOR [and …] [on STORE]');
3705 0         0 $ui->command('cds curl remove HASH* from (message|private|public) box of ACTOR [and …] [on STORE]');
3706 0         0 $ui->p('Modifies the indicated boxes with a POST request on an account store. Multiple modifications to different boxes may be chained using "and". All modifications are submitted using a single request, which is optionally signed (see below).');
3707 0         0 $ui->space;
3708 0         0 $ui->command('… using KEYPAIR');
3709 0         0 $ui->p('Signs the request using KEYPAIR instead of the actor\'s key pair. The store may or may not verify the signature.');
3710 0         0 $ui->p('For debugging purposes, information about the signature is stored as ".cds-curl-bytes-to-sign", ".cds-curl-hash-to-sign", and ".cds-curl-signature" in the current folder. Note that signatures are valid for 1-2 minutes only. After that, servers will reject them to guard against replay attacks.');
3711 0         0 $ui->space;
3712             }
3713              
3714             sub curlGet {
3715 0     0   0 my $o = shift;
3716 0         0 my $cmd = shift;
3717              
3718 0         0 $cmd->collect($o);
3719 0 0       0 $o->{keyPairToken} = $o->{actor}->preferredKeyPairToken if ! $o->{keyPairToken};
3720 0 0       0 $o->{store} = $o->{actor}->preferredStore if ! $o->{store};
3721              
3722 0         0 my $objectToken = CDS::ObjectToken->new($o->{store}, $o->{hash});
3723 0         0 $o->curlRequest('GET', $objectToken->url, ['--output', $o->{hash}->hex]);
3724             }
3725              
3726             sub curlPut {
3727 0     0   0 my $o = shift;
3728 0         0 my $cmd = shift;
3729              
3730 0         0 $cmd->collect($o);
3731 0 0       0 $o->{keyPairToken} = $o->{actor}->preferredKeyPairToken if ! $o->{keyPairToken};
3732 0 0       0 $o->{store} = $o->{actor}->preferredStore if ! $o->{store};
3733              
3734 0   0     0 my $bytes = CDS->readBytesFromFile($o->{file}) // return $o->{ui}->error('Unable to read "', $o->{file}, '".');
3735 0         0 my $hash = CDS::Hash->calculateFor($bytes);
3736 0         0 my $objectToken = CDS::ObjectToken->new($o->{store}, $hash);
3737 0         0 $o->curlRequest('PUT', $objectToken->url, ['--data-binary', '@'.$o->{file}, '-H', 'Content-Type: application/condensation-object']);
3738             }
3739              
3740             sub curlBook {
3741 0     0   0 my $o = shift;
3742 0         0 my $cmd = shift;
3743              
3744 0         0 $cmd->collect($o);
3745 0 0       0 $o->{keyPairToken} = $o->{actor}->preferredKeyPairToken if ! $o->{keyPairToken};
3746 0 0       0 $o->{store} = $o->{actor}->preferredStore if ! $o->{store};
3747              
3748 0         0 my $objectToken = CDS::ObjectToken->new($o->{store}, $o->{hash});
3749 0         0 $o->curlRequest('POST', $objectToken->url, []);
3750             }
3751              
3752             sub curlList {
3753 0     0   0 my $o = shift;
3754 0         0 my $cmd = shift;
3755              
3756 0         0 $cmd->collect($o);
3757 0 0       0 $o->{keyPairToken} = $o->{actor}->preferredKeyPairToken if ! $o->{keyPairToken};
3758 0 0       0 $o->{store} = $o->{actor}->preferredStore if ! $o->{store};
3759 0 0       0 $o->{actorHash} = $o->{actor}->preferredActorHash if ! $o->{actorHash};
3760              
3761 0         0 my $boxToken = CDS::BoxToken->new(CDS::AccountToken->new($o->{store}, $o->{actorHash}), $o->{boxLabel});
3762 0         0 my $args = ['--output', '.cds-curl-list'];
3763 0 0       0 push @$args, '-H', 'Condensation-Watch: '.$o->{watchTimeout}.' ms' if $o->{watchTimeout};
3764 0         0 $o->curlRequest('GET', $boxToken->url, $args);
3765             }
3766              
3767             sub curlModify {
3768 0     0   0 my $o = shift;
3769 0         0 my $cmd = shift;
3770              
3771             $o->{currentBatch} = {
3772 0         0 addHashes => [],
3773             addEnvelopes => [],
3774             removeHashes => [],
3775             };
3776 0         0 $o->{batches} = [];
3777 0         0 $cmd->collect($o);
3778 0 0       0 $o->{keyPairToken} = $o->{actor}->preferredKeyPairToken if ! $o->{keyPairToken};
3779 0 0       0 $o->{store} = $o->{actor}->preferredStore if ! $o->{store};
3780              
3781             # Prepare the modifications
3782 0         0 my $modifications = CDS::StoreModifications->new;
3783              
3784 0         0 for my $batch (@{$o->{batches}}, $o->{currentBatch}) {
  0         0  
3785 0 0       0 $batch->{actorHash} = $o->{actor}->preferredActorHash if ! $batch->{actorHash};
3786              
3787 0         0 for my $hash (@{$batch->{addHashes}}) {
  0         0  
3788 0         0 $modifications->add($batch->{actorHash}, $batch->{boxLabel}, $hash);
3789             }
3790              
3791 0         0 for my $file (@{$batch->{addFiles}}) {
  0         0  
3792 0   0     0 my $bytes = CDS->readBytesFromFile($file) // return $o->{ui}->error('Unable to read "', $file, '".');
3793 0   0     0 my $object = CDS::Object->fromBytes($bytes) // return $o->{ui}->error('"', $file, '" is not a Condensation object.');
3794 0         0 my $hash = $object->calculateHash;
3795 0 0       0 $o->{ui}->warning('"', $file, '" is not a valid envelope. The server may reject it.') if ! $o->{actor}->isEnvelope($object);
3796 0         0 $modifications->add($batch->{actorHash}, $batch->{boxLabel}, $hash, $object);
3797             }
3798              
3799 0         0 for my $hash (@{$batch->{removeHashes}}) {
  0         0  
3800 0         0 $modifications->remove($batch->{actorHash}, $batch->{boxLabel}, $hash);
3801             }
3802             }
3803              
3804 0 0       0 $o->{ui}->warning('You didn\'t specify any changes. The server should accept, but ignore this.') if $modifications->isEmpty;
3805              
3806             # Write a new file
3807 0         0 my $modificationsObject = $modifications->toRecord->toObject;
3808 0         0 my $modificationsHash = $modificationsObject->calculateHash;
3809 0         0 my $file = '.cds-curl-modifications-'.substr($modificationsHash->hex, 0, 8);
3810 0   0     0 CDS->writeBytesToFile($file, $modificationsObject->header, $modificationsObject->data) // return $o->{ui}->error('Unable to write modifications to "', $file, '".');
3811 0         0 $o->{ui}->line(scalar @{$modifications->additions}, ' addition(s) and ', scalar @{$modifications->removals}, ' removal(s) written to "', $file, '".');
  0         0  
  0         0  
3812              
3813             # Submit
3814 0         0 $o->curlRequest('POST', $o->{store}->url.'/accounts', ['--data-binary', '@'.$file, '-H', 'Content-Type: application/condensation-modifications'], $modificationsObject);
3815             }
3816              
3817             sub curlRequest {
3818 0     0   0 my $o = shift;
3819 0         0 my $method = shift;
3820 0         0 my $url = shift;
3821 0         0 my $curlArgs = shift;
3822 0         0 my $contentObjectToSign = shift;
3823              
3824             # Parse the URL
3825 0 0       0 $url =~ /^(https?):\/\/([^\/]+)(\/.*|)$/i || return $o->{ui}->error('"', $url, '" does not look like a valid and complete http://… or https://… URL.');
3826 0         0 my $protocol = lc($1);
3827 0         0 my $host = $2;
3828 0         0 my $path = $3;
3829              
3830             # Strip off user and password, if any
3831 0         0 my $credentials;
3832 0 0       0 if ($host =~ /^(.*)\@([^\@]*)$/) {
3833 0         0 $credentials = $1;
3834 0         0 $host = lc($2);
3835             } else {
3836 0         0 $host = lc($host);
3837             }
3838              
3839             # Remove default port
3840 0 0       0 if ($host =~ /^(.*):(\d+)$/) {
3841 0 0 0     0 $host = $1 if $protocol eq 'http' && $2 == 80;
3842 0 0 0     0 $host = $1 if $protocol eq 'https' && $2 == 443;
3843             }
3844              
3845             # Checks the path and warn the user if obvious things are likely to go wrong
3846 0 0       0 $o->{ui}->warning('Warning: "//" in URL may not work') if $path =~ /\/\//;
3847 0 0       0 $o->{ui}->warning('Warning: /./ or /../ in URL may not work') if $path =~ /\/\.+\//;
3848 0 0       0 $o->{ui}->warning('Warning: /. or /.. at the end of the URL may not work') if $path =~ /\/\.+$/;
3849              
3850             # Signature
3851              
3852             # Date
3853 0         0 my $dateString = CDS::ISODate->millisecondString(CDS->now);
3854              
3855             # Text to sign
3856 0         0 my $bytesToSign = $dateString."\0".uc($method)."\0".$host.$path;
3857 0 0       0 $bytesToSign .= "\0".$contentObjectToSign->header.$contentObjectToSign->data if defined $contentObjectToSign;
3858              
3859             # Signature
3860 0         0 my $keyPair = $o->{keyPairToken}->keyPair;
3861 0         0 my $hashToSign = CDS::Hash->calculateFor($bytesToSign);
3862 0         0 my $signature = $keyPair->signHash($hashToSign);
3863 0         0 push @$curlArgs, '-H', 'Condensation-Date: '.$dateString;
3864 0         0 push @$curlArgs, '-H', 'Condensation-Actor: '.$keyPair->publicKey->hash->hex;
3865 0         0 push @$curlArgs, '-H', 'Condensation-Signature: '.unpack('H*', $signature);
3866              
3867             # Write signature information to files
3868 0 0       0 CDS->writeBytesToFile('.cds-curl-bytesToSign', $bytesToSign) || $o->{ui}->warning('Unable to write the bytes to sign to ".cds-curl-bytesToSign".');
3869 0 0       0 CDS->writeBytesToFile('.cds-curl-hashToSign', $hashToSign->bytes) || $o->{ui}->warning('Unable to write the hash to sign to ".cds-curl-hashToSign".');
3870 0 0       0 CDS->writeBytesToFile('.cds-curl-signature', $signature) || $o->{ui}->warning('Unable to write signature to ".cds-curl-signature".');
3871              
3872             # Method
3873 0 0       0 unshift @$curlArgs, '-X', $method if $method ne 'GET';
3874 0         0 unshift @$curlArgs, '-#', '--dump-header', '-';
3875              
3876             # Print
3877 0 0 0     0 $o->{ui}->line($o->{ui}->gold('curl', join('', map { ($_ ne '-X' && $_ ne '-' && $_ ne '--dump-header' && $_ ne '-#' && substr($_, 0, 1) eq '-' ? " \\\n " : ' ').&withQuotesIfNecessary($_) } @$curlArgs), scalar @$curlArgs ? " \\\n " : ' ', &withQuotesIfNecessary($url)));
  0 0       0  
3878              
3879             # Execute
3880 0         0 system('curl', @$curlArgs, $url);
3881             }
3882              
3883             sub withQuotesIfNecessary {
3884 0     0   0 my $text = shift;
3885              
3886 0 0       0 return $text =~ /[^a-zA-Z0-9\.\/\@:,_-]/ ? '\''.$text.'\'' : $text;
3887             }
3888              
3889             # BEGIN AUTOGENERATED
3890             package CDS::Commands::DiscoverActorGroup;
3891              
3892             sub register {
3893 0     0   0 my $class = shift;
3894 0         0 my $cds = shift;
3895 0         0 my $help = shift;
3896              
3897 0         0 my $node000 = CDS::Parser::Node->new(0);
3898 0         0 my $node001 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
3899 0         0 my $node002 = CDS::Parser::Node->new(1);
3900 0         0 my $node003 = CDS::Parser::Node->new(0);
3901 0         0 my $node004 = CDS::Parser::Node->new(0);
3902 0         0 my $node005 = CDS::Parser::Node->new(0);
3903 0         0 my $node006 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&showActorGroupCmd});
3904 0         0 my $node007 = CDS::Parser::Node->new(0);
3905 0         0 my $node008 = CDS::Parser::Node->new(0);
3906 0         0 my $node009 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&discover});
3907 0         0 my $node010 = CDS::Parser::Node->new(0);
3908 0         0 my $node011 = CDS::Parser::Node->new(0);
3909 0         0 my $node012 = CDS::Parser::Node->new(0);
3910 0         0 my $node013 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&discover});
3911 0         0 $cds->addArrow($node000, 1, 0, 'show');
3912 0         0 $cds->addArrow($node002, 1, 0, 'discover');
3913 0         0 $help->addArrow($node001, 1, 0, 'discover');
3914 0         0 $help->addArrow($node001, 1, 0, 'rediscover');
3915 0         0 $node000->addArrow($node006, 1, 0, 'ACTORGROUP', \&collectActorgroup);
3916 0         0 $node002->addDefault($node003);
3917 0         0 $node002->addDefault($node004);
3918 0         0 $node002->addDefault($node005);
3919 0         0 $node002->addArrow($node009, 1, 0, 'me', \&collectMe);
3920 0         0 $node002->addArrow($node013, 1, 0, 'ACTORGROUP', \&collectActorgroup1);
3921 0         0 $node003->addArrow($node003, 1, 0, 'ACCOUNT', \&collectAccount);
3922 0         0 $node003->addArrow($node009, 1, 1, 'ACCOUNT', \&collectAccount);
3923 0         0 $node004->addArrow($node004, 1, 0, 'KEYPAIR', \&collectKeypair);
3924 0         0 $node004->addArrow($node007, 1, 0, 'KEYPAIR', \&collectKeypair);
3925 0         0 $node005->addArrow($node005, 1, 0, 'ACTOR', \&collectActor);
3926 0         0 $node005->addArrow($node007, 1, 0, 'ACTOR', \&collectActor);
3927 0         0 $node007->addArrow($node008, 1, 0, 'on');
3928 0         0 $node007->addDefault($node009);
3929 0         0 $node008->addArrow($node009, 1, 0, 'STORE', \&collectStore);
3930 0         0 $node009->addArrow($node010, 1, 0, 'and');
3931 0         0 $node010->addArrow($node011, 1, 0, 'remember');
3932 0         0 $node011->addArrow($node012, 1, 0, 'as');
3933 0         0 $node012->addArrow($node013, 1, 0, 'TEXT', \&collectText);
3934             }
3935              
3936             sub collectAccount {
3937 0     0   0 my $o = shift;
3938 0         0 my $label = shift;
3939 0         0 my $value = shift;
3940              
3941 0         0 push @{$o->{accounts}}, $value;
  0         0  
3942             }
3943              
3944             sub collectActor {
3945 0     0   0 my $o = shift;
3946 0         0 my $label = shift;
3947 0         0 my $value = shift;
3948              
3949 0         0 push @{$o->{actorHashes}}, $value;
  0         0  
3950             }
3951              
3952             sub collectActorgroup {
3953 0     0   0 my $o = shift;
3954 0         0 my $label = shift;
3955 0         0 my $value = shift;
3956              
3957 0         0 $o->{actorGroupToken} = $value;
3958             }
3959              
3960             sub collectActorgroup1 {
3961 0     0   0 my $o = shift;
3962 0         0 my $label = shift;
3963 0         0 my $value = shift;
3964              
3965 0         0 $o->{actorGroupToken} = $value;
3966 0         0 $o->{label} = $value->label;
3967             }
3968              
3969             sub collectKeypair {
3970 0     0   0 my $o = shift;
3971 0         0 my $label = shift;
3972 0         0 my $value = shift;
3973              
3974 0         0 push @{$o->{actorHashes}}, $value->keyPair->publicKey->hash;
  0         0  
3975             }
3976              
3977             sub collectMe {
3978 0     0   0 my $o = shift;
3979 0         0 my $label = shift;
3980 0         0 my $value = shift;
3981              
3982 0         0 $o->{me} = 1;
3983             }
3984              
3985             sub collectStore {
3986 0     0   0 my $o = shift;
3987 0         0 my $label = shift;
3988 0         0 my $value = shift;
3989              
3990 0         0 $o->{store} = $value;
3991             }
3992              
3993             sub collectText {
3994 0     0   0 my $o = shift;
3995 0         0 my $label = shift;
3996 0         0 my $value = shift;
3997              
3998 0         0 $o->{label} = $value;
3999             }
4000              
4001             sub new {
4002 0     0   0 my $class = shift;
4003 0         0 my $actor = shift;
4004 0         0 bless {actor => $actor, ui => $actor->ui} }
4005              
4006             # END AUTOGENERATED
4007              
4008             # HTML FOLDER NAME discover
4009             # HTML TITLE Discover actor groups
4010             sub help {
4011 0     0   0 my $o = shift;
4012 0         0 my $cmd = shift;
4013              
4014 0         0 my $ui = $o->{ui};
4015 0         0 $ui->space;
4016 0         0 $ui->command('cds discover ACCOUNT');
4017 0         0 $ui->command('cds discover ACTOR [on STORE]');
4018 0         0 $ui->p('Discovers the actor group the given account belongs to. Only active group members are discovered.');
4019 0         0 $ui->space;
4020 0         0 $ui->command('cds discover ACCOUNT*');
4021 0         0 $ui->command('cds discover ACTOR* on STORE');
4022 0         0 $ui->p('Same as above, but starts discovery with multiple accounts. All accounts must belong to the same actor group.');
4023 0         0 $ui->p('Note that this rarely makes sense. The actor group discovery algorithm reliably discovers an actor group from a single account.');
4024 0         0 $ui->space;
4025 0         0 $ui->command('cds discover me');
4026 0         0 $ui->p('Discovers your own actor group.');
4027 0         0 $ui->space;
4028 0         0 $ui->command('… and remember as TEXT');
4029 0         0 $ui->p('The discovered actor group is remembered as TEXT. See "cds help remember" for details.');
4030 0         0 $ui->space;
4031 0         0 $ui->command('cds discover ACTORGROUP');
4032 0         0 $ui->p('Updates a previously remembered actor group.');
4033 0         0 $ui->space;
4034 0         0 $ui->command('cds show ACTORGROUP');
4035 0         0 $ui->p('Shows a previously discovered and remembered actor group.');
4036 0         0 $ui->space;
4037             }
4038              
4039             sub discover {
4040 0     0   0 my $o = shift;
4041 0         0 my $cmd = shift;
4042              
4043 0         0 $o->{accounts} = [];
4044 0         0 $o->{actorHashes} = [];
4045 0         0 $cmd->collect($o);
4046              
4047             # Discover
4048 0         0 my $builder = $o->prepareBuilder;
4049 0         0 my ($actorGroup, $cards, $nodes) = $builder->discover($o->{actor}->keyPair, $o);
4050              
4051             # Show the graph
4052 0         0 $o->{ui}->space;
4053 0         0 $o->{ui}->title('Graph');
4054 0         0 for my $node (@$nodes) {
4055 0 0       0 my $status = $node->status eq 'active' ? $o->{ui}->green('active ') : $o->{ui}->gray('idle ');
4056 0         0 $o->{ui}->line($o->{ui}->blue($node->actorHash->hex), ' on ', $node->storeUrl, ' ', $status, $o->{ui}->gray($o->{ui}->niceDateTime($node->revision)));
4057 0         0 $o->{ui}->pushIndent;
4058 0         0 for my $link ($node->links) {
4059 0         0 my $isMostRecentInformation = $link->revision == $link->node->revision;
4060 0 0       0 my $color = $isMostRecentInformation ? 246 : 250;
4061 0         0 $o->{ui}->line($link->node->actorHash->shortHex, ' on ', $link->node->storeUrl, ' ', $o->{ui}->foreground($color, $o->{ui}->left(8, $link->status), $o->{ui}->niceDateTime($link->revision)));
4062             }
4063 0         0 $o->{ui}->popIndent;
4064             }
4065              
4066             # Show all accounts
4067 0         0 $o->showActorGroup($actorGroup);
4068              
4069             # Show all cards
4070 0         0 $o->{ui}->space;
4071 0         0 $o->{ui}->title('Cards');
4072 0         0 for my $card (@$cards) {
4073 0         0 $o->{ui}->line($o->{ui}->gold('cds show record ', $card->cardHash->hex, ' on ', $card->storeUrl));
4074             }
4075              
4076             # Remember the actor group if desired
4077 0 0       0 if ($o->{label}) {
4078 0         0 my $selector = $o->{actor}->labelSelector($o->{label});
4079              
4080 0         0 my $record = CDS::Record->new;
4081 0         0 my $actorGroupRecord = $record->add('actor group');
4082 0         0 $actorGroupRecord->add('discovered')->addInteger(CDS->now);
4083 0         0 $actorGroupRecord->addRecord($actorGroup->toBuilder->toRecord(1)->children);
4084 0         0 $selector->set($record);
4085              
4086 0         0 for my $publicKey ($actorGroup->publicKeys) {
4087 0         0 $selector->addObject($publicKey->hash, $publicKey->object);
4088             }
4089              
4090 0   0     0 $o->{actor}->saveOrShowError // return;
4091             }
4092              
4093 0         0 $o->{ui}->space;
4094             }
4095              
4096             sub prepareBuilder {
4097 0     0   0 my $o = shift;
4098              
4099             # Actor group
4100 0 0       0 return $o->{actorGroupToken}->actorGroup->toBuilder if $o->{actorGroupToken};
4101              
4102             # Other than actor group
4103 0         0 my $builder = CDS::ActorGroupBuilder->new;
4104 0         0 $builder->addKnownPublicKey($o->{actor}->keyPair->publicKey);
4105              
4106             # Me
4107 0 0       0 $builder->addMember($o->{actor}->messagingStoreUrl, $o->{actor}->keyPair->publicKey->hash) if $o->{me};
4108              
4109             # Accounts
4110 0         0 for my $account (@{$o->{accounts}}) {
  0         0  
4111 0         0 $builder->addMember($account->cliStore->url, $account->actorHash);
4112             }
4113              
4114             # Actors on store
4115 0 0       0 if (scalar @{$o->{actorHashes}}) {
  0         0  
4116 0   0     0 my $store = $o->{store} // $o->{actor}->preferredStore;
4117 0         0 for my $actorHash (@{$o->{actorHashes}}) {
  0         0  
4118 0         0 $builder->addMember($actorHash, $store->url);
4119             }
4120             }
4121              
4122 0         0 return $builder;
4123             }
4124              
4125             sub showActorGroupCmd {
4126 0     0   0 my $o = shift;
4127 0         0 my $cmd = shift;
4128              
4129 0         0 $cmd->collect($o);
4130 0         0 $o->showActorGroup($o->{actorGroupToken}->actorGroup);
4131 0         0 $o->{ui}->space;
4132             }
4133              
4134             sub showActorGroup {
4135 0     0   0 my $o = shift;
4136 0 0 0     0 my $actorGroup = shift; die 'wrong type '.ref($actorGroup).' for $actorGroup' if defined $actorGroup && ref $actorGroup ne 'CDS::ActorGroup';
  0         0  
4137              
4138 0         0 $o->{ui}->space;
4139 0 0       0 $o->{ui}->title(length $o->{label} ? 'Actors of '.$o->{label} : 'Actor group');
4140 0         0 for my $member ($actorGroup->members) {
4141 0 0       0 my $date = $member->revision ? $o->{ui}->niceDateTimeLocal($member->revision) : ' ';
4142 0 0       0 my $status = $member->isActive ? $o->{ui}->green('active ') : $o->{ui}->gray('idle ');
4143 0         0 my $storeReference = $o->{actor}->blueStoreUrlReference($member->storeUrl);
4144 0         0 $o->{ui}->line($o->{ui}->gray($date), ' ', $status, ' ', $member->actorOnStore->publicKey->hash->hex, ' on ', $storeReference);
4145             }
4146              
4147 0 0       0 if ($actorGroup->entrustedActorsRevision) {
4148 0         0 $o->{ui}->space;
4149 0 0       0 $o->{ui}->title(length $o->{label} ? 'Actors entrusted by '.$o->{label} : 'Entrusted actors');
4150 0         0 $o->{ui}->line($o->{ui}->gray($o->{ui}->niceDateTimeLocal($actorGroup->entrustedActorsRevision)));
4151 0         0 for my $actor ($actorGroup->entrustedActors) {
4152 0         0 my $storeReference = $o->{actor}->storeUrlReference($actor->storeUrl);
4153 0         0 $o->{ui}->line($actor->actorOnStore->publicKey->hash->hex, $o->{ui}->gray(' on ', $storeReference));
4154             }
4155              
4156 0 0       0 $o->{ui}->line($o->{ui}->gray('(none)')) if ! scalar $actorGroup->entrustedActors;
4157             }
4158             }
4159              
4160             sub onDiscoverActorGroupVerifyStore {
4161 0     0   0 my $o = shift;
4162 0         0 my $storeUrl = shift;
4163 0 0 0     0 my $actorHash = shift; die 'wrong type '.ref($actorHash).' for $actorHash' if defined $actorHash && ref $actorHash ne 'CDS::Hash';
  0         0  
4164              
4165 0         0 return $o->{actor}->storeForUrl($storeUrl);
4166             }
4167              
4168             sub onDiscoverActorGroupInvalidPublicKey {
4169 0     0   0 my $o = shift;
4170 0 0 0     0 my $actorHash = shift; die 'wrong type '.ref($actorHash).' for $actorHash' if defined $actorHash && ref $actorHash ne 'CDS::Hash';
  0         0  
4171 0         0 my $store = shift;
4172 0         0 my $reason = shift;
4173              
4174 0         0 $o->{ui}->warning('Public key ', $actorHash->hex, ' on ', $store->url, ' is invalid: ', $reason);
4175             }
4176              
4177             sub onDiscoverActorGroupInvalidCard {
4178 0     0   0 my $o = shift;
4179 0 0 0     0 my $actorOnStore = shift; die 'wrong type '.ref($actorOnStore).' for $actorOnStore' if defined $actorOnStore && ref $actorOnStore ne 'CDS::ActorOnStore';
  0         0  
4180 0 0 0     0 my $envelopeHash = shift; die 'wrong type '.ref($envelopeHash).' for $envelopeHash' if defined $envelopeHash && ref $envelopeHash ne 'CDS::Hash';
  0         0  
4181 0         0 my $reason = shift;
4182              
4183 0         0 $o->{ui}->warning('Card ', $envelopeHash->hex, ' on ', $actorOnStore->store->url, ' is invalid: ', $reason);
4184             }
4185              
4186             sub onDiscoverActorGroupStoreError {
4187 0     0   0 my $o = shift;
4188 0         0 my $store = shift;
4189 0         0 my $error = shift;
4190              
4191             }
4192              
4193             # BEGIN AUTOGENERATED
4194             package CDS::Commands::EntrustedActors;
4195              
4196             sub register {
4197 0     0   0 my $class = shift;
4198 0         0 my $cds = shift;
4199 0         0 my $help = shift;
4200              
4201 0         0 my $node000 = CDS::Parser::Node->new(0);
4202 0         0 my $node001 = CDS::Parser::Node->new(0);
4203 0         0 my $node002 = CDS::Parser::Node->new(0);
4204 0         0 my $node003 = CDS::Parser::Node->new(0);
4205 0         0 my $node004 = CDS::Parser::Node->new(0);
4206 0         0 my $node005 = CDS::Parser::Node->new(0);
4207 0         0 my $node006 = CDS::Parser::Node->new(0);
4208 0         0 my $node007 = CDS::Parser::Node->new(0);
4209 0         0 my $node008 = CDS::Parser::Node->new(0);
4210 0         0 my $node009 = CDS::Parser::Node->new(0);
4211 0         0 my $node010 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
4212 0         0 my $node011 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&show});
4213 0         0 my $node012 = CDS::Parser::Node->new(0);
4214 0         0 my $node013 = CDS::Parser::Node->new(0);
4215 0         0 my $node014 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&doNotEntrust});
4216 0         0 my $node015 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&entrust});
4217 0         0 my $node016 = CDS::Parser::Node->new(0);
4218 0         0 $cds->addArrow($node001, 1, 0, 'show');
4219 0         0 $cds->addArrow($node003, 1, 0, 'do');
4220 0         0 $cds->addArrow($node005, 1, 0, 'entrust');
4221 0         0 $help->addArrow($node000, 1, 0, 'entrusted');
4222 0         0 $node000->addArrow($node010, 1, 0, 'actors');
4223 0         0 $node001->addArrow($node002, 1, 0, 'entrusted');
4224 0         0 $node002->addArrow($node011, 1, 0, 'actors');
4225 0         0 $node003->addArrow($node004, 1, 0, 'not');
4226 0         0 $node004->addArrow($node008, 1, 0, 'entrust');
4227 0         0 $node005->addDefault($node006);
4228 0         0 $node005->addDefault($node007);
4229 0         0 $node005->addArrow($node012, 1, 0, 'ACTOR', \&collectActor);
4230 0         0 $node006->addArrow($node006, 1, 0, 'ACCOUNT', \&collectAccount);
4231 0         0 $node006->addArrow($node015, 1, 1, 'ACCOUNT', \&collectAccount);
4232 0         0 $node007->addArrow($node007, 1, 0, 'ACTOR', \&collectActor1);
4233 0         0 $node007->addArrow($node015, 1, 0, 'ACTOR', \&collectActor1);
4234 0         0 $node008->addDefault($node009);
4235 0         0 $node009->addArrow($node009, 1, 0, 'ACTOR', \&collectActor2);
4236 0         0 $node009->addArrow($node014, 1, 0, 'ACTOR', \&collectActor2);
4237 0         0 $node012->addArrow($node013, 1, 0, 'on');
4238 0         0 $node013->addArrow($node015, 1, 0, 'STORE', \&collectStore);
4239 0         0 $node015->addArrow($node016, 1, 0, 'and');
4240 0         0 $node016->addDefault($node005);
4241             }
4242              
4243             sub collectAccount {
4244 0     0   0 my $o = shift;
4245 0         0 my $label = shift;
4246 0         0 my $value = shift;
4247              
4248 0         0 push @{$o->{accountTokens}}, $value;
  0         0  
4249             }
4250              
4251             sub collectActor {
4252 0     0   0 my $o = shift;
4253 0         0 my $label = shift;
4254 0         0 my $value = shift;
4255              
4256 0         0 $o->{actorHash} = $value;
4257             }
4258              
4259             sub collectActor1 {
4260 0     0   0 my $o = shift;
4261 0         0 my $label = shift;
4262 0         0 my $value = shift;
4263              
4264 0         0 push @{$o->{accountTokens}}, CDS::AccountToken->new($o->{actor}->preferredStore, $value);
  0         0  
4265             }
4266              
4267             sub collectActor2 {
4268 0     0   0 my $o = shift;
4269 0         0 my $label = shift;
4270 0         0 my $value = shift;
4271              
4272 0         0 push @{$o->{actorHashes}}, $value;
  0         0  
4273             }
4274              
4275             sub collectStore {
4276 0     0   0 my $o = shift;
4277 0         0 my $label = shift;
4278 0         0 my $value = shift;
4279              
4280 0         0 push @{$o->{accountTokens}}, CDS::AccountToken->new($value, $o->{actorHash});
  0         0  
4281 0         0 delete $o->{actorHash};
4282             }
4283              
4284             sub new {
4285 0     0   0 my $class = shift;
4286 0         0 my $actor = shift;
4287 0         0 bless {actor => $actor, ui => $actor->ui} }
4288              
4289             # END AUTOGENERATED
4290              
4291             # HTML FOLDER NAME entrusted-actors
4292             # HTML TITLE Entrusted actors
4293             sub help {
4294 0     0   0 my $o = shift;
4295 0         0 my $cmd = shift;
4296              
4297 0         0 my $ui = $o->{ui};
4298 0         0 $ui->space;
4299 0         0 $ui->command('cds show entrusted actors');
4300 0         0 $ui->p('Shows all entrusted actors.');
4301 0         0 $ui->space;
4302 0         0 $ui->command('cds entrust ACCOUNT*');
4303 0         0 $ui->command('cds entrust ACTOR on STORE');
4304 0         0 $ui->p('Adds the indicated entrusted actors. Entrusted actors can read our private data and messages. The public key of the entrusted actor must be available on the store.');
4305 0         0 $ui->space;
4306 0         0 $ui->command('cds do not entrust ACTOR*');
4307 0         0 $ui->p('Removes the indicated entrusted actors.');
4308 0         0 $ui->space;
4309 0         0 $ui->p('After modifying the entrusted actors, you should "cds announce" yourself to publish the changes.');
4310 0         0 $ui->space;
4311             }
4312              
4313             sub show {
4314 0     0   0 my $o = shift;
4315 0         0 my $cmd = shift;
4316              
4317 0         0 my $builder = CDS::ActorGroupBuilder->new;
4318 0         0 $builder->parseEntrustedActorList($o->{actor}->entrustedActorsSelector->record, 1);
4319              
4320 0         0 my @actors = $builder->entrustedActors;
4321 0         0 for my $actor (@actors) {
4322 0         0 my $storeReference = $o->{actor}->storeUrlReference($actor->storeUrl);
4323 0         0 $o->{ui}->line($actor->hash->hex, $o->{ui}->gray(' on ', $storeReference));
4324             }
4325              
4326 0 0       0 return if scalar @actors;
4327 0         0 $o->{ui}->line($o->{ui}->gray('none'));
4328             }
4329              
4330             sub entrust {
4331 0     0   0 my $o = shift;
4332 0         0 my $cmd = shift;
4333              
4334 0         0 $o->{accountTokens} = [];
4335 0         0 $cmd->collect($o);
4336              
4337             # Get the list of currently entrusted actors
4338 0         0 my $entrusted = $o->createEntrustedActorsIndex;
4339              
4340             # Add new actors
4341 0         0 for my $accountToken (@{$o->{accountTokens}}) {
  0         0  
4342 0         0 my $actorHash = $accountToken->actorHash;
4343              
4344             # Check if the key is already entrusted
4345 0 0       0 if ($entrusted->{$accountToken->url}) {
4346 0         0 $o->{ui}->pOrange($accountToken->url, ' is already entrusted.');
4347 0         0 next;
4348             }
4349              
4350             # Get the public key
4351 0         0 my ($publicKey, $invalidReason, $storeError) = $o->{actor}->keyPair->getPublicKey($actorHash, $accountToken->cliStore);
4352 0 0       0 if (defined $storeError) {
4353 0         0 $o->{ui}->pRed('Unable to get the public key ', $actorHash->hex, ' from ', $accountToken->cliStore->url, ': ', $storeError);
4354 0         0 next;
4355             }
4356              
4357 0 0       0 if (defined $invalidReason) {
4358 0         0 $o->{ui}->pRed('Unable to get the public key ', $actorHash->hex, ' from ', $accountToken->cliStore->url, ': ', $invalidReason);
4359 0         0 next;
4360             }
4361              
4362             # Add it
4363 0         0 $o->{actor}->entrust($accountToken->cliStore->url, $publicKey);
4364 0 0       0 $o->{ui}->pGreen($entrusted->{$actorHash->hex} ? 'Updated ' : 'Added ', $actorHash->hex, ' as entrusted actor.');
4365             }
4366              
4367             # Save
4368 0         0 $o->{actor}->saveOrShowError;
4369             }
4370              
4371             sub doNotEntrust {
4372 0     0   0 my $o = shift;
4373 0         0 my $cmd = shift;
4374              
4375 0         0 $o->{actorHashes} = [];
4376 0         0 $cmd->collect($o);
4377              
4378             # Get the list of currently entrusted actors
4379 0         0 my $entrusted = $o->createEntrustedActorsIndex;
4380              
4381             # Remove entrusted actors
4382 0         0 for my $actorHash (@{$o->{actorHashes}}) {
  0         0  
4383 0 0       0 if ($entrusted->{$actorHash->hex}) {
4384 0         0 $o->{actor}->doNotEntrust($actorHash);
4385 0         0 $o->{ui}->pGreen('Removed ', $actorHash->hex, ' from the list of entrusted actors.');
4386             } else {
4387 0         0 $o->{ui}->pOrange($actorHash->hex, ' is not entrusted.');
4388             }
4389             }
4390              
4391             # Save
4392 0         0 $o->{actor}->saveOrShowError;
4393             }
4394              
4395             sub createEntrustedActorsIndex {
4396 0     0   0 my $o = shift;
4397              
4398 0         0 my $builder = CDS::ActorGroupBuilder->new;
4399 0         0 $builder->parseEntrustedActorList($o->{actor}->entrustedActorsSelector->record, 1);
4400              
4401 0         0 my $index = {};
4402 0         0 for my $actor ($builder->entrustedActors) {
4403 0         0 my $url = $actor->storeUrl.'/accounts/'.$actor->hash->hex;
4404 0         0 $index->{$actor->hash->hex} = 1;
4405 0         0 $index->{$url} = 1;
4406             }
4407              
4408 0         0 return $index;
4409             }
4410              
4411             package CDS::Commands::FolderStore;
4412              
4413             # BEGIN AUTOGENERATED
4414              
4415             sub register {
4416 0     0   0 my $class = shift;
4417 0         0 my $cds = shift;
4418 0         0 my $help = shift;
4419              
4420 0         0 my $node000 = CDS::Parser::Node->new(0);
4421 0         0 my $node001 = CDS::Parser::Node->new(0);
4422 0         0 my $node002 = CDS::Parser::Node->new(0);
4423 0         0 my $node003 = CDS::Parser::Node->new(0);
4424 0         0 my $node004 = CDS::Parser::Node->new(0);
4425 0         0 my $node005 = CDS::Parser::Node->new(0);
4426 0         0 my $node006 = CDS::Parser::Node->new(0);
4427 0         0 my $node007 = CDS::Parser::Node->new(0);
4428 0         0 my $node008 = CDS::Parser::Node->new(0);
4429 0         0 my $node009 = CDS::Parser::Node->new(0);
4430 0         0 my $node010 = CDS::Parser::Node->new(0);
4431 0         0 my $node011 = CDS::Parser::Node->new(0);
4432 0         0 my $node012 = CDS::Parser::Node->new(0);
4433 0         0 my $node013 = CDS::Parser::Node->new(0);
4434 0         0 my $node014 = CDS::Parser::Node->new(0);
4435 0         0 my $node015 = CDS::Parser::Node->new(0);
4436 0         0 my $node016 = CDS::Parser::Node->new(0);
4437 0         0 my $node017 = CDS::Parser::Node->new(0);
4438 0         0 my $node018 = CDS::Parser::Node->new(0);
4439 0         0 my $node019 = CDS::Parser::Node->new(0);
4440 0         0 my $node020 = CDS::Parser::Node->new(0);
4441 0         0 my $node021 = CDS::Parser::Node->new(0);
4442 0         0 my $node022 = CDS::Parser::Node->new(0);
4443 0         0 my $node023 = CDS::Parser::Node->new(0);
4444 0         0 my $node024 = CDS::Parser::Node->new(0);
4445 0         0 my $node025 = CDS::Parser::Node->new(1);
4446 0         0 my $node026 = CDS::Parser::Node->new(0);
4447 0         0 my $node027 = CDS::Parser::Node->new(0);
4448 0         0 my $node028 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
4449 0         0 my $node029 = CDS::Parser::Node->new(1);
4450 0         0 my $node030 = CDS::Parser::Node->new(0);
4451 0         0 my $node031 = CDS::Parser::Node->new(0);
4452 0         0 my $node032 = CDS::Parser::Node->new(0);
4453 0         0 my $node033 = CDS::Parser::Node->new(0);
4454 0         0 my $node034 = CDS::Parser::Node->new(0);
4455 0         0 my $node035 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&checkPermissions});
4456 0         0 my $node036 = CDS::Parser::Node->new(0);
4457 0         0 my $node037 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&fixPermissions});
4458 0         0 my $node038 = CDS::Parser::Node->new(0);
4459 0         0 my $node039 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&showPermissions});
4460 0         0 my $node040 = CDS::Parser::Node->new(0);
4461 0         0 my $node041 = CDS::Parser::Node->new(1);
4462 0         0 my $node042 = CDS::Parser::Node->new(0);
4463 0         0 my $node043 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&addAccount});
4464 0         0 my $node044 = CDS::Parser::Node->new(0);
4465 0         0 my $node045 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&removeAccount});
4466 0         0 my $node046 = CDS::Parser::Node->new(0);
4467 0         0 my $node047 = CDS::Parser::Node->new(1);
4468 0         0 my $node048 = CDS::Parser::Node->new(0);
4469 0         0 my $node049 = CDS::Parser::Node->new(0);
4470 0         0 my $node050 = CDS::Parser::Node->new(0);
4471 0         0 my $node051 = CDS::Parser::Node->new(0);
4472 0         0 my $node052 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&checkPermissions});
4473 0         0 my $node053 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&fixPermissions});
4474 0         0 my $node054 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&showPermissions});
4475 0         0 my $node055 = CDS::Parser::Node->new(1);
4476 0         0 my $node056 = CDS::Parser::Node->new(0);
4477 0         0 my $node057 = CDS::Parser::Node->new(0);
4478 0         0 my $node058 = CDS::Parser::Node->new(0);
4479 0         0 my $node059 = CDS::Parser::Node->new(0);
4480 0         0 my $node060 = CDS::Parser::Node->new(0);
4481 0         0 my $node061 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&addAccount});
4482 0         0 my $node062 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&removeAccount});
4483 0         0 my $node063 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&setPermissions});
4484 0         0 my $node064 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&createStore});
4485 0         0 $cds->addArrow($node001, 1, 0, 'create');
4486 0         0 $cds->addArrow($node003, 1, 0, 'check');
4487 0         0 $cds->addArrow($node004, 1, 0, 'fix');
4488 0         0 $cds->addArrow($node005, 1, 0, 'show');
4489 0         0 $cds->addArrow($node007, 1, 0, 'set');
4490 0         0 $cds->addArrow($node009, 1, 0, 'add');
4491 0         0 $cds->addArrow($node010, 1, 0, 'add');
4492 0         0 $cds->addArrow($node011, 1, 0, 'add');
4493 0         0 $cds->addArrow($node012, 1, 0, 'add');
4494 0         0 $cds->addArrow($node013, 1, 0, 'add');
4495 0         0 $cds->addArrow($node023, 1, 0, 'remove');
4496 0         0 $help->addArrow($node000, 1, 0, 'create');
4497 0         0 $node000->addArrow($node028, 1, 0, 'store');
4498 0         0 $node001->addArrow($node002, 1, 0, 'store');
4499 0         0 $node002->addArrow($node029, 1, 0, 'FOLDERNAME', \&collectFoldername);
4500 0         0 $node003->addArrow($node035, 1, 0, 'permissions');
4501 0         0 $node004->addArrow($node037, 1, 0, 'permissions');
4502 0         0 $node005->addArrow($node006, 1, 0, 'permission');
4503 0         0 $node006->addArrow($node039, 1, 0, 'scheme');
4504 0         0 $node007->addArrow($node008, 1, 0, 'permission');
4505 0         0 $node008->addArrow($node041, 1, 0, 'scheme');
4506 0         0 $node009->addArrow($node014, 1, 0, 'account');
4507 0         0 $node010->addArrow($node015, 1, 0, 'account');
4508 0         0 $node011->addArrow($node016, 1, 0, 'account');
4509 0         0 $node012->addArrow($node017, 1, 0, 'account');
4510 0         0 $node013->addArrow($node018, 1, 0, 'account');
4511 0         0 $node014->addArrow($node019, 1, 0, 'for');
4512 0         0 $node015->addArrow($node020, 1, 0, 'for');
4513 0         0 $node016->addArrow($node021, 1, 0, 'for');
4514 0         0 $node017->addArrow($node043, 1, 1, 'ACCOUNT', \&collectAccount);
4515 0         0 $node018->addArrow($node022, 1, 0, 'for');
4516 0         0 $node019->addArrow($node043, 1, 0, 'OBJECTFILE', \&collectObjectfile);
4517 0         0 $node020->addArrow($node043, 1, 0, 'KEYPAIR', \&collectKeypair);
4518 0         0 $node021->addArrow($node025, 1, 0, 'ACTOR', \&collectActor);
4519 0         0 $node022->addArrow($node043, 1, 0, 'OBJECT', \&collectObject);
4520 0         0 $node023->addArrow($node024, 1, 0, 'account');
4521 0         0 $node024->addArrow($node045, 1, 0, 'HASH', \&collectHash);
4522 0         0 $node025->addArrow($node026, 1, 0, 'on');
4523 0         0 $node025->addArrow($node027, 0, 0, 'from');
4524 0         0 $node026->addArrow($node043, 1, 0, 'STORE', \&collectStore);
4525 0         0 $node027->addArrow($node043, 0, 0, 'STORE', \&collectStore);
4526 0         0 $node029->addArrow($node030, 1, 0, 'for');
4527 0         0 $node029->addArrow($node031, 1, 0, 'for');
4528 0         0 $node029->addArrow($node032, 1, 0, 'for');
4529 0         0 $node029->addDefault($node047);
4530 0         0 $node030->addArrow($node033, 1, 0, 'user');
4531 0         0 $node031->addArrow($node034, 1, 0, 'group');
4532 0         0 $node032->addArrow($node047, 1, 0, 'everybody', \&collectEverybody);
4533 0         0 $node033->addArrow($node047, 1, 0, 'USER', \&collectUser);
4534 0         0 $node034->addArrow($node047, 1, 0, 'GROUP', \&collectGroup);
4535 0         0 $node035->addArrow($node036, 1, 0, 'of');
4536 0         0 $node036->addArrow($node052, 1, 0, 'STORE', \&collectStore1);
4537 0         0 $node037->addArrow($node038, 1, 0, 'of');
4538 0         0 $node038->addArrow($node053, 1, 0, 'STORE', \&collectStore1);
4539 0         0 $node039->addArrow($node040, 1, 0, 'of');
4540 0         0 $node040->addArrow($node054, 1, 0, 'STORE', \&collectStore1);
4541 0         0 $node041->addArrow($node042, 1, 0, 'of');
4542 0         0 $node041->addDefault($node055);
4543 0         0 $node042->addArrow($node055, 1, 0, 'STORE', \&collectStore1);
4544 0         0 $node043->addArrow($node044, 1, 0, 'to');
4545 0         0 $node044->addArrow($node061, 1, 0, 'STORE', \&collectStore1);
4546 0         0 $node045->addArrow($node046, 1, 0, 'from');
4547 0         0 $node046->addArrow($node062, 1, 0, 'STORE', \&collectStore1);
4548 0         0 $node047->addArrow($node048, 1, 0, 'and');
4549 0         0 $node047->addDefault($node064);
4550 0         0 $node048->addArrow($node049, 1, 0, 'remember');
4551 0         0 $node049->addArrow($node050, 1, 0, 'it');
4552 0         0 $node050->addArrow($node051, 1, 0, 'as');
4553 0         0 $node051->addArrow($node064, 1, 0, 'TEXT', \&collectText);
4554 0         0 $node055->addArrow($node056, 1, 0, 'to');
4555 0         0 $node055->addArrow($node057, 1, 0, 'to');
4556 0         0 $node055->addArrow($node058, 1, 0, 'to');
4557 0         0 $node056->addArrow($node059, 1, 0, 'user');
4558 0         0 $node057->addArrow($node060, 1, 0, 'group');
4559 0         0 $node058->addArrow($node063, 1, 0, 'everybody', \&collectEverybody);
4560 0         0 $node059->addArrow($node063, 1, 0, 'USER', \&collectUser);
4561 0         0 $node060->addArrow($node063, 1, 0, 'GROUP', \&collectGroup);
4562             }
4563              
4564             sub collectAccount {
4565 0     0   0 my $o = shift;
4566 0         0 my $label = shift;
4567 0         0 my $value = shift;
4568              
4569 0         0 $o->{accountToken} = $value;
4570             }
4571              
4572             sub collectActor {
4573 0     0   0 my $o = shift;
4574 0         0 my $label = shift;
4575 0         0 my $value = shift;
4576              
4577 0         0 $o->{actorHash} = $value;
4578             }
4579              
4580             sub collectEverybody {
4581 0     0   0 my $o = shift;
4582 0         0 my $label = shift;
4583 0         0 my $value = shift;
4584              
4585 0         0 $o->{permissions} = CDS::FolderStore::PosixPermissions::World->new;
4586             }
4587              
4588             sub collectFoldername {
4589 0     0   0 my $o = shift;
4590 0         0 my $label = shift;
4591 0         0 my $value = shift;
4592              
4593 0         0 $o->{foldername} = $value;
4594             }
4595              
4596             sub collectGroup {
4597 0     0   0 my $o = shift;
4598 0         0 my $label = shift;
4599 0         0 my $value = shift;
4600              
4601 0         0 $o->{permissions} = CDS::FolderStore::PosixPermissions::Group->new($o->{group});
4602             }
4603              
4604             sub collectHash {
4605 0     0   0 my $o = shift;
4606 0         0 my $label = shift;
4607 0         0 my $value = shift;
4608              
4609 0         0 $o->{hash} = $value;
4610             }
4611              
4612             sub collectKeypair {
4613 0     0   0 my $o = shift;
4614 0         0 my $label = shift;
4615 0         0 my $value = shift;
4616              
4617 0         0 $o->{keyPairToken} = $value;
4618             }
4619              
4620             sub collectObject {
4621 0     0   0 my $o = shift;
4622 0         0 my $label = shift;
4623 0         0 my $value = shift;
4624              
4625 0         0 $o->{accountToken} = CDS::AccountToken->new($value->cliStore, $value->hash);
4626             }
4627              
4628             sub collectObjectfile {
4629 0     0   0 my $o = shift;
4630 0         0 my $label = shift;
4631 0         0 my $value = shift;
4632              
4633 0         0 $o->{file} = $value;
4634             }
4635              
4636             sub collectStore {
4637 0     0   0 my $o = shift;
4638 0         0 my $label = shift;
4639 0         0 my $value = shift;
4640              
4641 0         0 $o->{accountToken} = CDS::AccountToken->new($value, $o->{actorHash});
4642             }
4643              
4644             sub collectStore1 {
4645 0     0   0 my $o = shift;
4646 0         0 my $label = shift;
4647 0         0 my $value = shift;
4648              
4649 0         0 $o->{store} = $value;
4650             }
4651              
4652             sub collectText {
4653 0     0   0 my $o = shift;
4654 0         0 my $label = shift;
4655 0         0 my $value = shift;
4656              
4657 0         0 $o->{label} = $value;
4658             }
4659              
4660             sub collectUser {
4661 0     0   0 my $o = shift;
4662 0         0 my $label = shift;
4663 0         0 my $value = shift;
4664              
4665 0         0 $o->{permissions} = CDS::FolderStore::PosixPermissions::User->new($value);
4666             }
4667              
4668             sub new {
4669 0     0   0 my $class = shift;
4670 0         0 my $actor = shift;
4671 0         0 bless {actor => $actor, ui => $actor->ui} }
4672              
4673             # END AUTOGENERATED
4674              
4675             # HTML FOLDER NAME folder-store
4676             # HTML TITLE Folder store management
4677             sub help {
4678 0     0   0 my $o = shift;
4679 0         0 my $cmd = shift;
4680              
4681 0         0 my $ui = $o->{ui};
4682 0         0 $ui->space;
4683 0         0 $ui->command('cds create store FOLDERNAME');
4684 0         0 $ui->p('Creates a new store in FOLDERNAME, and adds it to the list of known stores. If the folder does not exist, it is created. If it does exist, it must be empty.');
4685 0         0 $ui->space;
4686 0         0 $ui->p('By default, the filesystem permissions of the store are set such that only the current user can post objects and modify boxes. Other users on the system can post to the message box, list boxes, and read objects.');
4687 0         0 $ui->space;
4688 0         0 $ui->command('… for user USER');
4689 0         0 $ui->p('Makes the store accessible to the user USER.');
4690 0         0 $ui->space;
4691 0         0 $ui->command('… for group GROUP');
4692 0         0 $ui->p('Makes the store accessible to the group GROUP.');
4693 0         0 $ui->space;
4694 0         0 $ui->command('… for everybody');
4695 0         0 $ui->p('Makes the store accessible to everybody.');
4696 0         0 $ui->space;
4697 0         0 $ui->p('Note that the permissions only affect direct filesystem access. If your store is exposed by a server (e.g. a web server), it may be accessible to others.');
4698 0         0 $ui->space;
4699 0         0 $ui->command('… and remember it as TEXT');
4700 0         0 $ui->p('Remembers the store under the label TEXT. See "cds help remember" for details.');
4701 0         0 $ui->space;
4702 0         0 $ui->command('cds check permissions [of STORE]');
4703 0         0 $ui->p('Checks the permissions (owner, mode) of all accounts, boxes, box entries, and objects of the store, and reports any error. The permission scheme (user, group, or everybody) is derived from the "accounts" and "objects" folders.');
4704 0         0 $ui->p('If the store is omitted, the selected store is used.');
4705 0         0 $ui->space;
4706 0         0 $ui->command('cds fix permissions [of STORE]');
4707 0         0 $ui->p('Same as above, but tries to fix the permissions (chown, chmod) instead of just reporting them.');
4708 0         0 $ui->space;
4709 0         0 $ui->command('cds show permission scheme [of STORE]');
4710 0         0 $ui->p('Reports the permission scheme of the store.');
4711 0         0 $ui->space;
4712 0         0 $ui->command('cds set permission scheme [of STORE] to (user USER|group GROUP|everybody)');
4713 0         0 $ui->p('Sets the permission scheme of the stores, and changes all permissions accordingly.');
4714 0         0 $ui->space;
4715 0         0 $ui->command('cds add account ACCOUNT [to STORE]');
4716 0         0 $ui->command('cds add account for FILE [to STORE]');
4717 0         0 $ui->command('cds add account for KEYPAIR [to STORE]');
4718 0         0 $ui->command('cds add account for OBJECT [to STORE]');
4719 0         0 $ui->command('cds add account for ACTOR on STORE [to STORE]');
4720 0         0 $ui->p('Uploads the public key (FILE, KEYPAIR, OBJECT, ACCOUNT, or ACTOR on STORE) onto the store, and adds the corresponding account. This grants the user the right to access this account.');
4721 0         0 $ui->space;
4722 0         0 $ui->command('cds remove account HASH [from STORE]');
4723 0         0 $ui->p('Removes the indicated account from the store. This immediately destroys the user\'s data.');
4724 0         0 $ui->space;
4725             }
4726              
4727             sub createStore {
4728 0     0   0 my $o = shift;
4729 0         0 my $cmd = shift;
4730              
4731 0         0 $o->{permissions} = CDS::FolderStore::PosixPermissions::User->new;
4732 0         0 $cmd->collect($o);
4733              
4734             # Give up if the folder is non-empty (but we accept hidden files)
4735 0         0 for my $file (CDS->listFolder($o->{foldername})) {
4736 0 0       0 next if $file =~ /^\./;
4737 0         0 $o->{ui}->pRed('The folder ', $o->{foldername}, ' is not empty. Giving up …');
4738 0         0 return;
4739             }
4740              
4741             # Create the object store
4742 0   0     0 $o->create($o->{foldername}.'/objects') // return;
4743 0         0 $o->{ui}->pGreen('Object store created for ', $o->{permissions}->target, '.');
4744              
4745             # Create the account store
4746 0   0     0 $o->create($o->{foldername}.'/accounts') // return;
4747 0         0 $o->{ui}->pGreen('Account store created for ', $o->{permissions}->target, '.');
4748              
4749             # Return if the user does not want us to add the store
4750 0 0       0 return if ! defined $o->{label};
4751              
4752             # Remember the store
4753 0         0 my $record = CDS::Record->new;
4754 0         0 $record->addText('store')->addText('file://'.$o->{foldername});
4755 0         0 $o->{actor}->remember($o->{label}, $record);
4756 0         0 $o->{actor}->saveOrShowError;
4757             }
4758              
4759             # Creates a folder with the selected permissions.
4760             sub create {
4761 0     0   0 my $o = shift;
4762 0         0 my $folder = shift;
4763              
4764             # Create the folders to here if necessary
4765 0         0 for my $intermediateFolder (CDS->intermediateFolders($folder)) {
4766 0         0 mkdir $intermediateFolder, 0755;
4767             }
4768              
4769             # mkdir (if it does not exist yet) and chmod (if it does exist already)
4770 0         0 mkdir $folder, $o->{permissions}->baseFolderMode;
4771 0         0 chmod $o->{permissions}->baseFolderMode, $folder;
4772 0   0     0 chown $o->{permissions}->uid // -1, $o->{permissions}->gid // -1, $folder;
      0        
4773              
4774             # Check if the result is correct
4775 0         0 my @s = stat $folder;
4776 0 0       0 return $o->{ui}->error('Unable to create ', $o->{foldername}, '.') if ! scalar @s;
4777 0         0 my $mode = $s[2];
4778 0 0       0 return $o->{ui}->error($folder, ' exists, but is not a folder') if ! Fcntl::S_ISDIR($mode);
4779 0 0 0     0 return $o->{ui}->error('Unable to set the owning user ', $o->{permissions}->user, ' for ', $folder, '.') if defined $o->{permissions}->uid && $s[4] != $o->{permissions}->uid;
4780 0 0 0     0 return $o->{ui}->error('Unable to set the owning group ', $o->{permissions}->group, ' for ', $folder, '.') if defined $o->{permissions}->gid && $s[5] != $o->{permissions}->gid;
4781 0 0       0 return $o->{ui}->error('Unable to set the mode on ', $folder, '.') if ($mode & 0777) != $o->{permissions}->baseFolderMode;
4782 0         0 return 1;
4783             }
4784              
4785             sub existingFolderStoreOrShowError {
4786 0     0   0 my $o = shift;
4787              
4788 0   0     0 my $store = $o->{store} // $o->{actor}->preferredStore;
4789              
4790 0         0 my $folderStore = CDS::FolderStore->forUrl($store->url);
4791 0 0       0 if (! $folderStore) {
4792 0         0 $o->{ui}->error('"', $store->url, '" is not a folder store.');
4793 0         0 $o->{ui}->space;
4794 0         0 $o->{ui}->p('Account management and file system permission checks only apply to stores on the local file system. Such stores are referred to by file://… URLs, or file system paths.');
4795 0         0 $o->{ui}->p('To fix the permissions on a remote store, log onto that server and fix the permissions there. Note that permissions are not part of the Condensation protocol, but a property of some underlying storage systems, such as file systems.');
4796 0         0 $o->{ui}->space;
4797 0         0 return;
4798             }
4799              
4800 0 0       0 if (! $folderStore->exists) {
4801 0         0 $o->{ui}->error('"', $folderStore->folder, '" does not exist.');
4802 0         0 $o->{ui}->space;
4803 0         0 $o->{ui}->p('The folder either does not exist, or is not a folder store. You can create this store using:');
4804 0         0 $o->{ui}->line($o->{ui}->gold(' cds create store ', $folderStore->folder));
4805 0         0 $o->{ui}->space;
4806 0         0 return;
4807             }
4808              
4809 0         0 return $folderStore;
4810             }
4811              
4812             sub showPermissions {
4813 0     0   0 my $o = shift;
4814 0         0 my $cmd = shift;
4815              
4816 0         0 $cmd->collect($o);
4817 0   0     0 my $folderStore = $o->existingFolderStoreOrShowError // return;
4818 0         0 $o->showStore($folderStore);
4819 0         0 $o->{ui}->space;
4820             }
4821              
4822             sub showStore {
4823 0     0   0 my $o = shift;
4824 0         0 my $folderStore = shift;
4825              
4826 0         0 $o->{ui}->space;
4827 0         0 $o->{ui}->title('Store');
4828 0         0 $o->{ui}->line($folderStore->folder);
4829 0         0 $o->{ui}->line('Accessible to ', $folderStore->permissions->target, '.');
4830             }
4831              
4832             sub setPermissions {
4833 0     0   0 my $o = shift;
4834 0         0 my $cmd = shift;
4835              
4836 0         0 $cmd->collect($o);
4837              
4838 0   0     0 my $folderStore = $o->existingFolderStoreOrShowError // return;
4839 0         0 $o->showStore($folderStore);
4840              
4841 0         0 $folderStore->setPermissions($o->{permissions});
4842 0         0 $o->{ui}->line('Changing permissions …');
4843 0         0 my $logger = CDS::Commands::FolderStore::SetLogger->new($o, $folderStore->folder);
4844 0 0       0 $folderStore->checkPermissions($logger) || $o->traversalFailed($folderStore);
4845 0         0 $logger->summary;
4846              
4847 0         0 $o->{ui}->space;
4848             }
4849              
4850             sub checkPermissions {
4851 0     0   0 my $o = shift;
4852 0         0 my $cmd = shift;
4853              
4854 0         0 $cmd->collect($o);
4855              
4856 0   0     0 my $folderStore = $o->existingFolderStoreOrShowError // return;
4857 0         0 $o->showStore($folderStore);
4858              
4859 0         0 $o->{ui}->line('Checking permissions …');
4860 0         0 my $logger = CDS::Commands::FolderStore::CheckLogger->new($o, $folderStore->folder);
4861 0 0       0 $folderStore->checkPermissions($logger) || $o->traversalFailed($folderStore);
4862 0         0 $logger->summary;
4863              
4864 0         0 $o->{ui}->space;
4865             }
4866              
4867             sub fixPermissions {
4868 0     0   0 my $o = shift;
4869 0         0 my $cmd = shift;
4870              
4871 0         0 $cmd->collect($o);
4872              
4873 0   0     0 my $folderStore = $o->existingFolderStoreOrShowError // return;
4874 0         0 $o->showStore($folderStore);
4875              
4876 0         0 $o->{ui}->line('Fixing permissions …');
4877 0         0 my $logger = CDS::Commands::FolderStore::FixLogger->new($o, $folderStore->folder);
4878 0 0       0 $folderStore->checkPermissions($logger) || $o->traversalFailed($folderStore);
4879 0         0 $logger->summary;
4880              
4881 0         0 $o->{ui}->space;
4882             }
4883              
4884             sub traversalFailed {
4885 0     0   0 my $o = shift;
4886 0         0 my $folderStore = shift;
4887              
4888 0         0 $o->{ui}->space;
4889 0         0 $o->{ui}->p('Traversal failed because a file or folder could not be accessed. You may have to fix the permissions manually, or run this command with other privileges.');
4890 0         0 $o->{ui}->p('If you have root privileges, you can take over this store using:');
4891 0         0 my $userName = getpwuid($<);
4892 0         0 my $groupName = getgrgid($();
4893 0         0 $o->{ui}->line($o->{ui}->gold(' sudo chown -R ', $userName, ':', $groupName, ' ', $folderStore->folder));
4894 0         0 $o->{ui}->p('and then set the desired permission scheme:');
4895 0         0 $o->{ui}->line($o->{ui}->gold(' cds set permissions of ', $folderStore->folder, ' to …'));
4896 0         0 $o->{ui}->space;
4897 0         0 exit(1);
4898             }
4899              
4900             sub addAccount {
4901 0     0   0 my $o = shift;
4902 0         0 my $cmd = shift;
4903              
4904 0         0 $cmd->collect($o);
4905              
4906             # Prepare
4907 0   0     0 my $folderStore = $o->existingFolderStoreOrShowError // return;
4908 0   0     0 my $publicKey = $o->publicKey // return;
4909              
4910             # Upload the public key onto the store
4911 0         0 my $error = $folderStore->put($publicKey->hash, $publicKey->object);
4912 0 0       0 return $o->{ui}->error('Unable to upload the public key: ', $error) if $error;
4913              
4914             # Create the account folder
4915 0         0 my $folder = $folderStore->folder.'/accounts/'.$publicKey->hash->hex;
4916 0         0 my $permissions = $folderStore->permissions;
4917 0         0 $permissions->mkdir($folder, $permissions->accountFolderMode);
4918 0 0       0 return $o->{ui}->error('Unable to create folder "', $folder, '".') if ! -d $folder;
4919 0         0 $o->{ui}->pGreen('Account ', $publicKey->hash->hex, ' added.');
4920 0         0 return 1;
4921             }
4922              
4923             sub publicKey {
4924 0     0   0 my $o = shift;
4925              
4926 0 0       0 return $o->{keyPairToken}->keyPair->publicKey if $o->{keyPairToken};
4927              
4928 0 0       0 if ($o->{file}) {
4929 0   0     0 my $bytes = CDS->readBytesFromFile($o->{file}) // return $o->{ui}->error('Cannot read "', $o->{file}, '".');
4930 0   0     0 my $object = CDS::Object->fromBytes($bytes) // return $o->{ui}->error('"', $o->{file}, '" is not a public key.');
4931 0   0     0 return CDS::PublicKey->fromObject($object) // return $o->{ui}->error('"', $o->{file}, '" is not a public key.');
4932             }
4933              
4934 0         0 return $o->{actor}->uiGetPublicKey($o->{accountToken}->actorHash, $o->{accountToken}->cliStore, $o->{actor}->preferredKeyPairToken);
4935             }
4936              
4937             sub removeAccount {
4938 0     0   0 my $o = shift;
4939 0         0 my $cmd = shift;
4940              
4941 0         0 $cmd->collect($o);
4942              
4943             # Prepare the folder
4944 0   0     0 my $folderStore = $o->existingFolderStoreOrShowError // return;
4945 0         0 my $folder = $folderStore->folder.'/accounts/'.$o->{hash}->hex;
4946 0         0 my $deletedFolder = $folderStore->folder.'/accounts/deleted-'.$o->{hash}->hex;
4947              
4948             # Rename, so that it is not visible any more
4949 0 0       0 $o->recursivelyDelete($deletedFolder) if -e $deletedFolder;
4950 0 0       0 return $o->{ui}->line('The account ', $o->{hash}->hex, ' does not exist.') if ! -e $folder;
4951 0 0       0 rename($folder, $deletedFolder) || return $o->{ui}->error('Unable to rename the folder "', $folder, '".');
4952              
4953             # Try to delete it entirely
4954 0         0 $o->recursivelyDelete($deletedFolder);
4955 0         0 $o->{ui}->pGreen('Account ', $o->{hash}->hex, ' removed.');
4956 0         0 return 1;
4957             }
4958              
4959             sub recursivelyDelete {
4960 0     0   0 my $o = shift;
4961 0         0 my $folder = shift;
4962              
4963 0         0 for my $filename (CDS->listFolder($folder)) {
4964 0 0       0 next if $filename =~ /^\./;
4965 0         0 my $file = $folder.'/'.$filename;
4966 0 0       0 if (-f $file) {
    0          
4967 0   0     0 unlink $file || $o->{ui}->pOrange('Unable to remove the file "', $file, '".');
4968             } elsif (-d $file) {
4969 0         0 $o->recursivelyDelete($file);
4970             }
4971             }
4972              
4973 0 0       0 rmdir($folder) || $o->{ui}->pOrange('Unable to remove the folder "', $folder, '".');
4974             }
4975              
4976             package CDS::Commands::FolderStore::CheckLogger;
4977              
4978 1     1   25224 use parent -norequire, 'CDS::Commands::FolderStore::Logger';
  1         7  
  1         5  
4979              
4980             sub finalizeWrong {
4981 0     0   0 my $o = shift;
4982              
4983 0         0 $o->{ui}->pRed(@_);
4984 0         0 return 0;
4985             }
4986              
4987             sub summary {
4988 0     0   0 my $o = shift;
4989              
4990 0         0 $o->{ui}->p(($o->{correct} + $o->{wrong}).' files and folders traversed.');
4991 0 0       0 if ($o->{wrong} > 0) {
4992 0         0 $o->{ui}->p($o->{wrong}, ' files and folders have wrong permissions. To fix them, run');
4993 0         0 $o->{ui}->line($o->{ui}->gold(' cds fix permissions of ', $o->{store}->url));
4994             } else {
4995 0         0 $o->{ui}->pGreen('All permissions are OK.');
4996             }
4997             }
4998              
4999             package CDS::Commands::FolderStore::FixLogger;
5000              
5001 1     1   181 use parent -norequire, 'CDS::Commands::FolderStore::Logger';
  1         13  
  1         5  
5002              
5003             sub finalizeWrong {
5004 0     0   0 my $o = shift;
5005              
5006 0         0 $o->{ui}->line(@_);
5007 0         0 return 1;
5008             }
5009              
5010             sub summary {
5011 0     0   0 my $o = shift;
5012              
5013 0         0 $o->{ui}->p(($o->{correct} + $o->{wrong}).' files and folders traversed.');
5014 0 0       0 $o->{ui}->p('The permissions of ', $o->{wrong}, ' files and folders have been fixed.') if $o->{wrong} > 0;
5015 0         0 $o->{ui}->pGreen('All permissions are OK.');
5016             }
5017              
5018             package CDS::Commands::FolderStore::Logger;
5019              
5020             sub new {
5021 0     0   0 my $class = shift;
5022 0         0 my $parent = shift;
5023 0         0 my $baseFolder = shift;
5024              
5025             return bless {
5026             ui => $parent->{ui},
5027             store => $parent->{store},
5028 0         0 baseFolder => $baseFolder,
5029             correct => 0,
5030             wrong => 0,
5031             }, $class;
5032             }
5033              
5034             sub correct {
5035 0     0   0 my $o = shift;
5036              
5037 0         0 $o->{correct} += 1;
5038             }
5039              
5040             sub wrong {
5041 0     0   0 my $o = shift;
5042 0         0 my $item = shift;
5043 0         0 my $uid = shift;
5044 0         0 my $gid = shift;
5045 0         0 my $mode = shift;
5046 0         0 my $expectedUid = shift;
5047 0         0 my $expectedGid = shift;
5048 0         0 my $expectedMode = shift;
5049              
5050 0         0 my $len = length $o->{baseFolder};
5051 0         0 $o->{wrong} += 1;
5052 0 0 0     0 $item = '…'.substr($item, $len) if length $item > $len && substr($item, 0, $len) eq $o->{baseFolder};
5053 0         0 my @changes;
5054 0 0 0     0 push @changes, 'user '.&username($uid).' -> '.&username($expectedUid) if defined $expectedUid && $uid != $expectedUid;
5055 0 0 0     0 push @changes, 'group '.&groupname($gid).' -> '.&groupname($expectedGid) if defined $expectedGid && $gid != $expectedGid;
5056 0 0       0 push @changes, 'mode '.sprintf('%04o -> %04o', $mode, $expectedMode) if $mode != $expectedMode;
5057 0         0 return $o->finalizeWrong(join(', ', @changes), "\t", $item);
5058             }
5059              
5060             sub username {
5061 0     0   0 my $uid = shift;
5062              
5063 0   0     0 return getpwuid($uid) // $uid;
5064             }
5065              
5066             sub groupname {
5067 0     0   0 my $gid = shift;
5068              
5069 0   0     0 return getgrgid($gid) // $gid;
5070             }
5071              
5072             sub accessError {
5073 0     0   0 my $o = shift;
5074 0         0 my $item = shift;
5075              
5076 0         0 $o->{ui}->error('Error accessing ', $item, '.');
5077 0         0 return 0;
5078             }
5079              
5080             sub setError {
5081 0     0   0 my $o = shift;
5082 0         0 my $item = shift;
5083              
5084 0         0 $o->{ui}->error('Error setting permissions of ', $item, '.');
5085 0         0 return 0;
5086             }
5087              
5088             package CDS::Commands::FolderStore::SetLogger;
5089              
5090 1     1   588 use parent -norequire, 'CDS::Commands::FolderStore::Logger';
  1         1  
  1         4  
5091              
5092             sub finalizeWrong {
5093 0     0   0 my $o = shift;
5094              
5095 0         0 return 1;
5096             }
5097              
5098             sub summary {
5099 0     0   0 my $o = shift;
5100              
5101 0         0 $o->{ui}->p(($o->{correct} + $o->{wrong}).' files and folders traversed.');
5102 0 0       0 $o->{ui}->p('The permissions of ', $o->{wrong}, ' files and folders have been adjusted.') if $o->{wrong} > 0;
5103 0         0 $o->{ui}->pGreen('All permissions are OK.');
5104             }
5105              
5106             # BEGIN AUTOGENERATED
5107             package CDS::Commands::Get;
5108              
5109             sub register {
5110 0     0   0 my $class = shift;
5111 0         0 my $cds = shift;
5112 0         0 my $help = shift;
5113              
5114 0         0 my $node000 = CDS::Parser::Node->new(0);
5115 0         0 my $node001 = CDS::Parser::Node->new(0);
5116 0         0 my $node002 = CDS::Parser::Node->new(0);
5117 0         0 my $node003 = CDS::Parser::Node->new(0);
5118 0         0 my $node004 = CDS::Parser::Node->new(0);
5119 0         0 my $node005 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
5120 0         0 my $node006 = CDS::Parser::Node->new(0);
5121 0         0 my $node007 = CDS::Parser::Node->new(0);
5122 0         0 my $node008 = CDS::Parser::Node->new(0);
5123 0         0 my $node009 = CDS::Parser::Node->new(0);
5124 0         0 my $node010 = CDS::Parser::Node->new(1);
5125 0         0 my $node011 = CDS::Parser::Node->new(0);
5126 0         0 my $node012 = CDS::Parser::Node->new(0);
5127 0         0 my $node013 = CDS::Parser::Node->new(0);
5128 0         0 my $node014 = CDS::Parser::Node->new(0);
5129 0         0 my $node015 = CDS::Parser::Node->new(0);
5130 0         0 my $node016 = CDS::Parser::Node->new(1);
5131 0         0 my $node017 = CDS::Parser::Node->new(0);
5132 0         0 my $node018 = CDS::Parser::Node->new(0);
5133 0         0 my $node019 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&get});
5134 0         0 my $node020 = CDS::Parser::Node->new(1);
5135 0         0 my $node021 = CDS::Parser::Node->new(0);
5136 0         0 my $node022 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&get});
5137 0         0 $cds->addArrow($node000, 1, 0, 'get');
5138 0         0 $cds->addArrow($node001, 1, 0, 'save');
5139 0         0 $cds->addArrow($node002, 1, 0, 'get');
5140 0         0 $cds->addArrow($node003, 1, 0, 'get');
5141 0         0 $cds->addArrow($node009, 1, 0, 'save', \&collectSave);
5142 0         0 $help->addArrow($node005, 1, 0, 'get');
5143 0         0 $help->addArrow($node005, 1, 0, 'save');
5144 0         0 $node000->addArrow($node010, 1, 0, 'HASH', \&collectHash);
5145 0         0 $node001->addArrow($node004, 1, 0, 'data');
5146 0         0 $node002->addArrow($node006, 1, 0, 'HASH', \&collectHash1);
5147 0         0 $node003->addArrow($node010, 1, 0, 'OBJECT', \&collectObject);
5148 0         0 $node004->addArrow($node009, 1, 0, 'of', \&collectOf);
5149 0         0 $node006->addArrow($node007, 1, 0, 'on');
5150 0         0 $node006->addArrow($node008, 0, 0, 'from');
5151 0         0 $node007->addArrow($node010, 1, 0, 'STORE', \&collectStore);
5152 0         0 $node008->addArrow($node010, 0, 0, 'STORE', \&collectStore);
5153 0         0 $node009->addArrow($node013, 1, 0, 'HASH', \&collectHash1);
5154 0         0 $node009->addArrow($node016, 1, 0, 'HASH', \&collectHash);
5155 0         0 $node009->addArrow($node016, 1, 0, 'OBJECT', \&collectObject1);
5156 0         0 $node010->addArrow($node011, 1, 0, 'decrypted');
5157 0         0 $node010->addDefault($node019);
5158 0         0 $node011->addArrow($node012, 1, 0, 'with');
5159 0         0 $node012->addArrow($node019, 1, 0, 'AESKEY', \&collectAeskey);
5160 0         0 $node013->addArrow($node014, 1, 0, 'on');
5161 0         0 $node013->addArrow($node015, 0, 0, 'from');
5162 0         0 $node014->addArrow($node016, 1, 0, 'STORE', \&collectStore);
5163 0         0 $node015->addArrow($node016, 0, 0, 'STORE', \&collectStore);
5164 0         0 $node016->addArrow($node017, 1, 0, 'decrypted');
5165 0         0 $node016->addDefault($node020);
5166 0         0 $node017->addArrow($node018, 1, 0, 'with');
5167 0         0 $node018->addArrow($node020, 1, 0, 'AESKEY', \&collectAeskey);
5168 0         0 $node020->addArrow($node021, 1, 0, 'as');
5169 0         0 $node021->addArrow($node022, 1, 0, 'FILENAME', \&collectFilename);
5170             }
5171              
5172             sub collectAeskey {
5173 0     0   0 my $o = shift;
5174 0         0 my $label = shift;
5175 0         0 my $value = shift;
5176              
5177 0         0 $o->{aesKey} = $value;
5178             }
5179              
5180             sub collectFilename {
5181 0     0   0 my $o = shift;
5182 0         0 my $label = shift;
5183 0         0 my $value = shift;
5184              
5185 0         0 $o->{filename} = $value;
5186             }
5187              
5188             sub collectHash {
5189 0     0   0 my $o = shift;
5190 0         0 my $label = shift;
5191 0         0 my $value = shift;
5192              
5193 0         0 $o->{hash} = $value;
5194 0         0 $o->{store} = $o->{actor}->preferredStore;
5195             }
5196              
5197             sub collectHash1 {
5198 0     0   0 my $o = shift;
5199 0         0 my $label = shift;
5200 0         0 my $value = shift;
5201              
5202 0         0 $o->{hash} = $value;
5203             }
5204              
5205             sub collectObject {
5206 0     0   0 my $o = shift;
5207 0         0 my $label = shift;
5208 0         0 my $value = shift;
5209              
5210 0         0 $o->{hash} = $value->hash;
5211 0         0 $o->{store} = $value->cliStore;
5212             }
5213              
5214             sub collectObject1 {
5215 0     0   0 my $o = shift;
5216 0         0 my $label = shift;
5217 0         0 my $value = shift;
5218              
5219 0         0 $o->{hash} = $value->hash;
5220 0         0 push @{$o->{stores}}, $value->store;
  0         0  
5221             }
5222              
5223             sub collectOf {
5224 0     0   0 my $o = shift;
5225 0         0 my $label = shift;
5226 0         0 my $value = shift;
5227              
5228 0         0 $o->{saveData} = 1;
5229             }
5230              
5231             sub collectSave {
5232 0     0   0 my $o = shift;
5233 0         0 my $label = shift;
5234 0         0 my $value = shift;
5235              
5236 0         0 $o->{saveObject} = 1;
5237             }
5238              
5239             sub collectStore {
5240 0     0   0 my $o = shift;
5241 0         0 my $label = shift;
5242 0         0 my $value = shift;
5243              
5244 0         0 $o->{store} = $value;
5245             }
5246              
5247             sub new {
5248 0     0   0 my $class = shift;
5249 0         0 my $actor = shift;
5250 0         0 bless {actor => $actor, ui => $actor->ui} }
5251              
5252             # END AUTOGENERATED
5253              
5254             # HTML FOLDER NAME store-get
5255             # HTML TITLE Get
5256             sub help {
5257 0     0   0 my $o = shift;
5258 0         0 my $cmd = shift;
5259              
5260 0         0 my $ui = $o->{ui};
5261 0         0 $ui->space;
5262 0         0 $ui->command('cds get OBJECT');
5263 0         0 $ui->command('cds get HASH on STORE');
5264 0         0 $ui->p('Downloads an object and writes it to STDOUT. If the object is not found, the program quits with exit code 1.');
5265 0         0 $ui->space;
5266 0         0 $ui->command('cds get HASH');
5267 0         0 $ui->p('As above, but uses the selected store.');
5268 0         0 $ui->space;
5269 0         0 $ui->command('… decrypted with AESKEY');
5270 0         0 $ui->p('Decrypts the object after retrieval.');
5271 0         0 $ui->space;
5272 0         0 $ui->command('cds save … as FILENAME');
5273 0         0 $ui->p('Saves the object to FILENAME instead of writing it to STDOUT.');
5274 0         0 $ui->space;
5275 0         0 $ui->command('cds save data of … as FILENAME');
5276 0         0 $ui->p('Saves the object\'s data to FILENAME.');
5277 0         0 $ui->space;
5278 0         0 $ui->title('Related commands');
5279 0         0 $ui->line('cds open envelope OBJECT');
5280 0         0 $ui->line('cds show record OBJECT [decrypted with AESKEY]');
5281 0         0 $ui->line('cds show hashes of OBJECT');
5282 0         0 $ui->space;
5283             }
5284              
5285             sub get {
5286 0     0   0 my $o = shift;
5287 0         0 my $cmd = shift;
5288              
5289 0         0 $cmd->collect($o);
5290              
5291             # Retrieve the object
5292 0   0     0 my $object = $o->{actor}->uiGetObject($o->{hash}, $o->{store}, $o->{actor}->preferredKeyPairToken) // return;
5293              
5294             # Decrypt
5295 0 0       0 $object = $object->crypt($o->{aesKey}) if defined $o->{aesKey};
5296              
5297             # Output
5298 0 0       0 if ($o->{saveData}) {
    0          
5299 0   0     0 CDS->writeBytesToFile($o->{filename}, $object->data) // return $o->{ui}->error('Failed to write data to "', $o->{filename}, '".');
5300 0         0 $o->{ui}->pGreen(length $object->data, ' bytes written to ', $o->{filename}, '.');
5301             } elsif ($o->{saveObject}) {
5302 0   0     0 CDS->writeBytesToFile($o->{filename}, $object->bytes) // return $o->{ui}->error('Failed to write object to "', $o->{filename}, '".');
5303 0         0 $o->{ui}->pGreen(length $object->bytes, ' bytes written to ', $o->{filename}, '.');
5304             } else {
5305 0         0 $o->{ui}->raw($object->bytes);
5306             }
5307             }
5308              
5309             # BEGIN AUTOGENERATED
5310             package CDS::Commands::Help;
5311              
5312             sub register {
5313 0     0   0 my $class = shift;
5314 0         0 my $cds = shift;
5315 0         0 my $help = shift;
5316              
5317 0         0 my $node000 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
5318 0         0 my $node001 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&version});
5319 0         0 $cds->addArrow($node000, 0, 0, '--h');
5320 0         0 $cds->addArrow($node000, 0, 0, '--help');
5321 0         0 $cds->addArrow($node000, 0, 0, '-?');
5322 0         0 $cds->addArrow($node000, 0, 0, '-h');
5323 0         0 $cds->addArrow($node000, 0, 0, '-help');
5324 0         0 $cds->addArrow($node000, 0, 0, '/?');
5325 0         0 $cds->addArrow($node000, 0, 0, '/h');
5326 0         0 $cds->addArrow($node000, 0, 0, '/help');
5327 0         0 $cds->addArrow($node001, 0, 0, '--version');
5328 0         0 $cds->addArrow($node001, 0, 0, '-version');
5329 0         0 $cds->addArrow($node001, 1, 0, 'version');
5330             }
5331              
5332             sub new {
5333 0     0   0 my $class = shift;
5334 0         0 my $actor = shift;
5335 0         0 bless {actor => $actor, ui => $actor->ui} }
5336              
5337             # END AUTOGENERATED
5338              
5339             # HTML IGNORE
5340             sub help {
5341 0     0   0 my $o = shift;
5342 0         0 my $cmd = shift;
5343              
5344 0         0 my $ui = $o->{ui};
5345 0         0 $ui->space;
5346 0         0 $ui->title('Condensation CLI');
5347 0         0 $ui->line('Version ', $CDS::VERSION, ', ', $CDS::releaseDate, ', implementing the Condensation 1 protocol');
5348 0         0 $ui->space;
5349 0         0 $ui->p('Condensation is a distributed data system with conflict-free forward merging and end-to-end security. More information is available on ', $ui->a('https://condensation.io'), '.');
5350 0         0 $ui->space;
5351 0         0 $ui->p('The command line interface (CLI) understands english-like queries like these:');
5352 0         0 $ui->pushIndent;
5353 0         0 $ui->line($ui->blue('cds show key pair'));
5354 0         0 $ui->line($ui->blue('cds create key pair thomas'));
5355 0         0 $ui->line($ui->blue('cds get 45db86549d6d2af3a45be834f2cb0e08cdbbd7699624e7bfd947a3505e6b03e5 \\'));
5356 0         0 $ui->line($ui->blue(' and decrypt with 8b8b091bbe577d5e8d38eae9cd327aa8123fe402a41ea9dd16d86f42fb70cf7e'));
5357 0         0 $ui->popIndent;
5358 0         0 $ui->space;
5359 0         0 $ui->p('If you don\'t know how to continue a command, simply put a ? to see all valid options:');
5360 0         0 $ui->pushIndent;
5361 0         0 $ui->line($ui->blue('cds ?'));
5362 0         0 $ui->line($ui->blue('cds show ?'));
5363 0         0 $ui->popIndent;
5364 0         0 $ui->space;
5365 0         0 $ui->p('To see a list of help topics, type');
5366 0         0 $ui->pushIndent;
5367 0         0 $ui->line($ui->blue('cds help ?'));
5368 0         0 $ui->popIndent;
5369 0         0 $ui->space;
5370             }
5371              
5372             sub version {
5373 0     0   0 my $o = shift;
5374 0         0 my $cmd = shift;
5375              
5376 0         0 my $ui = $o->{ui};
5377 0         0 $ui->line('Condensation CLI ', $CDS::VERSION, ', ', $CDS::releaseDate);
5378 0         0 $ui->line('implementing the Condensation 1 protocol');
5379             }
5380              
5381             # BEGIN AUTOGENERATED
5382             package CDS::Commands::List;
5383              
5384             sub register {
5385 0     0   0 my $class = shift;
5386 0         0 my $cds = shift;
5387 0         0 my $help = shift;
5388              
5389 0         0 my $node000 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
5390 0         0 my $node001 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&list});
5391 0         0 my $node002 = CDS::Parser::Node->new(0);
5392 0         0 my $node003 = CDS::Parser::Node->new(0);
5393 0         0 my $node004 = CDS::Parser::Node->new(0);
5394 0         0 my $node005 = CDS::Parser::Node->new(0);
5395 0         0 my $node006 = CDS::Parser::Node->new(0);
5396 0         0 my $node007 = CDS::Parser::Node->new(0);
5397 0         0 my $node008 = CDS::Parser::Node->new(0);
5398 0         0 my $node009 = CDS::Parser::Node->new(0);
5399 0         0 my $node010 = CDS::Parser::Node->new(0);
5400 0         0 my $node011 = CDS::Parser::Node->new(0);
5401 0         0 my $node012 = CDS::Parser::Node->new(0);
5402 0         0 my $node013 = CDS::Parser::Node->new(0);
5403 0         0 my $node014 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&listBoxes});
5404 0         0 my $node015 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&list});
5405 0         0 $cds->addArrow($node001, 1, 0, 'list');
5406 0         0 $cds->addArrow($node001, 1, 0, 'watch', \&collectWatch);
5407 0         0 $help->addArrow($node000, 1, 0, 'list');
5408 0         0 $node001->addDefault($node002);
5409 0         0 $node001->addArrow($node003, 1, 0, 'message');
5410 0         0 $node001->addArrow($node004, 1, 0, 'private');
5411 0         0 $node001->addArrow($node005, 1, 0, 'public');
5412 0         0 $node001->addArrow($node006, 0, 0, 'messages', \&collectMessages);
5413 0         0 $node001->addArrow($node006, 0, 0, 'private', \&collectPrivate);
5414 0         0 $node001->addArrow($node006, 0, 0, 'public', \&collectPublic);
5415 0         0 $node001->addArrow($node007, 1, 0, 'my', \&collectMy);
5416 0         0 $node001->addDefault($node011);
5417 0         0 $node002->addArrow($node002, 1, 0, 'BOX', \&collectBox);
5418 0         0 $node002->addArrow($node014, 1, 0, 'BOX', \&collectBox);
5419 0         0 $node003->addArrow($node006, 1, 0, 'box', \&collectMessages);
5420 0         0 $node004->addArrow($node006, 1, 0, 'box', \&collectPrivate);
5421 0         0 $node005->addArrow($node006, 1, 0, 'box', \&collectPublic);
5422 0         0 $node006->addArrow($node011, 1, 0, 'of');
5423 0         0 $node006->addDefault($node012);
5424 0         0 $node007->addArrow($node008, 1, 0, 'message');
5425 0         0 $node007->addArrow($node009, 1, 0, 'private');
5426 0         0 $node007->addArrow($node010, 1, 0, 'public');
5427 0         0 $node007->addArrow($node015, 1, 0, 'boxes');
5428 0         0 $node007->addArrow($node015, 0, 0, 'messages', \&collectMessages);
5429 0         0 $node007->addArrow($node015, 0, 0, 'private', \&collectPrivate);
5430 0         0 $node007->addArrow($node015, 0, 0, 'public', \&collectPublic);
5431 0         0 $node008->addArrow($node015, 1, 0, 'box', \&collectMessages);
5432 0         0 $node009->addArrow($node015, 1, 0, 'box', \&collectPrivate);
5433 0         0 $node010->addArrow($node015, 1, 0, 'box', \&collectPublic);
5434 0         0 $node011->addArrow($node012, 1, 0, 'ACTOR', \&collectActor);
5435 0         0 $node011->addArrow($node012, 1, 0, 'KEYPAIR', \&collectKeypair);
5436 0         0 $node011->addArrow($node015, 1, 1, 'ACCOUNT', \&collectAccount);
5437 0         0 $node011->addArrow($node015, 1, 0, 'ACTORGROUP', \&collectActorgroup);
5438 0         0 $node012->addArrow($node013, 1, 0, 'on');
5439 0         0 $node012->addDefault($node015);
5440 0         0 $node013->addArrow($node015, 1, 0, 'STORE', \&collectStore);
5441             }
5442              
5443             sub collectAccount {
5444 0     0   0 my $o = shift;
5445 0         0 my $label = shift;
5446 0         0 my $value = shift;
5447              
5448 0         0 $o->{actorHash} = $value->actorHash;
5449 0         0 $o->{store} = $value->cliStore;
5450             }
5451              
5452             sub collectActor {
5453 0     0   0 my $o = shift;
5454 0         0 my $label = shift;
5455 0         0 my $value = shift;
5456              
5457 0         0 $o->{actorHash} = $value;
5458             }
5459              
5460             sub collectActorgroup {
5461 0     0   0 my $o = shift;
5462 0         0 my $label = shift;
5463 0         0 my $value = shift;
5464              
5465 0         0 $o->{actorGroup} = $value;
5466             }
5467              
5468             sub collectBox {
5469 0     0   0 my $o = shift;
5470 0         0 my $label = shift;
5471 0         0 my $value = shift;
5472              
5473 0         0 push @{$o->{boxTokens}}, $value;
  0         0  
5474             }
5475              
5476             sub collectKeypair {
5477 0     0   0 my $o = shift;
5478 0         0 my $label = shift;
5479 0         0 my $value = shift;
5480              
5481 0         0 $o->{actorHash} = $value->keyPair->publicKey->hash;
5482 0         0 $o->{keyPairToken} = $value;
5483             }
5484              
5485             sub collectMessages {
5486 0     0   0 my $o = shift;
5487 0         0 my $label = shift;
5488 0         0 my $value = shift;
5489              
5490 0         0 $o->{boxLabels} = ['messages'];
5491             }
5492              
5493             sub collectMy {
5494 0     0   0 my $o = shift;
5495 0         0 my $label = shift;
5496 0         0 my $value = shift;
5497              
5498 0         0 $o->{my} = 1;
5499             }
5500              
5501             sub collectPrivate {
5502 0     0   0 my $o = shift;
5503 0         0 my $label = shift;
5504 0         0 my $value = shift;
5505              
5506 0         0 $o->{boxLabels} = ['private'];
5507             }
5508              
5509             sub collectPublic {
5510 0     0   0 my $o = shift;
5511 0         0 my $label = shift;
5512 0         0 my $value = shift;
5513              
5514 0         0 $o->{boxLabels} = ['public'];
5515             }
5516              
5517             sub collectStore {
5518 0     0   0 my $o = shift;
5519 0         0 my $label = shift;
5520 0         0 my $value = shift;
5521              
5522 0         0 $o->{store} = $value;
5523             }
5524              
5525             sub collectWatch {
5526 0     0   0 my $o = shift;
5527 0         0 my $label = shift;
5528 0         0 my $value = shift;
5529              
5530 0         0 $o->{watchTimeout} = 60000;
5531             }
5532              
5533             sub new {
5534 0     0   0 my $class = shift;
5535 0         0 my $actor = shift;
5536 0         0 bless {actor => $actor, ui => $actor->ui} }
5537              
5538             # END AUTOGENERATED
5539              
5540             # HTML FOLDER NAME store-list
5541             # HTML TITLE List
5542             sub help {
5543 0     0   0 my $o = shift;
5544 0         0 my $cmd = shift;
5545              
5546 0         0 my $ui = $o->{ui};
5547 0         0 $ui->space;
5548 0         0 $ui->command('cds list BOX');
5549 0         0 $ui->p('Lists the indicated box. The object references are shown as "cds open envelope …" command, which can be executed to display the corresponding envelope. Change the command to "cds get …" to download the raw object, or "cds show record …" to show it as record.');
5550 0         0 $ui->space;
5551 0         0 $ui->command('cds list');
5552 0         0 $ui->p('Lists all boxes of the selected key pair.');
5553 0         0 $ui->space;
5554 0         0 $ui->command('cds list BOXLABEL');
5555 0         0 $ui->p('Lists only the indicated box of the selected key pair. BOXLABEL may be:');
5556 0         0 $ui->line(' message box');
5557 0         0 $ui->line(' public box');
5558 0         0 $ui->line(' private box');
5559 0         0 $ui->space;
5560 0         0 $ui->command('cds list my boxes');
5561 0         0 $ui->command('cds list my BOXLABEL');
5562 0         0 $ui->p('Lists your own boxes.');
5563 0         0 $ui->space;
5564 0         0 $ui->command('cds list [BOXLABEL of] ACTORGROUP|ACCOUNT');
5565 0         0 $ui->p('Lists boxes of an actor group, or account.');
5566 0         0 $ui->space;
5567 0         0 $ui->command('cds list [BOXLABEL of] KEYPAIR|ACTOR [on STORE]');
5568 0         0 $ui->p('Lists boxes of an actor on the specified or selected store.');
5569 0         0 $ui->space;
5570             }
5571              
5572             sub listBoxes {
5573 0     0   0 my $o = shift;
5574 0         0 my $cmd = shift;
5575              
5576 0         0 $o->{boxTokens} = [];
5577 0         0 $o->{boxLabels} = ['messages', 'private', 'public'];
5578 0         0 $cmd->collect($o);
5579              
5580             # Use the selected key pair to sign requests
5581 0 0       0 $o->{keyPairToken} = $o->{actor}->preferredKeyPairToken if ! $o->{keyPairToken};
5582              
5583 0         0 for my $boxToken (@{$o->{boxTokens}}) {
  0         0  
5584 0         0 $o->listBox($boxToken);
5585             }
5586              
5587 0         0 $o->{ui}->space;
5588             }
5589              
5590             sub list {
5591 0     0   0 my $o = shift;
5592 0         0 my $cmd = shift;
5593              
5594 0         0 $o->{boxLabels} = ['messages', 'private', 'public'];
5595 0         0 $cmd->collect($o);
5596              
5597             # Actor hashes
5598 0         0 my @actorHashes;
5599             my @stores;
5600 0 0       0 if ($o->{my}) {
    0          
    0          
5601 0         0 $o->{keyPairToken} = $o->{actor}->keyPairToken;
5602 0         0 push @actorHashes, $o->{keyPairToken}->keyPair->publicKey->hash;
5603 0         0 push @stores, $o->{actor}->storageStore, $o->{actor}->messagingStore;
5604             } elsif ($o->{actorHash}) {
5605 0         0 push @actorHashes, $o->{actorHash};
5606             } elsif ($o->{actorGroup}) {
5607             # TODO
5608             } else {
5609 0         0 push @actorHashes, $o->{actor}->preferredActorHash;
5610             }
5611              
5612             # Stores
5613 0 0       0 push @stores, $o->{store} if $o->{store};
5614 0 0       0 push @stores, $o->{actor}->preferredStore if ! scalar @stores;
5615              
5616             # Use the selected key pair to sign requests
5617 0         0 my $preferredKeyPairToken = $o->{actor}->preferredKeyPairToken;
5618 0 0       0 $o->{keyPairToken} = $preferredKeyPairToken if ! $o->{keyPairToken};
5619 0 0       0 $o->{keyPairContext} = $preferredKeyPairToken->keyPair->equals($o->{keyPairToken}->keyPair) ? '' : $o->{ui}->gray(' using ', $o->{actor}->keyPairReference($o->{keyPairToken}));
5620              
5621             # List boxes
5622 0         0 for my $store (@stores) {
5623 0         0 for my $actorHash (@actorHashes) {
5624 0         0 for my $boxLabel (@{$o->{boxLabels}}) {
  0         0  
5625 0         0 $o->listBox(CDS::BoxToken->new(CDS::AccountToken->new($store, $actorHash), $boxLabel));
5626             }
5627             }
5628             }
5629              
5630 0         0 $o->{ui}->space;
5631             }
5632              
5633             sub listBox {
5634 0     0   0 my $o = shift;
5635 0         0 my $boxToken = shift;
5636              
5637 0         0 $o->{ui}->space;
5638 0         0 $o->{ui}->title($o->{actor}->blueBoxReference($boxToken));
5639              
5640             # Query the store
5641 0         0 my $store = $boxToken->accountToken->cliStore;
5642 0   0     0 my ($hashes, $storeError) = $store->list($boxToken->accountToken->actorHash, $boxToken->boxLabel, $o->{watchTimeout} // 0, $o->{keyPairToken}->keyPair);
5643 0 0       0 return if defined $storeError;
5644              
5645             # Print the result
5646 0         0 my $count = scalar @$hashes;
5647 0 0       0 return if ! $count;
5648              
5649 0 0       0 my $context = $boxToken->boxLabel eq 'messages' ? $o->{ui}->gray(' on ', $o->{actor}->storeReference($store)) : $o->{ui}->gray(' from ', $o->{actor}->accountReference($boxToken->accountToken));
5650 0 0 0     0 my $keyPairContext = $boxToken->boxLabel eq 'public' ? '' : $o->{keyPairContext} // '';
5651 0         0 foreach my $hash (sort { $a->bytes cmp $b->bytes } @$hashes) {
  0         0  
5652 0         0 $o->{ui}->line($o->{ui}->gold('cds open envelope ', $hash->hex), $context, $keyPairContext);
5653             }
5654 0 0       0 $o->{ui}->line($count.' entries') if $count > 5;
5655             }
5656              
5657             # BEGIN AUTOGENERATED
5658             package CDS::Commands::Modify;
5659              
5660             sub register {
5661 0     0   0 my $class = shift;
5662 0         0 my $cds = shift;
5663 0         0 my $help = shift;
5664              
5665 0         0 my $node000 = CDS::Parser::Node->new(0);
5666 0         0 my $node001 = CDS::Parser::Node->new(0);
5667 0         0 my $node002 = CDS::Parser::Node->new(0);
5668 0         0 my $node003 = CDS::Parser::Node->new(0);
5669 0         0 my $node004 = CDS::Parser::Node->new(0);
5670 0         0 my $node005 = CDS::Parser::Node->new(0);
5671 0         0 my $node006 = CDS::Parser::Node->new(0);
5672 0         0 my $node007 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
5673 0         0 my $node008 = CDS::Parser::Node->new(1);
5674 0         0 my $node009 = CDS::Parser::Node->new(0);
5675 0         0 my $node010 = CDS::Parser::Node->new(0);
5676 0         0 my $node011 = CDS::Parser::Node->new(0);
5677 0         0 my $node012 = CDS::Parser::Node->new(0);
5678 0         0 my $node013 = CDS::Parser::Node->new(0);
5679 0         0 my $node014 = CDS::Parser::Node->new(0);
5680 0         0 my $node015 = CDS::Parser::Node->new(0);
5681 0         0 my $node016 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&modify});
5682 0         0 $cds->addDefault($node000);
5683 0         0 $help->addArrow($node007, 1, 0, 'add');
5684 0         0 $help->addArrow($node007, 1, 0, 'purge');
5685 0         0 $help->addArrow($node007, 1, 0, 'remove');
5686 0         0 $node000->addArrow($node001, 1, 0, 'add');
5687 0         0 $node000->addArrow($node002, 1, 0, 'remove');
5688 0         0 $node000->addArrow($node003, 1, 0, 'add');
5689 0         0 $node000->addArrow($node008, 1, 0, 'purge', \&collectPurge);
5690 0         0 $node001->addArrow($node001, 1, 0, 'HASH', \&collectHash);
5691 0         0 $node001->addArrow($node004, 1, 0, 'HASH', \&collectHash);
5692 0         0 $node002->addArrow($node002, 1, 0, 'HASH', \&collectHash1);
5693 0         0 $node002->addArrow($node005, 1, 0, 'HASH', \&collectHash1);
5694 0         0 $node003->addArrow($node003, 1, 0, 'FILE', \&collectFile);
5695 0         0 $node003->addArrow($node006, 1, 0, 'FILE', \&collectFile);
5696 0         0 $node004->addArrow($node008, 1, 0, 'to');
5697 0         0 $node005->addArrow($node008, 1, 0, 'from');
5698 0         0 $node006->addArrow($node008, 1, 0, 'to');
5699 0         0 $node008->addArrow($node000, 1, 0, 'and');
5700 0         0 $node008->addArrow($node009, 1, 0, 'message');
5701 0         0 $node008->addArrow($node010, 1, 0, 'private');
5702 0         0 $node008->addArrow($node011, 1, 0, 'public');
5703 0         0 $node008->addArrow($node012, 0, 0, 'messages', \&collectMessages);
5704 0         0 $node008->addArrow($node012, 0, 0, 'private', \&collectPrivate);
5705 0         0 $node008->addArrow($node012, 0, 0, 'public', \&collectPublic);
5706 0         0 $node008->addArrow($node016, 1, 0, 'BOX', \&collectBox);
5707 0         0 $node009->addArrow($node012, 1, 0, 'box', \&collectMessages);
5708 0         0 $node010->addArrow($node012, 1, 0, 'box', \&collectPrivate);
5709 0         0 $node011->addArrow($node012, 1, 0, 'box', \&collectPublic);
5710 0         0 $node012->addArrow($node013, 1, 0, 'of');
5711 0         0 $node013->addArrow($node014, 1, 0, 'ACTOR', \&collectActor);
5712 0         0 $node013->addArrow($node014, 1, 0, 'KEYPAIR', \&collectKeypair);
5713 0         0 $node013->addArrow($node016, 1, 1, 'ACCOUNT', \&collectAccount);
5714 0         0 $node014->addArrow($node015, 1, 0, 'on');
5715 0         0 $node014->addDefault($node016);
5716 0         0 $node015->addArrow($node016, 1, 0, 'STORE', \&collectStore);
5717             }
5718              
5719             sub collectAccount {
5720 0     0   0 my $o = shift;
5721 0         0 my $label = shift;
5722 0         0 my $value = shift;
5723              
5724 0         0 $o->{boxToken} = CDS::BoxToken->new($value, $o->{boxLabel});
5725 0         0 delete $o->{boxLabel};
5726             }
5727              
5728             sub collectActor {
5729 0     0   0 my $o = shift;
5730 0         0 my $label = shift;
5731 0         0 my $value = shift;
5732              
5733 0         0 $o->{actorHash} = $value;
5734             }
5735              
5736             sub collectBox {
5737 0     0   0 my $o = shift;
5738 0         0 my $label = shift;
5739 0         0 my $value = shift;
5740              
5741 0         0 $o->{boxToken} = $value;
5742             }
5743              
5744             sub collectFile {
5745 0     0   0 my $o = shift;
5746 0         0 my $label = shift;
5747 0         0 my $value = shift;
5748              
5749 0         0 push @{$o->{fileAdditions}}, $value;
  0         0  
5750             }
5751              
5752             sub collectHash {
5753 0     0   0 my $o = shift;
5754 0         0 my $label = shift;
5755 0         0 my $value = shift;
5756              
5757 0         0 push @{$o->{additions}}, $value;
  0         0  
5758             }
5759              
5760             sub collectHash1 {
5761 0     0   0 my $o = shift;
5762 0         0 my $label = shift;
5763 0         0 my $value = shift;
5764              
5765 0         0 push @{$o->{removals}}, $value;
  0         0  
5766             }
5767              
5768             sub collectKeypair {
5769 0     0   0 my $o = shift;
5770 0         0 my $label = shift;
5771 0         0 my $value = shift;
5772              
5773 0         0 $o->{actorHash} = $value->publicKey->hash;
5774 0         0 $o->{keyPairToken} = $value;
5775             }
5776              
5777             sub collectMessages {
5778 0     0   0 my $o = shift;
5779 0         0 my $label = shift;
5780 0         0 my $value = shift;
5781              
5782 0         0 $o->{boxLabel} = 'messages';
5783             }
5784              
5785             sub collectPrivate {
5786 0     0   0 my $o = shift;
5787 0         0 my $label = shift;
5788 0         0 my $value = shift;
5789              
5790 0         0 $o->{boxLabel} = 'private';
5791             }
5792              
5793             sub collectPublic {
5794 0     0   0 my $o = shift;
5795 0         0 my $label = shift;
5796 0         0 my $value = shift;
5797              
5798 0         0 $o->{boxLabel} = 'public';
5799             }
5800              
5801             sub collectPurge {
5802 0     0   0 my $o = shift;
5803 0         0 my $label = shift;
5804 0         0 my $value = shift;
5805              
5806 0         0 $o->{purge} = 1;
5807             }
5808              
5809             sub collectStore {
5810 0     0   0 my $o = shift;
5811 0         0 my $label = shift;
5812 0         0 my $value = shift;
5813              
5814 0         0 $o->{boxToken} = CDS::BoxToken->new(CDS::AccountToken->new($value, $o->{actorHash}), $o->{boxLabel});
5815 0         0 delete $o->{boxLabel};
5816 0         0 delete $o->{actorHash};
5817             }
5818              
5819             sub new {
5820 0     0   0 my $class = shift;
5821 0         0 my $actor = shift;
5822 0         0 bless {actor => $actor, ui => $actor->ui} }
5823              
5824             # END AUTOGENERATED
5825              
5826             # HTML FOLDER NAME store-modify
5827             # HTML TITLE Modify
5828             sub help {
5829 0     0   0 my $o = shift;
5830 0         0 my $cmd = shift;
5831              
5832 0         0 my $ui = $o->{ui};
5833 0         0 $ui->space;
5834 0         0 $ui->command('cds add HASH* to BOX');
5835 0         0 $ui->p('Adds HASH to BOX.');
5836 0         0 $ui->space;
5837 0         0 $ui->command('cds add FILE* to BOX');
5838 0         0 $ui->p('Adds the envelope FILE to BOX.');
5839 0         0 $ui->space;
5840 0         0 $ui->command('cds remove HASH* from BOX');
5841 0         0 $ui->p('Removes HASH from BOX.');
5842 0         0 $ui->p('Note that the store may just mark the hash for removal, and defer its actual removal, or even cancel it. Such removals will still be reported as success.');
5843 0         0 $ui->space;
5844 0         0 $ui->command('cds purge BOX');
5845 0         0 $ui->p('Empties BOX, i.e., removes all its hashes.');
5846 0         0 $ui->space;
5847 0         0 $ui->command('… BOXLABEL of ACCOUNT');
5848 0         0 $ui->p('Modifies a box of an actor group, or account.');
5849 0         0 $ui->space;
5850 0         0 $ui->command('… BOXLABEL of KEYPAIR on STORE');
5851 0         0 $ui->command('… BOXLABEL of ACTOR on STORE');
5852 0         0 $ui->p('Modifies a box of a key pair or an actor on a specific store.');
5853 0         0 $ui->space;
5854             }
5855              
5856             sub modify {
5857 0     0   0 my $o = shift;
5858 0         0 my $cmd = shift;
5859              
5860 0         0 $o->{additions} = [];
5861 0         0 $o->{removals} = [];
5862 0         0 $cmd->collect($o);
5863              
5864             # Add a box using the selected store
5865 0 0 0     0 if ($o->{actorHash} && $o->{boxLabel}) {
5866 0         0 $o->{boxToken} = CDS::BoxToken->new(CDS::AccountToken->new($o->{actor}->preferredStore, $o->{actorHash}), $o->{boxLabel});
5867 0         0 delete $o->{actorHash};
5868 0         0 delete $o->{boxLabel};
5869             }
5870              
5871 0         0 my $store = $o->{boxToken}->accountToken->cliStore;
5872              
5873             # Prepare additions
5874 0         0 my $modifications = CDS::StoreModifications->new;
5875 0         0 for my $hash (@{$o->{additions}}) {
  0         0  
5876 0         0 $modifications->add($o->{boxToken}->accountToken->actorHash, $o->{boxToken}->boxLabel, $hash);
5877             }
5878              
5879 0         0 for my $file (@{$o->{fileAdditions}}) {
  0         0  
5880 0   0     0 my $bytes = CDS->readBytesFromFile($file) // return $o->{ui}->error('Unable to read "', $file, '".');
5881 0   0     0 my $object = CDS::Object->fromBytes($bytes) // return $o->{ui}->error('"', $file, '" is not a Condensation object.');
5882 0         0 my $hash = $object->calculateHash;
5883 0 0       0 $o->{ui}->warning('"', $file, '" is not a valid envelope. The server may reject it.') if ! $o->{actor}->isEnvelope($object);
5884 0         0 $modifications->add($o->{boxToken}->accountToken->actorHash, $o->{boxToken}->boxLabel, $hash, $object);
5885             }
5886              
5887             # Prepare removals
5888 0         0 my $boxRemovals = [];
5889 0         0 for my $hash (@{$o->{removals}}) {
  0         0  
5890 0         0 $modifications->remove($o->{boxToken}->accountToken->actorHash, $o->{boxToken}->boxLabel, $hash);
5891             }
5892              
5893             # If purging is requested, list the box
5894 0 0       0 if ($o->{purge}) {
5895 0         0 my ($hashes, $error) = $store->list($o->{boxToken}->accountToken->actorHash, $o->{boxToken}->boxLabel, 0);
5896 0 0       0 return if defined $error;
5897 0 0       0 $o->{ui}->warning('The box is empty.') if ! scalar @$hashes;
5898              
5899 0         0 for my $hash (@$hashes) {
5900 0         0 $modifications->remove($o->{boxToken}->accountToken->actorHash, $o->{boxToken}->boxLabel, $hash);
5901             }
5902             }
5903              
5904             # Cancel if there is nothing to do
5905 0 0       0 return if $modifications->isEmpty;
5906              
5907             # Modify the box
5908 0   0     0 my $keyPairToken = $o->{keyPairToken} // $o->{actor}->preferredKeyPairToken;
5909 0         0 my $error = $store->modify($modifications, $keyPairToken->keyPair);
5910 0 0       0 $o->{ui}->pGreen('Box modified.') if ! defined $error;
5911              
5912             # Print undo information
5913 0 0 0     0 if ($o->{purge} && scalar @$boxRemovals) {
5914 0         0 $o->{ui}->space;
5915 0         0 $o->{ui}->line($o->{ui}->gray('To undo purging, type:'));
5916 0         0 $o->{ui}->line($o->{ui}->gray(' cds add ', join(" \\\n ", map { $_->{hash}->hex } @$boxRemovals), " \\\n to ", $o->{actor}->boxReference($o->{boxToken})));
  0         0  
5917 0         0 $o->{ui}->space;
5918             }
5919             }
5920              
5921             # BEGIN AUTOGENERATED
5922             package CDS::Commands::OpenEnvelope;
5923              
5924             sub register {
5925 0     0   0 my $class = shift;
5926 0         0 my $cds = shift;
5927 0         0 my $help = shift;
5928              
5929 0         0 my $node000 = CDS::Parser::Node->new(0);
5930 0         0 my $node001 = CDS::Parser::Node->new(0);
5931 0         0 my $node002 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
5932 0         0 my $node003 = CDS::Parser::Node->new(1);
5933 0         0 my $node004 = CDS::Parser::Node->new(1);
5934 0         0 my $node005 = CDS::Parser::Node->new(0);
5935 0         0 my $node006 = CDS::Parser::Node->new(0);
5936 0         0 my $node007 = CDS::Parser::Node->new(1);
5937 0         0 my $node008 = CDS::Parser::Node->new(0);
5938 0         0 my $node009 = CDS::Parser::Node->new(0);
5939 0         0 my $node010 = CDS::Parser::Node->new(0);
5940 0         0 my $node011 = CDS::Parser::Node->new(1);
5941 0         0 my $node012 = CDS::Parser::Node->new(0);
5942 0         0 my $node013 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&openEnvelope});
5943 0         0 $cds->addArrow($node001, 1, 0, 'open');
5944 0         0 $help->addArrow($node000, 1, 0, 'open');
5945 0         0 $node000->addArrow($node002, 1, 0, 'envelope');
5946 0         0 $node001->addArrow($node003, 1, 0, 'envelope');
5947 0         0 $node003->addArrow($node004, 1, 0, 'HASH', \&collectHash);
5948 0         0 $node003->addArrow($node007, 1, 0, 'OBJECT', \&collectObject);
5949 0         0 $node004->addArrow($node005, 1, 0, 'from');
5950 0         0 $node004->addArrow($node006, 1, 0, 'from');
5951 0         0 $node004->addDefault($node009);
5952 0         0 $node005->addArrow($node009, 1, 0, 'ACTOR', \&collectActor);
5953 0         0 $node006->addArrow($node011, 1, 1, 'ACCOUNT', \&collectAccount);
5954 0         0 $node007->addArrow($node008, 1, 0, 'from');
5955 0         0 $node007->addDefault($node011);
5956 0         0 $node008->addArrow($node011, 1, 0, 'ACTOR', \&collectActor);
5957 0         0 $node009->addArrow($node010, 1, 0, 'on');
5958 0         0 $node009->addDefault($node011);
5959 0         0 $node010->addArrow($node011, 1, 0, 'STORE', \&collectStore);
5960 0         0 $node011->addArrow($node012, 1, 0, 'using');
5961 0         0 $node011->addDefault($node013);
5962 0         0 $node012->addArrow($node013, 1, 0, 'KEYPAIR', \&collectKeypair);
5963             }
5964              
5965             sub collectAccount {
5966 0     0   0 my $o = shift;
5967 0         0 my $label = shift;
5968 0         0 my $value = shift;
5969              
5970 0         0 $o->{senderHash} = $value->actorHash;
5971 0         0 $o->{store} = $value->cliStore;
5972             }
5973              
5974             sub collectActor {
5975 0     0   0 my $o = shift;
5976 0         0 my $label = shift;
5977 0         0 my $value = shift;
5978              
5979 0         0 $o->{senderHash} = $value;
5980             }
5981              
5982             sub collectHash {
5983 0     0   0 my $o = shift;
5984 0         0 my $label = shift;
5985 0         0 my $value = shift;
5986              
5987 0         0 $o->{hash} = $value;
5988 0         0 $o->{store} = $o->{actor}->preferredStore;
5989             }
5990              
5991             sub collectKeypair {
5992 0     0   0 my $o = shift;
5993 0         0 my $label = shift;
5994 0         0 my $value = shift;
5995              
5996 0         0 $o->{keyPairToken} = $value;
5997             }
5998              
5999             sub collectObject {
6000 0     0   0 my $o = shift;
6001 0         0 my $label = shift;
6002 0         0 my $value = shift;
6003              
6004 0         0 $o->{hash} = $value->hash;
6005 0         0 $o->{store} = $value->cliStore;
6006             }
6007              
6008             sub collectStore {
6009 0     0   0 my $o = shift;
6010 0         0 my $label = shift;
6011 0         0 my $value = shift;
6012              
6013 0         0 $o->{store} = $value;
6014             }
6015              
6016             sub new {
6017 0     0   0 my $class = shift;
6018 0         0 my $actor = shift;
6019 0         0 bless {actor => $actor, ui => $actor->ui} }
6020              
6021             # END AUTOGENERATED
6022              
6023             # HTML FOLDER NAME open-envelope
6024             # HTML TITLE Open envelope
6025             sub help {
6026 0     0   0 my $o = shift;
6027 0         0 my $cmd = shift;
6028              
6029 0         0 my $ui = $o->{ui};
6030 0         0 $ui->space;
6031 0         0 $ui->command('cds open envelope OBJECT');
6032 0         0 $ui->command('cds open envelope HASH on STORE');
6033 0         0 $ui->p('Downloads an envelope, verifies its signatures, and tries to decrypt the AES key using the selected key pair and your own key pair.');
6034 0         0 $ui->p('In addition to displaying the envelope details, this command also displays the necessary "cds show record …" command to retrieve the content.');
6035 0         0 $ui->space;
6036 0         0 $ui->command('cds open envelope HASH');
6037 0         0 $ui->p('As above, but uses the selected store.');
6038 0         0 $ui->space;
6039 0         0 $ui->command('… from ACTOR');
6040 0         0 $ui->p('Assumes that the envelope was signed by ACTOR, and downloads the corresponding public key. The sender store is assumed to be the envelope\'s store. This is useful to verify public and private envelopes.');
6041 0         0 $ui->space;
6042 0         0 $ui->command('… using KEYPAIR');
6043 0         0 $ui->p('Tries to decrypt the AES key using this key pair, instead of the selected key pair.');
6044 0         0 $ui->space;
6045             }
6046              
6047             sub openEnvelope {
6048 0     0   0 my $o = shift;
6049 0         0 my $cmd = shift;
6050              
6051 0         0 $o->{keyPairToken} = $o->{actor}->preferredKeyPairToken;
6052 0         0 $cmd->collect($o);
6053              
6054             # Get the envelope
6055 0   0     0 my $envelope = $o->{actor}->uiGetRecord($o->{hash}, $o->{store}, $o->{keyPairToken}) // return;
6056              
6057             # Continue by envelope type
6058 0         0 my $contentRecord = $envelope->child('content');
6059 0 0       0 if ($contentRecord->hashValue) {
    0          
6060 0 0       0 if ($envelope->contains('encrypted for')) {
6061 0         0 $o->processPrivateEnvelope($envelope);
6062             } else {
6063 0         0 $o->processPublicEnvelope($envelope);
6064             }
6065             } elsif (length $contentRecord->bytesValue) {
6066 0 0 0     0 if ($envelope->contains('head') && $envelope->contains('mac')) {
6067 0         0 $o->processStreamEnvelope($envelope);
6068             } else {
6069 0         0 $o->processMessageEnvelope($envelope);
6070             }
6071             } else {
6072 0         0 $o->processOther($envelope);
6073             }
6074             }
6075              
6076             sub processOther {
6077 0     0   0 my $o = shift;
6078 0 0 0     0 my $envelope = shift; die 'wrong type '.ref($envelope).' for $envelope' if defined $envelope && ref $envelope ne 'CDS::Record';
  0         0  
6079              
6080 0         0 $o->{ui}->space;
6081 0         0 $o->{ui}->pOrange('This is not an envelope. Envelopes always have a "content" section. The raw record is shown below.');
6082 0         0 $o->{ui}->space;
6083 0         0 $o->{ui}->title('Record');
6084 0         0 $o->{ui}->recordChildren($envelope, $o->{actor}->storeReference($o->{store}));
6085 0         0 $o->{ui}->space;
6086             }
6087              
6088             sub processPublicEnvelope {
6089 0     0   0 my $o = shift;
6090 0 0 0     0 my $envelope = shift; die 'wrong type '.ref($envelope).' for $envelope' if defined $envelope && ref $envelope ne 'CDS::Record';
  0         0  
6091              
6092 0         0 $o->{ui}->space;
6093 0         0 $o->{ui}->title('Public envelope');
6094 0         0 $o->{ui}->line($o->{ui}->gold('cds show record ', $o->{hash}->hex, ' on ', $o->{actor}->storeReference($o->{store})));
6095              
6096 0         0 my $contentHash = $envelope->child('content')->hashValue;
6097 0         0 $o->showPublicPrivateSignature($envelope, $contentHash);
6098              
6099 0         0 $o->{ui}->space;
6100 0         0 $o->{ui}->title('Content');
6101 0         0 $o->{ui}->line($o->{ui}->gold('cds show record ', $contentHash->hex, ' on ', $o->{actor}->storeReference($o->{store})));
6102              
6103 0         0 $o->{ui}->space;
6104             }
6105              
6106             sub processPrivateEnvelope {
6107 0     0   0 my $o = shift;
6108 0 0 0     0 my $envelope = shift; die 'wrong type '.ref($envelope).' for $envelope' if defined $envelope && ref $envelope ne 'CDS::Record';
  0         0  
6109              
6110 0         0 $o->{ui}->space;
6111 0         0 $o->{ui}->title('Private envelope');
6112 0         0 $o->{ui}->line($o->{ui}->gold('cds show record ', $o->{hash}->hex, ' on ', $o->{actor}->storeReference($o->{store})));
6113              
6114 0         0 my $aesKey = $o->decryptAesKey($envelope);
6115 0         0 my $contentHash = $envelope->child('content')->hashValue;
6116 0         0 $o->showPublicPrivateSignature($envelope, $contentHash);
6117 0         0 $o->showEncryptedFor($envelope);
6118              
6119 0         0 $o->{ui}->space;
6120 0 0       0 if ($aesKey) {
6121 0         0 $o->{ui}->title('Content');
6122 0         0 $o->{ui}->line($o->{ui}->gold('cds show record ', $contentHash->hex, ' on ', $o->{actor}->storeReference($o->{store}), ' decrypted with ', unpack('H*', $aesKey)));
6123             } else {
6124 0         0 $o->{ui}->title('Encrypted content');
6125 0         0 $o->{ui}->line($o->{ui}->gold('cds get ', $contentHash->hex, ' on ', $o->{actor}->storeReference($o->{store})));
6126             }
6127              
6128 0         0 $o->{ui}->space;
6129             }
6130              
6131             sub showPublicPrivateSignature {
6132 0     0   0 my $o = shift;
6133 0 0 0     0 my $envelope = shift; die 'wrong type '.ref($envelope).' for $envelope' if defined $envelope && ref $envelope ne 'CDS::Record';
  0         0  
6134 0 0 0     0 my $contentHash = shift; die 'wrong type '.ref($contentHash).' for $contentHash' if defined $contentHash && ref $contentHash ne 'CDS::Hash';
  0         0  
6135              
6136 0         0 $o->{ui}->space;
6137 0         0 $o->{ui}->title('Signed by');
6138 0 0       0 if ($o->{senderHash}) {
6139 0         0 my $accountToken = CDS::AccountToken->new($o->{store}, $o->{senderHash});
6140 0         0 $o->{ui}->line($o->{actor}->blueAccountReference($accountToken));
6141 0         0 $o->showSignature($envelope, $o->{senderHash}, $o->{store}, $contentHash);
6142             } else {
6143 0         0 $o->{ui}->p('The signer is not known. To verify the signature of a public or private envelope, you need to indicate the account on which it was found:');
6144 0         0 $o->{ui}->line($o->{ui}->gold(' cds show envelope ', $o->{hash}->hex, ' from ', $o->{ui}->underlined('ACTOR'), ' on ', $o->{actor}->storeReference($o->{store})));
6145             }
6146             }
6147              
6148             sub processMessageEnvelope {
6149 0     0   0 my $o = shift;
6150 0 0 0     0 my $envelope = shift; die 'wrong type '.ref($envelope).' for $envelope' if defined $envelope && ref $envelope ne 'CDS::Record';
  0         0  
6151              
6152 0         0 $o->{ui}->space;
6153 0         0 $o->{ui}->title('Message envelope');
6154 0         0 $o->{ui}->line($o->{ui}->gold('cds show record ', $o->{hash}->hex, ' on ', $o->{actor}->storeReference($o->{store})));
6155              
6156             # Decrypt
6157 0         0 my $encryptedContentBytes = $envelope->child('content')->bytesValue;
6158 0         0 my $aesKey = $o->decryptAesKey($envelope);
6159 0 0       0 if (! $aesKey) {
6160 0         0 $o->{ui}->space;
6161 0         0 $o->{ui}->title('Encrypted content');
6162 0         0 $o->{ui}->line(length $encryptedContentBytes, ' bytes');
6163 0         0 return $o->processMessageEnvelope2($envelope);
6164             }
6165              
6166 0         0 my $contentObject = CDS::Object->fromBytes(CDS::C::aesCrypt($encryptedContentBytes, $aesKey, CDS->zeroCTR));
6167 0 0       0 if (! $contentObject) {
6168 0         0 $o->{ui}->pRed('The embedded content object is invalid, or the AES key (', unpack('H*', $aesKey), ') is wrong.');
6169 0         0 return $o->processMessageEnvelope2($envelope);
6170             }
6171              
6172             #my $signedHash = $contentObject->calculateHash; # before 2020-05-05
6173 0         0 my $signedHash = CDS::Hash->calculateFor($encryptedContentBytes);
6174 0         0 my $content = CDS::Record->fromObject($contentObject);
6175 0 0       0 if (! $content) {
6176 0         0 $o->{ui}->pRed('The embedded content object does not contain a record, or the AES key (', unpack('H*', $aesKey), ') is wrong.');
6177 0         0 return $o->processMessageEnvelope2($envelope);
6178             }
6179              
6180             # Sender hash
6181 0         0 my $senderHash = $content->child('sender')->hashValue;
6182 0 0       0 $o->{ui}->pRed('The content object is missing the sender.') if ! $senderHash;
6183              
6184             # Sender store
6185 0         0 my $senderStoreRecord = $content->child('store');
6186 0         0 my $senderStoreBytes = $senderStoreRecord->bytesValue;
6187 0         0 my $mentionsSenderStore = length $senderStoreBytes;
6188 0 0       0 $o->{ui}->pRed('The content object is missing the sender\'s store.') if ! $mentionsSenderStore;
6189 0 0       0 my $senderStore = scalar $mentionsSenderStore ? $o->{actor}->storeForUrl($senderStoreRecord->textValue) : undef;
6190              
6191             # Sender
6192 0         0 $o->{ui}->space;
6193 0         0 $o->{ui}->title('Signed by');
6194 0 0 0     0 if ($senderHash && $senderStore) {
    0          
    0          
    0          
6195 0         0 my $senderToken = CDS::AccountToken->new($senderStore, $senderHash);
6196 0         0 $o->{ui}->line($o->{actor}->blueAccountReference($senderToken));
6197 0         0 $o->showSignature($envelope, $senderHash, $senderStore, $signedHash);
6198             } elsif ($senderHash) {
6199 0   0     0 my $actorLabel = $o->{actor}->actorLabel($senderHash) // $senderHash->hex;
6200 0 0       0 if ($mentionsSenderStore) {
6201 0         0 $o->{ui}->line($actorLabel, ' on ', $o->{ui}->red($o->{ui}->niceBytes($senderStoreBytes, 64)));
6202             } else {
6203 0         0 $o->{ui}->line($actorLabel);
6204             }
6205 0         0 $o->{ui}->pOrange('The signature cannot be verified, because the signer\'s store is not known.');
6206             } elsif ($senderStore) {
6207 0         0 $o->{ui}->line($o->{ui}->red('?'), ' on ', $o->{actor}->storeReference($senderStore));
6208 0         0 $o->{ui}->pOrange('The signature cannot be verified, because the signer is not known.');
6209             } elsif ($mentionsSenderStore) {
6210 0         0 $o->{ui}->line($o->{ui}->red('?'), ' on ', $o->{ui}->red($o->{ui}->niceBytes($senderStoreBytes, 64)));
6211 0         0 $o->{ui}->pOrange('The signature cannot be verified, because the signer is not known.');
6212             } else {
6213 0         0 $o->{ui}->pOrange('The signature cannot be verified, because the signer is not known.');
6214             }
6215              
6216             # Content
6217 0         0 $o->{ui}->space;
6218 0         0 $o->{ui}->title('Content');
6219 0 0       0 $o->{ui}->recordChildren($content, $senderStore ? $o->{actor}->storeReference($senderStore) : undef);
6220              
6221 0         0 return $o->processMessageEnvelope2($envelope);
6222             }
6223              
6224             sub processMessageEnvelope2 {
6225 0     0   0 my $o = shift;
6226 0 0 0     0 my $envelope = shift; die 'wrong type '.ref($envelope).' for $envelope' if defined $envelope && ref $envelope ne 'CDS::Record';
  0         0  
6227              
6228             # Encrypted for
6229 0         0 $o->showEncryptedFor($envelope);
6230              
6231             # Updated by
6232 0         0 $o->{ui}->space;
6233 0         0 $o->{ui}->title('May be removed or updated by');
6234              
6235 0         0 for my $child ($envelope->child('updated by')->children) {
6236 0         0 $o->showActorHash24($child->bytes);
6237             }
6238              
6239             # Expires
6240 0         0 $o->{ui}->space;
6241 0         0 $o->{ui}->title('Expires');
6242 0         0 my $expires = $envelope->child('expires')->integerValue;
6243 0 0       0 $o->{ui}->line($expires ? $o->{ui}->niceDateTime($expires) : $o->{ui}->gray('never'));
6244 0         0 $o->{ui}->space;
6245             }
6246              
6247             sub processStreamHead {
6248 0     0   0 my $o = shift;
6249 0         0 my $head = shift;
6250              
6251 0         0 $o->{ui}->space;
6252 0         0 $o->{ui}->title('Stream head');
6253 0 0       0 return $o->{ui}->pRed('The envelope does not mention a stream head.') if ! $head;
6254 0         0 $o->{ui}->line($o->{ui}->gold('cds open envelope ', $head->hex, ' on ', $o->{actor}->storeReference($o->{store})));
6255              
6256             # Get the envelope
6257 0   0     0 my $envelope = $o->{actor}->uiGetRecord($head, $o->{store}, $o->{keyPairToken}) // return;
6258              
6259             # Decrypt the content
6260 0         0 my $encryptedContentBytes = $envelope->child('content')->bytesValue;
6261 0   0     0 my $aesKey = $o->decryptAesKey($envelope) // return;
6262 0   0     0 my $contentObject = CDS::Object->fromBytes(CDS::C::aesCrypt($encryptedContentBytes, $aesKey, CDS->zeroCTR)) // return {aesKey => $aesKey};
6263 0         0 my $signedHash = CDS::Hash->calculateFor($encryptedContentBytes);
6264 0   0     0 my $content = CDS::Record->fromObject($contentObject) // return {aesKey => $aesKey};
6265              
6266             # Sender
6267 0         0 my $senderHash = $content->child('sender')->hashValue;
6268 0         0 my $senderStoreRecord = $content->child('store');
6269 0         0 my $senderStore = $o->{actor}->storeForUrl($senderStoreRecord->textValue);
6270 0 0 0     0 return {aesKey => $aesKey, senderHash => $senderHash, senderStore => $senderStore} if ! $senderHash || ! $senderStore;
6271              
6272 0         0 $o->{ui}->pushIndent;
6273 0         0 $o->{ui}->space;
6274 0         0 $o->{ui}->title('Signed by');
6275 0         0 my $senderToken = CDS::AccountToken->new($senderStore, $senderHash);
6276 0         0 $o->{ui}->line($o->{actor}->blueAccountReference($senderToken));
6277 0         0 $o->showSignature($envelope, $senderHash, $senderStore, $signedHash);
6278              
6279             # Recipients
6280 0         0 $o->{ui}->space;
6281 0         0 $o->{ui}->title('Encrypted for');
6282 0         0 for my $child ($envelope->child('encrypted for')->children) {
6283 0         0 $o->showActorHash24($child->bytes);
6284             }
6285              
6286 0         0 $o->{ui}->popIndent;
6287 0         0 return {aesKey => $aesKey, senderHash => $senderHash, senderStore => $senderStore, isValid => 1};
6288             }
6289              
6290             sub processStreamEnvelope {
6291 0     0   0 my $o = shift;
6292 0 0 0     0 my $envelope = shift; die 'wrong type '.ref($envelope).' for $envelope' if defined $envelope && ref $envelope ne 'CDS::Record';
  0         0  
6293              
6294 0         0 $o->{ui}->space;
6295 0         0 $o->{ui}->title('Stream envelope');
6296 0         0 $o->{ui}->line($o->{ui}->gold('cds show record ', $o->{hash}->hex, ' on ', $o->{actor}->storeReference($o->{store})));
6297              
6298             # Get the head
6299 0         0 my $streamHead = $o->processStreamHead($envelope->child('head')->hashValue);
6300 0 0 0     0 $o->{ui}->pRed('The stream head cannot be opened. Open the stream head envelope for details.') if ! $streamHead || ! $streamHead->{isValid};
6301              
6302             # Get the content
6303 0         0 my $encryptedBytes = $envelope->child('content')->bytesValue;
6304              
6305             # Get the CTR
6306 0         0 $o->{ui}->space;
6307 0         0 $o->{ui}->title('CTR');
6308 0         0 my $ctr = $envelope->child('ctr')->bytesValue;
6309 0 0       0 if (length $ctr == 16) {
6310 0         0 $o->{ui}->line(unpack('H*', $ctr));
6311             } else {
6312 0         0 $o->{ui}->pRed('The CTR value is invalid.');
6313             }
6314              
6315 0 0       0 return $o->{ui}->space if ! $streamHead;
6316 0 0       0 return $o->{ui}->space if ! $streamHead->{aesKey};
6317              
6318             # Get and verify the MAC
6319 0         0 $o->{ui}->space;
6320 0         0 $o->{ui}->title('Message authentication (MAC)');
6321 0         0 my $mac = $envelope->child('mac')->bytesValue;
6322 0         0 my $signedHash = CDS::Hash->calculateFor($encryptedBytes);
6323 0         0 my $expectedMac = CDS::C::aesCrypt($signedHash->bytes, $streamHead->{aesKey}, $ctr);
6324 0 0       0 if ($mac eq $expectedMac) {
6325 0         0 $o->{ui}->pGreen('The MAC valid.');
6326             } else {
6327 0         0 $o->{ui}->pRed('The MAC is invalid.');
6328             }
6329              
6330             # Decrypt the content
6331 0         0 $o->{ui}->space;
6332 0         0 $o->{ui}->title('Content');
6333 0         0 my $contentObject = CDS::Object->fromBytes(CDS::C::aesCrypt($encryptedBytes, $streamHead->{aesKey}, CDS::C::counterPlusInt($ctr, 2)));
6334 0 0       0 if (! $contentObject) {
6335 0         0 $o->{ui}->pRed('The embedded content object is invalid, or the provided AES key (', unpack('H*', $streamHead->{aesKey}), ') is wrong.') ;
6336 0         0 $o->{ui}->space;
6337 0         0 return;
6338             }
6339              
6340 0         0 my $content = CDS::Record->fromObject($contentObject);
6341 0 0       0 return $o->{ui}->pRed('The content is not a record.') if ! $content;
6342 0 0       0 $o->{ui}->recordChildren($content, $streamHead->{senderStore} ? $o->{actor}->storeReference($streamHead->{senderStore}) : undef);
6343 0         0 $o->{ui}->space;
6344              
6345             # The envelope is valid
6346             #my $source = CDS::Source->new($o->{pool}->{keyPair}, $o->{actorOnStore}, 'messages', $entry->{hash});
6347             #return CDS::ReceivedMessage->new($o, $entry, $source, $envelope, $streamHead->senderStoreUrl, $streamHead->sender, $content, $streamHead);
6348              
6349             }
6350              
6351             sub showActorHash24 {
6352 0     0   0 my $o = shift;
6353 0         0 my $actorHashBytes = shift;
6354              
6355 0         0 my $actorHashHex = unpack('H*', $actorHashBytes);
6356 0 0       0 return $o->{ui}->line($o->{ui}->red($actorHashHex, ' (', length $actorHashBytes, ' instead of 24 bytes)')) if length $actorHashBytes != 24;
6357              
6358 0         0 my $actorName = $o->{actor}->actorLabelByHashStartBytes($actorHashBytes);
6359 0         0 $actorHashHex .= '·' x 16;
6360              
6361 0         0 my $keyPairHashBytes = $o->{keyPairToken}->keyPair->publicKey->hash->bytes;
6362 0         0 my $isMe = substr($keyPairHashBytes, 0, 24) eq $actorHashBytes;
6363 0 0       0 $o->{ui}->line($isMe ? $o->{ui}->violet($actorHashHex) : $actorHashHex, (defined $actorName ? $o->{ui}->blue(' '.$actorName) : ''));
    0          
6364 0         0 return $isMe;
6365             }
6366              
6367             sub showSignature {
6368 0     0   0 my $o = shift;
6369 0 0 0     0 my $envelope = shift; die 'wrong type '.ref($envelope).' for $envelope' if defined $envelope && ref $envelope ne 'CDS::Record';
  0         0  
6370 0 0 0     0 my $senderHash = shift; die 'wrong type '.ref($senderHash).' for $senderHash' if defined $senderHash && ref $senderHash ne 'CDS::Hash';
  0         0  
6371 0         0 my $senderStore = shift;
6372 0 0 0     0 my $signedHash = shift; die 'wrong type '.ref($signedHash).' for $signedHash' if defined $signedHash && ref $signedHash ne 'CDS::Hash';
  0         0  
6373              
6374             # Get the public key
6375 0         0 my $publicKey = $o->getPublicKey($senderHash, $senderStore);
6376 0 0       0 return $o->{ui}->line($o->{ui}->orange('The signature cannot be verified, because the signer\'s public key is not available.')) if ! $publicKey;
6377              
6378             # Verify the signature
6379 0 0       0 if (CDS->verifyEnvelopeSignature($envelope, $publicKey, $signedHash)) {
6380 0         0 $o->{ui}->pGreen('The signature is valid.');
6381             } else {
6382 0         0 $o->{ui}->pRed('The signature is not valid.');
6383             }
6384             }
6385              
6386             sub getPublicKey {
6387 0     0   0 my $o = shift;
6388 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
6389 0         0 my $store = shift;
6390              
6391 0 0       0 return $o->{keyPairToken}->keyPair->publicKey if $hash->equals($o->{keyPairToken}->keyPair->publicKey->hash);
6392 0         0 return $o->{actor}->uiGetPublicKey($hash, $store, $o->{keyPairToken});
6393             }
6394              
6395             sub showEncryptedFor {
6396 0     0   0 my $o = shift;
6397 0 0 0     0 my $envelope = shift; die 'wrong type '.ref($envelope).' for $envelope' if defined $envelope && ref $envelope ne 'CDS::Record';
  0         0  
6398              
6399 0         0 $o->{ui}->space;
6400 0         0 $o->{ui}->title('Encrypted for');
6401              
6402 0         0 my $canDecrypt = 0;
6403 0         0 for my $child ($envelope->child('encrypted for')->children) {
6404 0 0       0 $canDecrypt = 1 if $o->showActorHash24($child->bytes);
6405             }
6406              
6407 0 0       0 return if $canDecrypt;
6408 0         0 $o->{ui}->space;
6409 0         0 my $keyPairHash = $o->{keyPairToken}->keyPair->publicKey->hash;
6410 0         0 $o->{ui}->pOrange('This envelope is not encrypted for you (', $keyPairHash->shortHex, '). If you possess one of the keypairs mentioned above, add "… using KEYPAIR" to open this envelope.');
6411             }
6412              
6413             sub decryptAesKey {
6414 0     0   0 my $o = shift;
6415 0 0 0     0 my $envelope = shift; die 'wrong type '.ref($envelope).' for $envelope' if defined $envelope && ref $envelope ne 'CDS::Record';
  0         0  
6416              
6417 0         0 my $keyPair = $o->{keyPairToken}->keyPair;
6418 0         0 my $hashBytes24 = substr($keyPair->publicKey->hash->bytes, 0, 24);
6419 0         0 my $child = $envelope->child('encrypted for')->child($hashBytes24);
6420              
6421 0         0 my $encryptedAesKey = $child->bytesValue;
6422 0 0       0 return if ! length $encryptedAesKey;
6423              
6424 0         0 my $aesKey = $keyPair->decrypt($encryptedAesKey);
6425 0 0 0     0 return $aesKey if defined $aesKey && length $aesKey == 32;
6426              
6427 0         0 $o->{ui}->pRed('The AES key failed to decrypt. It either wasn\'t encrypted properly, or the encryption was performed with the wrong public key.');
6428 0         0 return;
6429             }
6430              
6431             # BEGIN AUTOGENERATED
6432             package CDS::Commands::Put;
6433              
6434             sub register {
6435 0     0   0 my $class = shift;
6436 0         0 my $cds = shift;
6437 0         0 my $help = shift;
6438              
6439 0         0 my $node000 = CDS::Parser::Node->new(0);
6440 0         0 my $node001 = CDS::Parser::Node->new(0);
6441 0         0 my $node002 = CDS::Parser::Node->new(0);
6442 0         0 my $node003 = CDS::Parser::Node->new(0);
6443 0         0 my $node004 = CDS::Parser::Node->new(0);
6444 0         0 my $node005 = CDS::Parser::Node->new(0);
6445 0         0 my $node006 = CDS::Parser::Node->new(0);
6446 0         0 my $node007 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
6447 0         0 my $node008 = CDS::Parser::Node->new(0);
6448 0         0 my $node009 = CDS::Parser::Node->new(0);
6449 0         0 my $node010 = CDS::Parser::Node->new(0);
6450 0         0 my $node011 = CDS::Parser::Node->new(0);
6451 0         0 my $node012 = CDS::Parser::Node->new(1);
6452 0         0 my $node013 = CDS::Parser::Node->new(0);
6453 0         0 my $node014 = CDS::Parser::Node->new(0);
6454 0         0 my $node015 = CDS::Parser::Node->new(0);
6455 0         0 my $node016 = CDS::Parser::Node->new(0);
6456 0         0 my $node017 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&put});
6457 0         0 $cds->addArrow($node000, 1, 0, 'put');
6458 0         0 $cds->addArrow($node001, 1, 0, 'put');
6459 0         0 $cds->addArrow($node002, 1, 0, 'put');
6460 0         0 $help->addArrow($node007, 1, 0, 'put');
6461 0         0 $node000->addArrow($node012, 1, 0, 'OBJECTFILE', \&collectObjectfile);
6462 0         0 $node001->addArrow($node003, 1, 0, 'object');
6463 0         0 $node002->addArrow($node004, 1, 0, 'public');
6464 0         0 $node003->addArrow($node008, 1, 0, 'with');
6465 0         0 $node004->addArrow($node005, 1, 0, 'key');
6466 0         0 $node005->addArrow($node006, 1, 0, 'of');
6467 0         0 $node006->addArrow($node012, 1, 0, 'KEYPAIR', \&collectKeypair);
6468 0         0 $node008->addDefault($node009);
6469 0         0 $node008->addDefault($node011);
6470 0         0 $node009->addArrow($node009, 1, 0, 'HASH', \&collectHash);
6471 0         0 $node009->addArrow($node010, 1, 0, 'HASH', \&collectHash);
6472 0         0 $node010->addArrow($node011, 1, 0, 'and');
6473 0         0 $node011->addArrow($node012, 1, 0, 'FILE', \&collectFile);
6474 0         0 $node012->addArrow($node013, 1, 0, 'encrypted');
6475 0         0 $node012->addDefault($node015);
6476 0         0 $node013->addArrow($node014, 1, 0, 'with');
6477 0         0 $node014->addArrow($node015, 1, 0, 'AESKEY', \&collectAeskey);
6478 0         0 $node015->addArrow($node016, 1, 0, 'onto');
6479 0         0 $node015->addDefault($node017);
6480 0         0 $node016->addArrow($node016, 1, 0, 'STORE', \&collectStore);
6481 0         0 $node016->addArrow($node017, 1, 0, 'STORE', \&collectStore);
6482             }
6483              
6484             sub collectAeskey {
6485 0     0   0 my $o = shift;
6486 0         0 my $label = shift;
6487 0         0 my $value = shift;
6488              
6489 0         0 $o->{aesKey} = $value;
6490             }
6491              
6492             sub collectFile {
6493 0     0   0 my $o = shift;
6494 0         0 my $label = shift;
6495 0         0 my $value = shift;
6496              
6497 0         0 $o->{dataFile} = $value;
6498             }
6499              
6500             sub collectHash {
6501 0     0   0 my $o = shift;
6502 0         0 my $label = shift;
6503 0         0 my $value = shift;
6504              
6505 0         0 push @{$o->{hashes}}, $value;
  0         0  
6506             }
6507              
6508             sub collectKeypair {
6509 0     0   0 my $o = shift;
6510 0         0 my $label = shift;
6511 0         0 my $value = shift;
6512              
6513 0         0 $o->{object} = $value->keyPair->publicKey->object;
6514             }
6515              
6516             sub collectObjectfile {
6517 0     0   0 my $o = shift;
6518 0         0 my $label = shift;
6519 0         0 my $value = shift;
6520              
6521 0         0 $o->{objectFile} = $value;
6522             }
6523              
6524             sub collectStore {
6525 0     0   0 my $o = shift;
6526 0         0 my $label = shift;
6527 0         0 my $value = shift;
6528              
6529 0         0 push @{$o->{stores}}, $value;
  0         0  
6530             }
6531              
6532             sub new {
6533 0     0   0 my $class = shift;
6534 0         0 my $actor = shift;
6535 0         0 bless {actor => $actor, ui => $actor->ui} }
6536              
6537             # END AUTOGENERATED
6538              
6539             # HTML FOLDER NAME store-put
6540             # HTML TITLE Put
6541             sub help {
6542 0     0   0 my $o = shift;
6543 0         0 my $cmd = shift;
6544              
6545 0         0 my $ui = $o->{ui};
6546 0         0 $ui->space;
6547 0         0 $ui->command('cds put FILE* [onto STORE*]');
6548 0         0 $ui->p('Uploads object files onto object stores. If no stores are provided, the selected store is used. If an upload fails, the program immediately quits with exit code 1.');
6549 0         0 $ui->space;
6550 0         0 $ui->command('cds put FILE encrypted with AESKEY [onto STORE*]');
6551 0         0 $ui->p('Encrypts the object before the upload.');
6552 0         0 $ui->space;
6553 0         0 $ui->command('cds put object with [HASH* and] FILE …');
6554 0         0 $ui->p('Creates an object with the HASHes as hash list and FILE as data.');
6555 0         0 $ui->space;
6556 0         0 $ui->command('cds put public key of KEYPAIR …');
6557 0         0 $ui->p('Uploads the public key of the indicated key pair onto the store.');
6558 0         0 $ui->space;
6559             }
6560              
6561             sub put {
6562 0     0   0 my $o = shift;
6563 0         0 my $cmd = shift;
6564              
6565 0         0 $o->{hashes} = [];
6566 0         0 $o->{stores} = [];
6567 0         0 $cmd->collect($o);
6568              
6569             # Stores
6570 0 0       0 push @{$o->{stores}}, $o->{actor}->preferredStore if ! scalar @{$o->{stores}};
  0         0  
  0         0  
6571              
6572 0         0 $o->{get} = [];
6573 0 0       0 return $o->putObject($o->{object}) if $o->{object};
6574 0 0       0 return $o->putObjectFile if $o->{objectFile};
6575 0         0 $o->putConstructedFile;
6576             }
6577              
6578             sub putObjectFile {
6579 0     0   0 my $o = shift;
6580              
6581 0         0 my $object = $o->{objectFile}->object;
6582              
6583             # Display object information
6584 0         0 $o->{ui}->space;
6585 0         0 $o->{ui}->title('Uploading ', $o->{objectFile}->file, ' ', $o->{ui}->gray($o->{ui}->niceFileSize($object->byteLength)));
6586 0 0       0 $o->{ui}->line($object->hashesCount == 1 ? '1 hash' : $object->hashesCount.' hashes');
6587 0         0 $o->{ui}->line($o->{ui}->niceFileSize(length $object->data).' data');
6588 0         0 $o->{ui}->space;
6589              
6590             # Upload
6591 0         0 $o->putObject($object);
6592             }
6593              
6594             sub putConstructedFile {
6595 0     0   0 my $o = shift;
6596              
6597             # Create the object
6598 0   0     0 my $data = CDS->readBytesFromFile($o->{dataFile}) // return $o->{ui}->error('Unable to read "', $o->{dataFile}, '".');
6599 0         0 my $header = pack('L>', scalar @{$o->{hashes}}) . join('', map { $_->bytes } @{$o->{hashes}});
  0         0  
  0         0  
  0         0  
6600 0         0 my $object = CDS::Object->create($header, $data);
6601              
6602             # Display object information
6603 0         0 $o->{ui}->space;
6604 0         0 $o->{ui}->title('Uploading new object ', $o->{ui}->gray($o->{ui}->niceFileSize(length $object->bytes)));
6605 0 0       0 $o->{ui}->line($object->hashesCount == 1 ? '1 hash' : $object->hashesCount.' hashes');
6606 0         0 $o->{ui}->line($o->{ui}->niceFileSize(length $object->data).' data from ', $o->{dataFile});
6607 0         0 $o->{ui}->space;
6608              
6609             # Upload
6610 0         0 $o->putObject($object);
6611             }
6612              
6613             sub putObject {
6614 0     0   0 my $o = shift;
6615 0 0 0     0 my $object = shift; die 'wrong type '.ref($object).' for $object' if defined $object && ref $object ne 'CDS::Object';
  0         0  
6616              
6617 0         0 my $keyPair = $o->{actor}->preferredKeyPairToken->keyPair;
6618              
6619             # Encrypt it if desired
6620 0         0 my $objectBytes;
6621 0 0       0 if (defined $o->{aesKey}) {
6622 0         0 $object = $object->crypt($o->{aesKey});
6623 0         0 unshift @{$o->{get}}, ' decrypted with ', unpack('H*', $o->{aesKey}), ' ';
  0         0  
6624             }
6625              
6626             # Calculate the hash
6627 0         0 my $hash = $object->calculateHash;
6628              
6629             # Upload the object
6630 0         0 my $successfulStore;
6631 0         0 for my $store (@{$o->{stores}}) {
  0         0  
6632 0         0 my $error = $store->put($hash, $object, $keyPair);
6633 0 0       0 next if $error;
6634 0         0 $o->{ui}->pGreen('The object was uploaded onto ', $store->url, '.');
6635 0         0 $successfulStore = $store;
6636             }
6637              
6638             # Show the corresponding download line
6639 0 0       0 return if ! $successfulStore;
6640 0         0 $o->{ui}->space;
6641 0         0 $o->{ui}->line('To download the object, type:');
6642 0         0 $o->{ui}->line($o->{ui}->gold('cds get ', $hash->hex), $o->{ui}->gray(' on ', $successfulStore->url, @{$o->{get}}));
  0         0  
6643 0         0 $o->{ui}->space;
6644             }
6645              
6646             package CDS::Commands::Remember;
6647              
6648             # BEGIN AUTOGENERATED
6649              
6650             sub register {
6651 0     0   0 my $class = shift;
6652 0         0 my $cds = shift;
6653 0         0 my $help = shift;
6654              
6655 0         0 my $node000 = CDS::Parser::Node->new(0, {constructor => \&new, function => \&showLabels});
6656 0         0 my $node001 = CDS::Parser::Node->new(0);
6657 0         0 my $node002 = CDS::Parser::Node->new(0);
6658 0         0 my $node003 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
6659 0         0 my $node004 = CDS::Parser::Node->new(0);
6660 0         0 my $node005 = CDS::Parser::Node->new(0);
6661 0         0 my $node006 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&forget});
6662 0         0 my $node007 = CDS::Parser::Node->new(1);
6663 0         0 my $node008 = CDS::Parser::Node->new(0);
6664 0         0 my $node009 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&remember});
6665 0         0 $cds->addArrow($node000, 1, 0, 'remember');
6666 0         0 $cds->addArrow($node001, 1, 0, 'forget');
6667 0         0 $help->addArrow($node003, 1, 0, 'forget');
6668 0         0 $help->addArrow($node003, 1, 0, 'remember');
6669 0         0 $node000->addArrow($node004, 1, 0, 'ACTOR', \&collectActor);
6670 0         0 $node000->addArrow($node007, 1, 1, 'ACCOUNT', \&collectAccount);
6671 0         0 $node000->addArrow($node007, 1, 0, 'ACTOR', \&collectActor);
6672 0         0 $node000->addArrow($node007, 1, 0, 'KEYPAIR', \&collectKeypair);
6673 0         0 $node000->addArrow($node007, 1, 0, 'STORE', \&collectStore);
6674 0         0 $node001->addDefault($node002);
6675 0         0 $node002->addArrow($node002, 1, 0, 'LABEL', \&collectLabel);
6676 0         0 $node002->addArrow($node006, 1, 0, 'LABEL', \&collectLabel);
6677 0         0 $node004->addArrow($node005, 1, 0, 'on');
6678 0         0 $node005->addArrow($node007, 1, 0, 'STORE', \&collectStore);
6679 0         0 $node007->addArrow($node008, 1, 0, 'as');
6680 0         0 $node008->addArrow($node009, 1, 0, 'TEXT', \&collectText);
6681             }
6682              
6683             sub collectAccount {
6684 0     0   0 my $o = shift;
6685 0         0 my $label = shift;
6686 0         0 my $value = shift;
6687              
6688 0         0 $o->{store} = $value->cliStore;
6689 0         0 $o->{actorHash} = $value->actorHash;
6690             }
6691              
6692             sub collectActor {
6693 0     0   0 my $o = shift;
6694 0         0 my $label = shift;
6695 0         0 my $value = shift;
6696              
6697 0         0 $o->{actorHash} = $value;
6698             }
6699              
6700             sub collectKeypair {
6701 0     0   0 my $o = shift;
6702 0         0 my $label = shift;
6703 0         0 my $value = shift;
6704              
6705 0         0 $o->{keyPairToken} = $value;
6706             }
6707              
6708             sub collectLabel {
6709 0     0   0 my $o = shift;
6710 0         0 my $label = shift;
6711 0         0 my $value = shift;
6712              
6713 0         0 push @{$o->{forget}}, $value;
  0         0  
6714             }
6715              
6716             sub collectStore {
6717 0     0   0 my $o = shift;
6718 0         0 my $label = shift;
6719 0         0 my $value = shift;
6720              
6721 0         0 $o->{store} = $value;
6722             }
6723              
6724             sub collectText {
6725 0     0   0 my $o = shift;
6726 0         0 my $label = shift;
6727 0         0 my $value = shift;
6728              
6729 0         0 $o->{label} = $value;
6730             }
6731              
6732             sub new {
6733 0     0   0 my $class = shift;
6734 0         0 my $actor = shift;
6735 0         0 bless {actor => $actor, ui => $actor->ui} }
6736              
6737             # END AUTOGENERATED
6738              
6739             # HTML FOLDER NAME remember
6740             # HTML TITLE Remember
6741             sub help {
6742 0     0   0 my $o = shift;
6743 0         0 my $cmd = shift;
6744              
6745 0         0 my $ui = $o->{ui};
6746 0         0 $ui->space;
6747 0         0 $ui->command('cds remember');
6748 0         0 $ui->p('Shows all remembered values.');
6749 0         0 $ui->space;
6750 0         0 $ui->command('cds remember ACCOUNT|ACTOR|STORE|KEYPAIR as TEXT');
6751 0         0 $ui->command('cds remember ACTOR on STORE as TEXT');
6752 0         0 $ui->p('Remembers the indicated actor hash, account, store, or key pair as TEXT. This information is stored in the global state, and therefore persists until the name is deleted (cds forget …) or redefined (cds remember …).');
6753 0         0 $ui->space;
6754 0         0 $ui->p('Key pairs are stored as link (absolute path) to the key pair file, and specific to the device.');
6755 0         0 $ui->space;
6756 0         0 $ui->command('cds forget LABEL');
6757 0         0 $ui->p('Forgets the corresponding item.');
6758 0         0 $ui->space;
6759             }
6760              
6761             sub remember {
6762 0     0   0 my $o = shift;
6763 0         0 my $cmd = shift;
6764              
6765 0         0 $cmd->collect($o);
6766              
6767 0         0 my $record = CDS::Record->new;
6768 0 0       0 $record->add('store')->addText($o->{store}->url) if defined $o->{store};
6769 0 0       0 $record->add('actor')->add($o->{actorHash}->bytes) if defined $o->{actorHash};
6770 0 0       0 $record->add('key pair')->addText($o->{keyPairToken}->file) if defined $o->{keyPairToken};
6771 0         0 $o->{actor}->remember($o->{label}, $record);
6772 0         0 $o->{actor}->saveOrShowError;
6773             }
6774              
6775             sub forget {
6776 0     0   0 my $o = shift;
6777 0         0 my $cmd = shift;
6778              
6779 0         0 $o->{forget} = [];
6780 0         0 $cmd->collect($o);
6781              
6782 0         0 for my $label (@{$o->{forget}}) {
  0         0  
6783 0         0 $o->{actor}->groupRoot->child('labels')->child($label)->clear;
6784             }
6785              
6786 0         0 $o->{actor}->saveOrShowError;
6787             }
6788              
6789             sub showLabels {
6790 0     0   0 my $o = shift;
6791 0         0 my $cmd = shift;
6792              
6793 0         0 $o->{ui}->space;
6794 0         0 $o->showRememberedValues;
6795 0         0 $o->{ui}->space;
6796             }
6797              
6798             sub showRememberedValues {
6799 0     0   0 my $o = shift;
6800              
6801 0         0 my $hasLabel = 0;
6802 0         0 for my $child (sort { $a->{id} cmp $b->{id} } $o->{actor}->groupRoot->child('labels')->children) {
  0         0  
6803 0         0 my $record = $child->record;
6804 0         0 my $label = $o->{ui}->blue($o->{ui}->left(15, Encode::decode_utf8($child->label)));
6805              
6806 0         0 my $actorHash = CDS::Hash->fromBytes($record->child('actor')->bytesValue);
6807 0         0 my $storeUrl = $record->child('store')->textValue;
6808 0         0 my $keyPairFile = $record->child('key pair')->textValue;
6809              
6810 0 0       0 if (length $keyPairFile) {
6811 0         0 $o->{ui}->line($label, ' ', $o->{ui}->gray('key pair'), ' ', $keyPairFile);
6812 0         0 $hasLabel = 1;
6813             }
6814              
6815 0 0 0     0 if ($actorHash && length $storeUrl) {
    0          
    0          
6816 0         0 my $storeReference = $o->{actor}->blueStoreUrlReference($storeUrl);
6817 0         0 $o->{ui}->line($label, ' ', $o->{ui}->gray('account'), ' ', $actorHash->hex, ' on ', $storeReference);
6818 0         0 $hasLabel = 1;
6819             } elsif ($actorHash) {
6820 0         0 $o->{ui}->line($label, ' ', $o->{ui}->gray('actor'), ' ', $actorHash->hex);
6821 0         0 $hasLabel = 1;
6822             } elsif (length $storeUrl) {
6823 0         0 $o->{ui}->line($label, ' ', $o->{ui}->gray('store'), ' ', $storeUrl);
6824 0         0 $hasLabel = 1;
6825             }
6826              
6827 0         0 $o->showActorGroupLabel($label, $record->child('actor group'));
6828             }
6829              
6830 0 0       0 return if $hasLabel;
6831 0         0 $o->{ui}->line($o->{ui}->gray('none'));
6832             }
6833              
6834             sub showActorGroupLabel {
6835 0     0   0 my $o = shift;
6836 0         0 my $label = shift;
6837 0 0 0     0 my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0         0  
6838              
6839 0 0       0 return if ! $record->contains('actor group');
6840              
6841 0         0 my $builder = CDS::ActorGroupBuilder->new;
6842 0         0 $builder->parse($record, 1);
6843              
6844 0         0 my $countActive = 0;
6845 0         0 my $countIdle = 0;
6846 0         0 my $newestActive = undef;
6847              
6848 0         0 for my $member ($builder->members) {
6849 0         0 my $isActive = $member->status eq 'active';
6850 0 0       0 $countActive += 1 if $isActive;
6851 0 0       0 $countIdle += 1 if $member->status eq 'idle';
6852              
6853 0 0       0 next if ! $isActive;
6854 0 0 0     0 next if $newestActive && $member->revision <= $newestActive->revision;
6855 0         0 $newestActive = $member;
6856             }
6857              
6858 0         0 my @line;
6859 0         0 push @line, $label, ' ', $o->{ui}->gray('actor group'), ' ';
6860 0 0       0 push @line, $newestActive->hash->hex, ' on ', $o->{actor}->blueStoreUrlReference($newestActive->storeUrl) if $newestActive;
6861 0 0       0 push @line, $o->{ui}->gray('(no active actor)') if ! $newestActive;
6862 0         0 push @line, $o->{ui}->green(' ', $countActive, ' active');
6863 0         0 my $discovered = $record->child('discovered')->integerValue;
6864 0 0       0 push @line, $o->{ui}->gray(' ', $o->{ui}->niceDateTimeLocal($discovered)) if $discovered;
6865 0         0 $o->{ui}->line(@line);
6866             }
6867              
6868             # BEGIN AUTOGENERATED
6869             package CDS::Commands::Select;
6870              
6871             sub register {
6872 0     0   0 my $class = shift;
6873 0         0 my $cds = shift;
6874 0         0 my $help = shift;
6875              
6876 0         0 my $node000 = CDS::Parser::Node->new(0);
6877 0         0 my $node001 = CDS::Parser::Node->new(0);
6878 0         0 my $node002 = CDS::Parser::Node->new(0);
6879 0         0 my $node003 = CDS::Parser::Node->new(0);
6880 0         0 my $node004 = CDS::Parser::Node->new(0);
6881 0         0 my $node005 = CDS::Parser::Node->new(0);
6882 0         0 my $node006 = CDS::Parser::Node->new(0);
6883 0         0 my $node007 = CDS::Parser::Node->new(0);
6884 0         0 my $node008 = CDS::Parser::Node->new(0);
6885 0         0 my $node009 = CDS::Parser::Node->new(0);
6886 0         0 my $node010 = CDS::Parser::Node->new(0);
6887 0         0 my $node011 = CDS::Parser::Node->new(0);
6888 0         0 my $node012 = CDS::Parser::Node->new(0);
6889 0         0 my $node013 = CDS::Parser::Node->new(0);
6890 0         0 my $node014 = CDS::Parser::Node->new(0);
6891 0         0 my $node015 = CDS::Parser::Node->new(0);
6892 0         0 my $node016 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
6893 0         0 my $node017 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&showSelectionCmd});
6894 0         0 my $node018 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&unselectKeyPair});
6895 0         0 my $node019 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&unselectStore});
6896 0         0 my $node020 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&unselectActor});
6897 0         0 my $node021 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&unselectAll});
6898 0         0 my $node022 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&select});
6899 0         0 $cds->addArrow($node000, 1, 0, 'select');
6900 0         0 $cds->addArrow($node001, 1, 0, 'select');
6901 0         0 $cds->addArrow($node002, 1, 0, 'select');
6902 0         0 $cds->addArrow($node003, 1, 0, 'select');
6903 0         0 $cds->addArrow($node004, 1, 0, 'select');
6904 0         0 $cds->addArrow($node005, 1, 0, 'select');
6905 0         0 $cds->addArrow($node006, 1, 0, 'select');
6906 0         0 $cds->addArrow($node009, 1, 0, 'unselect');
6907 0         0 $cds->addArrow($node010, 1, 0, 'unselect');
6908 0         0 $cds->addArrow($node011, 1, 0, 'unselect');
6909 0         0 $cds->addArrow($node012, 1, 0, 'unselect');
6910 0         0 $cds->addArrow($node017, 1, 0, 'select');
6911 0         0 $help->addArrow($node016, 1, 0, 'select');
6912 0         0 $node000->addArrow($node022, 1, 0, 'KEYPAIR', \&collectKeypair);
6913 0         0 $node001->addArrow($node022, 1, 0, 'STORE', \&collectStore);
6914 0         0 $node002->addArrow($node014, 1, 0, 'ACTOR', \&collectActor);
6915 0         0 $node003->addArrow($node007, 1, 0, 'storage');
6916 0         0 $node004->addArrow($node008, 1, 0, 'messaging');
6917 0         0 $node005->addArrow($node022, 1, 0, 'ACTOR', \&collectActor);
6918 0         0 $node006->addArrow($node022, 1, 1, 'ACCOUNT', \&collectAccount);
6919 0         0 $node007->addArrow($node022, 1, 0, 'store', \&collectStore1);
6920 0         0 $node008->addArrow($node022, 1, 0, 'store', \&collectStore2);
6921 0         0 $node009->addArrow($node013, 1, 0, 'key');
6922 0         0 $node010->addArrow($node019, 1, 0, 'store');
6923 0         0 $node011->addArrow($node020, 1, 0, 'actor');
6924 0         0 $node012->addArrow($node021, 1, 0, 'all');
6925 0         0 $node013->addArrow($node018, 1, 0, 'pair');
6926 0         0 $node014->addArrow($node015, 1, 0, 'on');
6927 0         0 $node015->addArrow($node022, 1, 0, 'STORE', \&collectStore);
6928             }
6929              
6930             sub collectAccount {
6931 0     0   0 my $o = shift;
6932 0         0 my $label = shift;
6933 0         0 my $value = shift;
6934              
6935 0         0 $o->{store} = $value->cliStore;
6936 0         0 $o->{actorHash} = $value->actorHash;
6937             }
6938              
6939             sub collectActor {
6940 0     0   0 my $o = shift;
6941 0         0 my $label = shift;
6942 0         0 my $value = shift;
6943              
6944 0         0 $o->{actorHash} = $value;
6945             }
6946              
6947             sub collectKeypair {
6948 0     0   0 my $o = shift;
6949 0         0 my $label = shift;
6950 0         0 my $value = shift;
6951              
6952 0         0 $o->{keyPairToken} = $value;
6953 0         0 $o->{actorHash} = $value->keyPair->publicKey->hash;
6954             }
6955              
6956             sub collectStore {
6957 0     0   0 my $o = shift;
6958 0         0 my $label = shift;
6959 0         0 my $value = shift;
6960              
6961 0         0 $o->{store} = $value;
6962             }
6963              
6964             sub collectStore1 {
6965 0     0   0 my $o = shift;
6966 0         0 my $label = shift;
6967 0         0 my $value = shift;
6968              
6969 0         0 $o->{store} = $o->{actor}->storageStore;
6970             }
6971              
6972             sub collectStore2 {
6973 0     0   0 my $o = shift;
6974 0         0 my $label = shift;
6975 0         0 my $value = shift;
6976              
6977 0         0 $o->{store} = $o->{actor}->messagingStore;
6978             }
6979              
6980             sub new {
6981 0     0   0 my $class = shift;
6982 0         0 my $actor = shift;
6983 0         0 bless {actor => $actor, ui => $actor->ui} }
6984              
6985             # END AUTOGENERATED
6986              
6987             # HTML FOLDER NAME select
6988             # HTML TITLE Select
6989             sub help {
6990 0     0   0 my $o = shift;
6991 0         0 my $cmd = shift;
6992              
6993 0         0 my $ui = $o->{ui};
6994 0         0 $ui->space;
6995 0         0 $ui->command('cds select');
6996 0         0 $ui->p('Shows the current selection.');
6997 0         0 $ui->space;
6998 0         0 $ui->command('cds select KEYPAIR');
6999 0         0 $ui->p('Selects KEYPAIR on this terminal. Some commands will use this key pair by default.');
7000 0         0 $ui->space;
7001 0         0 $ui->command('cds unselect key pair');
7002 0         0 $ui->p('Removes the key pair selection.');
7003 0         0 $ui->space;
7004 0         0 $ui->command('cds select STORE');
7005 0         0 $ui->p('Selects STORE on this terminal. Some commands will use this store by default.');
7006 0         0 $ui->space;
7007 0         0 $ui->command('cds unselect store');
7008 0         0 $ui->p('Removes the store selection.');
7009 0         0 $ui->space;
7010 0         0 $ui->command('cds select ACTOR');
7011 0         0 $ui->p('Selects ACTOR on this terminal. Some commands will use this store by default.');
7012 0         0 $ui->space;
7013 0         0 $ui->command('cds unselect actor');
7014 0         0 $ui->p('Removes the actor selection.');
7015 0         0 $ui->space;
7016 0         0 $ui->command('cds unselect');
7017 0         0 $ui->p('Removes any selection.');
7018 0         0 $ui->space;
7019             }
7020              
7021             sub select {
7022 0     0   0 my $o = shift;
7023 0         0 my $cmd = shift;
7024              
7025 0         0 $cmd->collect($o);
7026              
7027 0 0       0 if ($o->{keyPairToken}) {
7028 0         0 $o->{actor}->sessionRoot->child('selected key pair')->setText($o->{keyPairToken}->file);
7029 0         0 $o->{ui}->pGreen('Key pair ', $o->{keyPairToken}->file, ' selected.');
7030             }
7031              
7032 0 0       0 if ($o->{store}) {
7033 0         0 $o->{actor}->sessionRoot->child('selected store')->setText($o->{store}->url);
7034 0         0 $o->{ui}->pGreen('Store ', $o->{store}->url, ' selected.');
7035             }
7036              
7037 0 0       0 if ($o->{actorHash}) {
7038 0         0 $o->{actor}->sessionRoot->child('selected actor')->setBytes($o->{actorHash}->bytes);
7039 0         0 $o->{ui}->pGreen('Actor ', $o->{actorHash}->hex, ' selected.');
7040             }
7041              
7042 0         0 $o->{actor}->saveOrShowError;
7043             }
7044              
7045             sub unselectKeyPair {
7046 0     0   0 my $o = shift;
7047 0         0 my $cmd = shift;
7048              
7049 0         0 $o->{actor}->sessionRoot->child('selected key pair')->clear;
7050 0         0 $o->{ui}->pGreen('Key pair selection cleared.');
7051 0         0 $o->{actor}->saveOrShowError;
7052             }
7053              
7054             sub unselectStore {
7055 0     0   0 my $o = shift;
7056 0         0 my $cmd = shift;
7057              
7058 0         0 $o->{actor}->sessionRoot->child('selected store')->clear;
7059 0         0 $o->{ui}->pGreen('Store selection cleared.');
7060 0         0 $o->{actor}->saveOrShowError;
7061             }
7062              
7063             sub unselectActor {
7064 0     0   0 my $o = shift;
7065 0         0 my $cmd = shift;
7066              
7067 0         0 $o->{actor}->sessionRoot->child('selected actor')->clear;
7068 0         0 $o->{ui}->pGreen('Actor selection cleared.');
7069 0         0 $o->{actor}->saveOrShowError;
7070             }
7071              
7072             sub unselectAll {
7073 0     0   0 my $o = shift;
7074 0         0 my $cmd = shift;
7075              
7076 0         0 $o->{actor}->sessionRoot->child('selected key pair')->clear;
7077 0         0 $o->{actor}->sessionRoot->child('selected store')->clear;
7078 0         0 $o->{actor}->sessionRoot->child('selected actor')->clear;
7079 0   0     0 $o->{actor}->saveOrShowError // return;
7080 0         0 $o->showSelection;
7081             }
7082              
7083             sub showSelectionCmd {
7084 0     0   0 my $o = shift;
7085 0         0 my $cmd = shift;
7086              
7087 0         0 $o->{ui}->space;
7088 0         0 $o->showSelection;
7089 0         0 $o->{ui}->space;
7090             }
7091              
7092             sub showSelection {
7093 0     0   0 my $o = shift;
7094              
7095 0         0 my $keyPairFile = $o->{actor}->sessionRoot->child('selected key pair')->textValue;
7096 0         0 my $storeUrl = $o->{actor}->sessionRoot->child('selected store')->textValue;
7097 0         0 my $actorBytes = $o->{actor}->sessionRoot->child('selected actor')->bytesValue;
7098              
7099 0 0       0 $o->{ui}->line($o->{ui}->darkBold('Selected key pair '), length $keyPairFile ? $keyPairFile : $o->{ui}->gray('none'));
7100 0 0       0 $o->{ui}->line($o->{ui}->darkBold('Selected store '), length $storeUrl ? $storeUrl : $o->{ui}->gray('none'));
7101 0 0       0 $o->{ui}->line($o->{ui}->darkBold('Selected actor '), length $actorBytes == 32 ? unpack('H*', $actorBytes) : $o->{ui}->gray('none'));
7102             }
7103              
7104             # BEGIN AUTOGENERATED
7105             package CDS::Commands::ShowCard;
7106              
7107             sub register {
7108 0     0   0 my $class = shift;
7109 0         0 my $cds = shift;
7110 0         0 my $help = shift;
7111              
7112 0         0 my $node000 = CDS::Parser::Node->new(0);
7113 0         0 my $node001 = CDS::Parser::Node->new(0);
7114 0         0 my $node002 = CDS::Parser::Node->new(0);
7115 0         0 my $node003 = CDS::Parser::Node->new(0);
7116 0         0 my $node004 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
7117 0         0 my $node005 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&showMyCard});
7118 0         0 my $node006 = CDS::Parser::Node->new(1);
7119 0         0 my $node007 = CDS::Parser::Node->new(0);
7120 0         0 my $node008 = CDS::Parser::Node->new(0);
7121 0         0 my $node009 = CDS::Parser::Node->new(0);
7122 0         0 my $node010 = CDS::Parser::Node->new(0);
7123 0         0 my $node011 = CDS::Parser::Node->new(0);
7124 0         0 my $node012 = CDS::Parser::Node->new(0);
7125 0         0 my $node013 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&showCard});
7126 0         0 $cds->addArrow($node001, 1, 0, 'show');
7127 0         0 $cds->addArrow($node002, 1, 0, 'show');
7128 0         0 $help->addArrow($node000, 1, 0, 'show');
7129 0         0 $node000->addArrow($node004, 1, 0, 'card');
7130 0         0 $node001->addArrow($node006, 1, 0, 'card');
7131 0         0 $node002->addArrow($node003, 1, 0, 'my');
7132 0         0 $node003->addArrow($node005, 1, 0, 'card');
7133 0         0 $node006->addArrow($node007, 1, 0, 'of');
7134 0         0 $node006->addArrow($node008, 1, 0, 'of');
7135 0         0 $node006->addArrow($node009, 1, 0, 'of');
7136 0         0 $node006->addArrow($node010, 1, 0, 'of');
7137 0         0 $node006->addDefault($node011);
7138 0         0 $node007->addArrow($node007, 1, 0, 'ACCOUNT', \&collectAccount);
7139 0         0 $node007->addArrow($node013, 1, 1, 'ACCOUNT', \&collectAccount);
7140 0         0 $node008->addArrow($node013, 1, 0, 'ACTORGROUP', \&collectActorgroup);
7141 0         0 $node009->addArrow($node011, 1, 0, 'KEYPAIR', \&collectKeypair);
7142 0         0 $node010->addArrow($node011, 1, 0, 'ACTOR', \&collectActor);
7143 0         0 $node011->addArrow($node012, 1, 0, 'on');
7144 0         0 $node011->addDefault($node013);
7145 0         0 $node012->addArrow($node012, 1, 0, 'STORE', \&collectStore);
7146 0         0 $node012->addArrow($node013, 1, 0, 'STORE', \&collectStore);
7147             }
7148              
7149             sub collectAccount {
7150 0     0   0 my $o = shift;
7151 0         0 my $label = shift;
7152 0         0 my $value = shift;
7153              
7154 0         0 push @{$o->{accountTokens}}, $value;
  0         0  
7155             }
7156              
7157             sub collectActor {
7158 0     0   0 my $o = shift;
7159 0         0 my $label = shift;
7160 0         0 my $value = shift;
7161              
7162 0         0 $o->{actorHash} = $value;
7163             }
7164              
7165             sub collectActorgroup {
7166 0     0   0 my $o = shift;
7167 0         0 my $label = shift;
7168 0         0 my $value = shift;
7169              
7170 0         0 for my $member ($value->actorGroup->members) {
7171 0         0 my $actorOnStore = $member->actorOnStore;
7172 0         0 $o->addKnownPublicKey($actorOnStore->publicKey);
7173 0         0 push @{$o->{accountTokens}}, CDS::AccountToken->new($actorOnStore->store, $actorOnStore->publicKey->hash);
  0         0  
7174             }
7175             }
7176              
7177             sub collectKeypair {
7178 0     0   0 my $o = shift;
7179 0         0 my $label = shift;
7180 0         0 my $value = shift;
7181              
7182 0         0 $o->{keyPairToken} = $value;
7183 0         0 $o->{actorHash} = $value->keyPair->publicKey->hash;
7184             }
7185              
7186             sub collectStore {
7187 0     0   0 my $o = shift;
7188 0         0 my $label = shift;
7189 0         0 my $value = shift;
7190              
7191 0         0 push @{$o->{stores}}, $value;
  0         0  
7192             }
7193              
7194             sub new {
7195 0     0   0 my $class = shift;
7196 0         0 my $actor = shift;
7197 0         0 bless {actor => $actor, ui => $actor->ui} }
7198              
7199             # END AUTOGENERATED
7200              
7201             # HTML FOLDER NAME show-card
7202             # HTML TITLE Show an actor's public card
7203             sub help {
7204 0     0   0 my $o = shift;
7205 0         0 my $cmd = shift;
7206              
7207 0         0 my $ui = $o->{ui};
7208 0         0 $ui->space;
7209 0         0 $ui->command('cds show card of ACCOUNT');
7210 0         0 $ui->command('cds show card of ACTOR [on STORE]');
7211 0         0 $ui->command('cds show card of KEYPAIR [on STORE]');
7212 0         0 $ui->p('Shows the card(s) of an actor.');
7213 0         0 $ui->space;
7214 0         0 $ui->command('cds show card of ACTORGROUP');
7215 0         0 $ui->p('Shows all cards of an actor group.');
7216 0         0 $ui->space;
7217 0         0 $ui->command('cds show card');
7218 0         0 $ui->p('Shows the card of the selected actor on the selected store.');
7219 0         0 $ui->space;
7220 0         0 $ui->command('cds show my card');
7221 0         0 $ui->p('Shows your own card.');
7222 0         0 $ui->space;
7223 0         0 $ui->p('An actor usually has one card. If no cards are shown, the corresponding actor does not exist, is not using that store, or has not properly announced itself. Two cards may exist while the actor is updating its card. Such a state is temporary, but may exist for hours or days if the actor has intermittent network access. Three or more cards may point to an error in the way the actor updates his card, an error in the synchronization code (if the account is synchronized). Two or more cards may also occur naturally when stores are merged.');
7224 0         0 $ui->space;
7225 0         0 $ui->p('A peer consists of one or more actors, which all publish their own card. The cards are usually different, but should contain consistent information.');
7226 0         0 $ui->space;
7227 0         0 $ui->p('You can publish your own card (i.e. the card of your main key pair) using');
7228 0         0 $ui->p(' cds announce');
7229 0         0 $ui->space;
7230             }
7231              
7232             sub showCard {
7233 0     0   0 my $o = shift;
7234 0         0 my $cmd = shift;
7235              
7236 0         0 $o->{keyPairToken} = $o->{actor}->preferredKeyPairToken;
7237 0         0 $o->{stores} = [];
7238 0         0 $o->{accountTokens} = [];
7239 0         0 $o->{knownPublicKeys} = {};
7240 0         0 $cmd->collect($o);
7241              
7242             # Use actorHash/store
7243 0 0       0 if (! scalar @{$o->{accountTokens}}) {
  0         0  
7244 0 0       0 $o->{actorHash} = $o->{actor}->preferredActorHash if ! $o->{actorHash};
7245 0 0       0 push @{$o->{stores}}, $o->{actor}->preferredStores if ! scalar @{$o->{stores}};
  0         0  
  0         0  
7246 0         0 for my $store (@{$o->{stores}}) {
  0         0  
7247 0         0 push @{$o->{accountTokens}}, CDS::AccountToken->new($store, $o->{actorHash});
  0         0  
7248             }
7249             }
7250              
7251             # Show the cards
7252 0         0 $o->addKnownPublicKey($o->{keyPairToken}->keyPair->publicKey);
7253 0         0 $o->addKnownPublicKey($o->{actor}->keyPair->publicKey);
7254 0         0 for my $accountToken (@{$o->{accountTokens}}) {
  0         0  
7255 0         0 $o->processAccount($accountToken);
7256             }
7257              
7258 0         0 $o->{ui}->space;
7259             }
7260              
7261             sub showMyCard {
7262 0     0   0 my $o = shift;
7263 0         0 my $cmd = shift;
7264              
7265 0         0 $o->{keyPairToken} = $o->{actor}->preferredKeyPairToken;
7266 0         0 $o->processAccount(CDS::AccountToken->new($o->{actor}->messagingStore, $o->{actor}->keyPair->publicKey->hash));
7267 0 0       0 $o->processAccount(CDS::AccountToken->new($o->{actor}->storageStore, $o->{actor}->keyPair->publicKey->hash)) if $o->{actor}->storageStore->url ne $o->{actor}->messagingStore->url;
7268 0         0 $o->{ui}->space;
7269             }
7270              
7271             sub processAccount {
7272 0     0   0 my $o = shift;
7273 0         0 my $accountToken = shift;
7274              
7275 0         0 $o->{ui}->space;
7276              
7277             # Query the store
7278 0         0 my $store = $accountToken->cliStore;
7279 0         0 my ($hashes, $storeError) = $store->list($accountToken->actorHash, 'public', 0);
7280 0 0       0 if (defined $storeError) {
7281 0         0 $o->{ui}->title('public box of ', $o->{actor}->blueAccountReference($accountToken));
7282 0         0 return;
7283             }
7284              
7285             # Print the result
7286 0         0 my $count = scalar @$hashes;
7287 0 0       0 $o->{ui}->title('public box of ', $o->{actor}->blueAccountReference($accountToken), ' ', $o->{ui}->blue($count == 0 ? 'no cards' : $count == 1 ? '1 card' : $count.' cards'));
    0          
7288 0 0       0 return if ! $count;
7289              
7290 0         0 foreach my $hash (sort { $a->bytes cmp $b->bytes } @$hashes) {
  0         0  
7291 0         0 $o->processEntry($accountToken, $hash);
7292             }
7293             }
7294              
7295             sub processEntry {
7296 0     0   0 my $o = shift;
7297 0         0 my $accountToken = shift;
7298 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
7299              
7300 0         0 my $keyPair = $o->{keyPairToken}->keyPair;
7301 0         0 my $store = $accountToken->cliStore;
7302 0         0 my $storeReference = $o->{actor}->storeReference($store);
7303              
7304             # Open the envelope
7305 0         0 $o->{ui}->line($o->{ui}->gold('cds open envelope ', $hash->hex), $o->{ui}->gray(' from ', $accountToken->actorHash->hex, ' on ', $storeReference));
7306              
7307 0   0     0 my $envelope = $o->{actor}->uiGetRecord($hash, $accountToken->cliStore, $o->{keyPairToken}) // return;
7308 0   0     0 my $publicKey = $o->getPublicKey($accountToken) // $o->{ui}->pRed('The owner\'s public key is missing. Skipping signature verification.');
7309 0   0     0 my $cardHash = $envelope->child('content')->hashValue // $o->{ui}->pRed('Missing content hash.');
7310 0 0 0     0 return $o->{ui}->pRed('Invalid signature.') if $publicKey && $cardHash && ! CDS->verifyEnvelopeSignature($envelope, $publicKey, $cardHash);
      0        
7311              
7312             # Read and show the card
7313 0 0       0 return if ! $cardHash;
7314 0         0 $o->{ui}->line($o->{ui}->gold('cds show record ', $cardHash->hex), $o->{ui}->gray(' on ', $storeReference));
7315 0   0     0 my $card = $o->{actor}->uiGetRecord($cardHash, $accountToken->cliStore, $o->{keyPairToken}) // return;
7316              
7317 0         0 $o->{ui}->pushIndent;
7318 0         0 $o->{ui}->recordChildren($card, $storeReference);
7319 0         0 $o->{ui}->popIndent;
7320 0         0 return;
7321             }
7322              
7323             sub getPublicKey {
7324 0     0   0 my $o = shift;
7325 0         0 my $accountToken = shift;
7326              
7327 0         0 my $hash = $accountToken->actorHash;
7328 0         0 my $knownPublicKey = $o->{knownPublicKeys}->{$hash->bytes};
7329 0 0       0 return $knownPublicKey if $knownPublicKey;
7330 0   0     0 my $publicKey = $o->{actor}->uiGetPublicKey($hash, $accountToken->cliStore, $o->{keyPairToken}) // return;
7331 0         0 $o->addKnownPublicKey($publicKey);
7332 0         0 return $publicKey;
7333             }
7334              
7335             sub addKnownPublicKey {
7336 0     0   0 my $o = shift;
7337 0 0 0     0 my $publicKey = shift; die 'wrong type '.ref($publicKey).' for $publicKey' if defined $publicKey && ref $publicKey ne 'CDS::PublicKey';
  0         0  
7338              
7339 0         0 $o->{knownPublicKeys}->{$publicKey->hash->bytes} = $publicKey;
7340             }
7341              
7342             # BEGIN AUTOGENERATED
7343             package CDS::Commands::ShowKeyPair;
7344              
7345             sub register {
7346 0     0   0 my $class = shift;
7347 0         0 my $cds = shift;
7348 0         0 my $help = shift;
7349              
7350 0         0 my $node000 = CDS::Parser::Node->new(0);
7351 0         0 my $node001 = CDS::Parser::Node->new(0);
7352 0         0 my $node002 = CDS::Parser::Node->new(0);
7353 0         0 my $node003 = CDS::Parser::Node->new(0);
7354 0         0 my $node004 = CDS::Parser::Node->new(0);
7355 0         0 my $node005 = CDS::Parser::Node->new(0);
7356 0         0 my $node006 = CDS::Parser::Node->new(0);
7357 0         0 my $node007 = CDS::Parser::Node->new(0);
7358 0         0 my $node008 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
7359 0         0 my $node009 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&showKeyPair});
7360 0         0 my $node010 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&showMyKeyPair});
7361 0         0 my $node011 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&showSelectedKeyPair});
7362 0         0 $cds->addArrow($node002, 1, 0, 'show');
7363 0         0 $cds->addArrow($node003, 1, 0, 'show');
7364 0         0 $cds->addArrow($node004, 1, 0, 'show');
7365 0         0 $help->addArrow($node000, 1, 0, 'show');
7366 0         0 $node000->addArrow($node001, 1, 0, 'key');
7367 0         0 $node001->addArrow($node008, 1, 0, 'pair');
7368 0         0 $node002->addArrow($node009, 1, 0, 'KEYPAIR', \&collectKeypair);
7369 0         0 $node003->addArrow($node005, 1, 0, 'my');
7370 0         0 $node004->addArrow($node006, 1, 0, 'key');
7371 0         0 $node005->addArrow($node007, 1, 0, 'key');
7372 0         0 $node006->addArrow($node011, 1, 0, 'pair');
7373 0         0 $node007->addArrow($node010, 1, 0, 'pair');
7374             }
7375              
7376             sub collectKeypair {
7377 0     0   0 my $o = shift;
7378 0         0 my $label = shift;
7379 0         0 my $value = shift;
7380              
7381 0         0 $o->{keyPairToken} = $value;
7382             }
7383              
7384             sub new {
7385 0     0   0 my $class = shift;
7386 0         0 my $actor = shift;
7387 0         0 bless {actor => $actor, ui => $actor->ui} }
7388              
7389             # END AUTOGENERATED
7390              
7391             # HTML FOLDER NAME show-key-pair
7392             # HTML TITLE Show key pair
7393             sub help {
7394 0     0   0 my $o = shift;
7395 0         0 my $cmd = shift;
7396              
7397 0         0 my $ui = $o->{ui};
7398 0         0 $ui->space;
7399 0         0 $ui->command('cds show KEYPAIR');
7400 0         0 $ui->command('cds show my key pair');
7401 0         0 $ui->command('cds show key pair');
7402 0         0 $ui->p('Shows information about KEYPAIR, your key pair, or the currently selected key pair (see "cds use …").');
7403 0         0 $ui->space;
7404             }
7405              
7406             sub showKeyPair {
7407 0     0   0 my $o = shift;
7408 0         0 my $cmd = shift;
7409              
7410 0         0 $cmd->collect($o);
7411 0         0 $o->showAll($o->{keyPairToken});
7412             }
7413              
7414             sub showMyKeyPair {
7415 0     0   0 my $o = shift;
7416 0         0 my $cmd = shift;
7417              
7418 0         0 $cmd->collect($o);
7419 0         0 $o->showAll($o->{actor}->keyPairToken);
7420             }
7421              
7422             sub showSelectedKeyPair {
7423 0     0   0 my $o = shift;
7424 0         0 my $cmd = shift;
7425              
7426 0         0 $cmd->collect($o);
7427 0         0 $o->showAll($o->{actor}->preferredKeyPairToken);
7428             }
7429              
7430             sub show {
7431 0     0   0 my $o = shift;
7432 0         0 my $keyPairToken = shift;
7433              
7434 0 0       0 $o->{ui}->line($o->{ui}->darkBold('File '), $keyPairToken->file) if defined $keyPairToken->file;
7435 0         0 $o->{ui}->line($o->{ui}->darkBold('Hash '), $keyPairToken->keyPair->publicKey->hash->hex);
7436             }
7437              
7438             sub showAll {
7439 0     0   0 my $o = shift;
7440 0         0 my $keyPairToken = shift;
7441              
7442 0         0 $o->{ui}->space;
7443 0         0 $o->{ui}->title('Key pair');
7444 0         0 $o->show($keyPairToken);
7445 0         0 $o->showPublicKeyObject($keyPairToken);
7446 0         0 $o->showPublicKey($keyPairToken);
7447 0         0 $o->showPrivateKey($keyPairToken);
7448 0         0 $o->{ui}->space;
7449             }
7450              
7451             sub showPublicKeyObject {
7452 0     0   0 my $o = shift;
7453 0         0 my $keyPairToken = shift;
7454              
7455 0         0 my $object = $keyPairToken->keyPair->publicKey->object;
7456 0         0 $o->{ui}->space;
7457 0         0 $o->{ui}->title('Public key object');
7458 0         0 $o->byteData(' ', $object->bytes);
7459             }
7460              
7461             sub showPublicKey {
7462 0     0   0 my $o = shift;
7463 0         0 my $keyPairToken = shift;
7464              
7465 0         0 my $rsaPublicKey = $keyPairToken->keyPair->publicKey->{rsaPublicKey};
7466 0         0 $o->{ui}->space;
7467 0         0 $o->{ui}->title('Public key');
7468 0         0 $o->byteData('e ', CDS::C::publicKeyE($rsaPublicKey));
7469 0         0 $o->byteData('n ', CDS::C::publicKeyN($rsaPublicKey));
7470             }
7471              
7472             sub showPrivateKey {
7473 0     0   0 my $o = shift;
7474 0         0 my $keyPairToken = shift;
7475              
7476 0         0 my $rsaPrivateKey = $keyPairToken->keyPair->{rsaPrivateKey};
7477 0         0 $o->{ui}->space;
7478 0         0 $o->{ui}->title('Private key');
7479 0         0 $o->byteData('e ', CDS::C::privateKeyE($rsaPrivateKey));
7480 0         0 $o->byteData('p ', CDS::C::privateKeyP($rsaPrivateKey));
7481 0         0 $o->byteData('q ', CDS::C::privateKeyQ($rsaPrivateKey));
7482             }
7483              
7484             sub byteData {
7485 0     0   0 my $o = shift;
7486 0         0 my $label = shift;
7487 0         0 my $bytes = shift;
7488              
7489 0         0 my $hex = unpack('H*', $bytes);
7490 0         0 $o->{ui}->line($o->{ui}->darkBold($label), substr($hex, 0, 64));
7491              
7492 0         0 my $start = 64;
7493 0         0 my $spaces = ' ' x length $label;
7494 0         0 while ($start < length $hex) {
7495 0         0 $o->{ui}->line($spaces, substr($hex, $start, 64));
7496 0         0 $start += 64;
7497             }
7498             }
7499              
7500             # BEGIN AUTOGENERATED
7501             package CDS::Commands::ShowMessages;
7502              
7503             sub register {
7504 0     0   0 my $class = shift;
7505 0         0 my $cds = shift;
7506 0         0 my $help = shift;
7507              
7508 0         0 my $node000 = CDS::Parser::Node->new(0);
7509 0         0 my $node001 = CDS::Parser::Node->new(0);
7510 0         0 my $node002 = CDS::Parser::Node->new(0);
7511 0         0 my $node003 = CDS::Parser::Node->new(0);
7512 0         0 my $node004 = CDS::Parser::Node->new(0);
7513 0         0 my $node005 = CDS::Parser::Node->new(0);
7514 0         0 my $node006 = CDS::Parser::Node->new(0);
7515 0         0 my $node007 = CDS::Parser::Node->new(0);
7516 0         0 my $node008 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
7517 0         0 my $node009 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&showMessagesOfSelected});
7518 0         0 my $node010 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&showMyMessages});
7519 0         0 my $node011 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&showOurMessages});
7520 0         0 my $node012 = CDS::Parser::Node->new(1);
7521 0         0 my $node013 = CDS::Parser::Node->new(0);
7522 0         0 my $node014 = CDS::Parser::Node->new(0);
7523 0         0 my $node015 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&showMessages});
7524 0         0 $cds->addArrow($node001, 1, 0, 'show');
7525 0         0 $cds->addArrow($node002, 1, 0, 'show');
7526 0         0 $cds->addArrow($node003, 1, 0, 'show');
7527 0         0 $cds->addArrow($node004, 1, 0, 'show');
7528 0         0 $help->addArrow($node000, 1, 0, 'show');
7529 0         0 $node000->addArrow($node008, 1, 0, 'messages');
7530 0         0 $node001->addArrow($node005, 1, 0, 'messages');
7531 0         0 $node002->addArrow($node006, 1, 0, 'my');
7532 0         0 $node003->addArrow($node009, 1, 0, 'messages');
7533 0         0 $node004->addArrow($node007, 1, 0, 'our');
7534 0         0 $node005->addArrow($node012, 1, 0, 'of');
7535 0         0 $node006->addArrow($node010, 1, 0, 'messages');
7536 0         0 $node007->addArrow($node011, 1, 0, 'messages');
7537 0         0 $node012->addArrow($node013, 1, 0, 'ACTOR', \&collectActor);
7538 0         0 $node012->addArrow($node013, 1, 0, 'KEYPAIR', \&collectKeypair);
7539 0         0 $node012->addArrow($node015, 1, 1, 'ACCOUNT', \&collectAccount);
7540 0         0 $node012->addArrow($node015, 1, 0, 'ACTOR', \&collectActor1);
7541 0         0 $node012->addArrow($node015, 1, 0, 'ACTORGROUP', \&collectActorgroup);
7542 0         0 $node012->addArrow($node015, 1, 0, 'KEYPAIR', \&collectKeypair1);
7543 0         0 $node013->addArrow($node014, 1, 0, 'on');
7544 0         0 $node014->addArrow($node015, 1, 0, 'STORE', \&collectStore);
7545             }
7546              
7547             sub collectAccount {
7548 0     0   0 my $o = shift;
7549 0         0 my $label = shift;
7550 0         0 my $value = shift;
7551              
7552 0         0 push @{$o->{accountTokens}}, $value;
  0         0  
7553             }
7554              
7555             sub collectActor {
7556 0     0   0 my $o = shift;
7557 0         0 my $label = shift;
7558 0         0 my $value = shift;
7559              
7560 0         0 $o->{actorHash} = $value;
7561             }
7562              
7563             sub collectActor1 {
7564 0     0   0 my $o = shift;
7565 0         0 my $label = shift;
7566 0         0 my $value = shift;
7567              
7568 0         0 push @{$o->{accountTokens}}, CDS::AccountToken->new($o->{actor}->preferredStore, $value);
  0         0  
7569             }
7570              
7571             sub collectActorgroup {
7572 0     0   0 my $o = shift;
7573 0         0 my $label = shift;
7574 0         0 my $value = shift;
7575              
7576 0         0 for my $member ($value->actorGroup->members) {
7577 0         0 push @{$o->{accountTokens}}, CDS::AccountToken->new($member->actorOnStore->store, $member->actorOnStore->publicKey->hash);
  0         0  
7578             }
7579             }
7580              
7581             sub collectKeypair {
7582 0     0   0 my $o = shift;
7583 0         0 my $label = shift;
7584 0         0 my $value = shift;
7585              
7586 0         0 $o->{keyPairToken} = $value;
7587 0         0 $o->{actorHash} = $value->keyPair->publicKey->hash;
7588             }
7589              
7590             sub collectKeypair1 {
7591 0     0   0 my $o = shift;
7592 0         0 my $label = shift;
7593 0         0 my $value = shift;
7594              
7595 0         0 $o->{keyPairToken} = $value;
7596 0         0 push @{$o->{accountTokens}}, CDS::AccountToken->new($o->{actor}->preferredStore, $value->publicKey->hash);
  0         0  
7597             }
7598              
7599             sub collectStore {
7600 0     0   0 my $o = shift;
7601 0         0 my $label = shift;
7602 0         0 my $value = shift;
7603              
7604 0         0 push @{$o->{accountTokens}}, CDS::AccountToken->new($value, $o->{actorHash});
  0         0  
7605 0         0 delete $o->{actorHash};
7606             }
7607              
7608             sub new {
7609 0     0   0 my $class = shift;
7610 0         0 my $actor = shift;
7611 0         0 bless {actor => $actor, ui => $actor->ui} }
7612              
7613             # END AUTOGENERATED
7614              
7615             # HTML FOLDER NAME show-messages
7616             # HTML TITLE Show messages
7617             sub help {
7618 0     0   0 my $o = shift;
7619 0         0 my $cmd = shift;
7620              
7621 0         0 my $ui = $o->{ui};
7622 0         0 $ui->space;
7623 0         0 $ui->command('cds show messages of ACCOUNT');
7624 0         0 $ui->command('cds show messages of ACTOR|KEYPAIR [on STORE]');
7625 0         0 $ui->p('Shows all (unprocessed) messages of an actor ordered by their envelope hash. If store is omitted, the selected store is used.');
7626 0         0 $ui->space;
7627 0         0 $ui->command('cds show messages of ACTORGROUP');
7628 0         0 $ui->p('Shows all messages of all actors of that group.');
7629 0         0 $ui->space;
7630 0         0 $ui->command('cds show messages');
7631 0         0 $ui->p('Shows the messages of the selected key pair on the selected store.');
7632 0         0 $ui->space;
7633 0         0 $ui->command('cds show my messages');
7634 0         0 $ui->p('Shows your messages.');
7635 0         0 $ui->space;
7636 0         0 $ui->command('cds show our messages');
7637 0         0 $ui->p('Shows all messages of your actor group.');
7638 0         0 $ui->space;
7639 0         0 $ui->p('Unprocessed messages are stored in the message box of an actor. Each entry points to an envelope, which in turn points to a record object. The envelope is signed by the sender, but does not hold any date. If the application relies on dates, it must include this date in the message.');
7640 0         0 $ui->space;
7641 0         0 $ui->p('While the envelope hash is stored on the actor\'s store, the envelope and the message are stored on the sender\'s store, and are downloaded from there. Depending on the reachability and responsiveness of that store, messages may not always be accessible.');
7642 0         0 $ui->space;
7643 0         0 $ui->p('Senders typically keep sent messages for about 10 days on their store. After that, the envelope hash may still be in the message box, but the actual message may have vanished.');
7644 0         0 $ui->space;
7645             }
7646              
7647             sub showMessagesOfSelected {
7648 0     0   0 my $o = shift;
7649 0         0 my $cmd = shift;
7650              
7651 0         0 $o->{keyPairToken} = $o->{actor}->preferredKeyPairToken;
7652 0         0 $o->processAccounts(CDS::AccountToken->new($o->{actor}->preferredStore, $o->{actor}->preferredActorHash));
7653             }
7654              
7655             sub showMyMessages {
7656 0     0   0 my $o = shift;
7657 0         0 my $cmd = shift;
7658              
7659 0         0 $o->{keyPairToken} = $o->{actor}->keyPairToken;
7660 0         0 my $actorHash = $o->{actor}->keyPair->publicKey->hash;
7661 0         0 my $store = $o->{actor}->messagingStore;
7662 0         0 $o->processAccounts(CDS::AccountToken->new($store, $actorHash));
7663             }
7664              
7665             sub showOurMessages {
7666 0     0   0 my $o = shift;
7667 0         0 my $cmd = shift;
7668              
7669 0         0 $o->{keyPairToken} = $o->{actor}->keyPairToken;
7670              
7671 0         0 my @accountTokens;
7672 0         0 for my $child ($o->{actor}->actorGroupSelector->children) {
7673 0 0       0 next if $child->child('revoked')->isSet;
7674 0 0       0 next if ! $child->child('active')->isSet;
7675              
7676 0         0 my $record = $child->record;
7677 0   0     0 my $actorHash = $record->child('hash')->hashValue // next;
7678 0         0 my $storeUrl = $record->child('store')->textValue;
7679 0   0     0 my $store = $o->{actor}->storeForUrl($storeUrl) // next;
7680 0         0 push @accountTokens, CDS::AccountToken->new($store, $actorHash);
7681             }
7682              
7683 0         0 $o->processAccounts(@accountTokens);
7684             }
7685              
7686             sub showMessages {
7687 0     0   0 my $o = shift;
7688 0         0 my $cmd = shift;
7689              
7690 0         0 $o->{accountTokens} = [];
7691 0         0 $cmd->collect($o);
7692              
7693             # Unless a key pair was provided, use the selected key pair
7694 0 0       0 $o->{keyPairToken} = $o->{actor}->keyPairToken if ! $o->{keyPairToken};
7695              
7696 0         0 $o->processAccounts(@{$o->{accountTokens}});
  0         0  
7697             }
7698              
7699             sub processAccounts {
7700 0     0   0 my $o = shift;
7701              
7702             # Initialize the statistics
7703 0         0 $o->{countValid} = 0;
7704 0         0 $o->{countInvalid} = 0;
7705              
7706             # Show the messages of all selected accounts
7707 0         0 for my $accountToken (@_) {
7708 0         0 CDS::Commands::ShowMessages::ProcessAccount->new($o, $accountToken);
7709             }
7710              
7711             # Show the statistics
7712 0         0 $o->{ui}->space;
7713 0         0 $o->{ui}->title('Total');
7714 0 0       0 $o->{ui}->line(scalar @_, ' account', scalar @_ == 1 ? '' : 's');
7715 0 0       0 $o->{ui}->line($o->{countValid}, ' message', $o->{countValid} == 1 ? '' : 's');
7716 0 0       0 $o->{ui}->line($o->{countInvalid}, ' invalid message', $o->{countInvalid} == 1 ? '' : 's') if $o->{countInvalid};
    0          
7717 0         0 $o->{ui}->space;
7718             }
7719              
7720             package CDS::Commands::ShowMessages::ProcessAccount;
7721              
7722             sub new {
7723 0     0   0 my $class = shift;
7724 0         0 my $cmd = shift;
7725 0         0 my $accountToken = shift;
7726              
7727 0         0 my $o = bless {
7728             cmd => $cmd,
7729             accountToken => $accountToken,
7730             countValid => 0,
7731             countInvalid => 0,
7732             };
7733              
7734 0         0 $cmd->{ui}->space;
7735 0         0 $cmd->{ui}->title('Messages of ', $cmd->{actor}->blueAccountReference($accountToken));
7736              
7737             # Get the public key
7738 0   0     0 my $publicKey = $o->getPublicKey // return;
7739              
7740             # Read all messages
7741 0         0 my $publicKeyCache = CDS::PublicKeyCache->new(128);
7742 0         0 my $pool = CDS::MessageBoxReaderPool->new($cmd->{keyPairToken}->keyPair, $publicKeyCache, $o);
7743 0         0 my $reader = CDS::MessageBoxReader->new($pool, CDS::ActorOnStore->new($publicKey, $accountToken->cliStore));
7744 0         0 $reader->read;
7745              
7746 0 0       0 $cmd->{ui}->line($cmd->{ui}->gray('No messages.')) if $o->{countValid} + $o->{countInvalid} == 0;
7747             }
7748              
7749             sub getPublicKey {
7750 0     0   0 my $o = shift;
7751              
7752             # Use the keypair's public key if possible
7753 0 0       0 return $o->{cmd}->{keyPairToken}->keyPair->publicKey if $o->{accountToken}->actorHash->equals($o->{cmd}->{keyPairToken}->keyPair->publicKey->hash);
7754              
7755             # Retrieve the public key
7756 0         0 return $o->{cmd}->{actor}->uiGetPublicKey($o->{accountToken}->actorHash, $o->{accountToken}->cliStore, $o->{cmd}->{keyPairToken});
7757             }
7758              
7759             sub onMessageBoxVerifyStore {
7760 0     0   0 my $o = shift;
7761 0         0 my $senderStoreUrl = shift;
7762 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
7763 0 0 0     0 my $envelope = shift; die 'wrong type '.ref($envelope).' for $envelope' if defined $envelope && ref $envelope ne 'CDS::Record';
  0         0  
7764 0 0 0     0 my $senderHash = shift; die 'wrong type '.ref($senderHash).' for $senderHash' if defined $senderHash && ref $senderHash ne 'CDS::Hash';
  0         0  
7765              
7766 0         0 return $o->{cmd}->{actor}->storeForUrl($senderStoreUrl);
7767             }
7768              
7769             sub onMessageBoxEntry {
7770 0     0   0 my $o = shift;
7771 0         0 my $message = shift;
7772              
7773 0         0 $o->{countValid} += 1;
7774 0         0 $o->{cmd}->{countValid} += 1;
7775              
7776 0         0 my $ui = $o->{cmd}->{ui};
7777 0         0 my $sender = CDS::AccountToken->new($message->sender->store, $message->sender->publicKey->hash);
7778              
7779 0         0 $ui->space;
7780 0         0 $ui->title($message->source->hash->hex);
7781 0         0 $ui->line('from ', $o->{cmd}->{actor}->blueAccountReference($sender));
7782 0         0 $ui->line('for ', $o->{cmd}->{actor}->blueAccountReference($o->{accountToken}));
7783 0         0 $ui->space;
7784 0         0 $ui->recordChildren($message->content);
7785             }
7786              
7787             sub onMessageBoxInvalidEntry {
7788 0     0   0 my $o = shift;
7789 0 0 0     0 my $source = shift; die 'wrong type '.ref($source).' for $source' if defined $source && ref $source ne 'CDS::Source';
  0         0  
7790 0         0 my $reason = shift;
7791              
7792 0         0 $o->{countInvalid} += 1;
7793 0         0 $o->{cmd}->{countInvalid} += 1;
7794              
7795 0         0 my $ui = $o->{cmd}->{ui};
7796 0         0 my $hashHex = $source->hash->hex;
7797 0         0 my $storeReference = $o->{cmd}->{actor}->storeReference($o->{accountToken}->cliStore);
7798              
7799 0         0 $ui->space;
7800 0         0 $ui->title($hashHex);
7801 0         0 $ui->pOrange($reason);
7802 0         0 $ui->space;
7803 0         0 $ui->p('You may use the following commands to check out the envelope:');
7804 0         0 $ui->line($ui->gold(' cds open envelope ', $hashHex, ' on ', $storeReference));
7805 0         0 $ui->line($ui->gold(' cds show record ', $hashHex, ' on ', $storeReference));
7806 0         0 $ui->line($ui->gold(' cds show hashes and data of ', $hashHex, ' on ', $storeReference));
7807             }
7808              
7809             # BEGIN AUTOGENERATED
7810             package CDS::Commands::ShowObject;
7811              
7812             sub register {
7813 0     0   0 my $class = shift;
7814 0         0 my $cds = shift;
7815 0         0 my $help = shift;
7816              
7817 0         0 my $node000 = CDS::Parser::Node->new(0);
7818 0         0 my $node001 = CDS::Parser::Node->new(0);
7819 0         0 my $node002 = CDS::Parser::Node->new(0);
7820 0         0 my $node003 = CDS::Parser::Node->new(0);
7821 0         0 my $node004 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
7822 0         0 my $node005 = CDS::Parser::Node->new(1);
7823 0         0 my $node006 = CDS::Parser::Node->new(0);
7824 0         0 my $node007 = CDS::Parser::Node->new(0);
7825 0         0 my $node008 = CDS::Parser::Node->new(0);
7826 0         0 my $node009 = CDS::Parser::Node->new(0);
7827 0         0 my $node010 = CDS::Parser::Node->new(1);
7828 0         0 my $node011 = CDS::Parser::Node->new(0);
7829 0         0 my $node012 = CDS::Parser::Node->new(0);
7830 0         0 my $node013 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&show});
7831 0         0 $cds->addArrow($node000, 1, 0, 'show');
7832 0         0 $cds->addArrow($node001, 1, 0, 'show');
7833 0         0 $cds->addArrow($node003, 1, 0, 'show');
7834 0         0 $help->addArrow($node002, 1, 0, 'show');
7835 0         0 $node000->addArrow($node006, 1, 0, 'object', \&collectObject);
7836 0         0 $node001->addArrow($node006, 1, 0, 'record', \&collectRecord);
7837 0         0 $node002->addArrow($node004, 1, 0, 'bytes');
7838 0         0 $node002->addArrow($node004, 1, 0, 'data');
7839 0         0 $node002->addArrow($node004, 1, 0, 'hash');
7840 0         0 $node002->addArrow($node004, 1, 0, 'hashes');
7841 0         0 $node002->addArrow($node004, 1, 0, 'object');
7842 0         0 $node002->addArrow($node004, 1, 0, 'record');
7843 0         0 $node002->addArrow($node004, 1, 0, 'size');
7844 0         0 $node003->addArrow($node005, 1, 0, 'bytes', \&collectBytes);
7845 0         0 $node003->addArrow($node005, 1, 0, 'data', \&collectData);
7846 0         0 $node003->addArrow($node005, 1, 0, 'hash', \&collectHash);
7847 0         0 $node003->addArrow($node005, 1, 0, 'hashes', \&collectHashes);
7848 0         0 $node003->addArrow($node005, 1, 0, 'record', \&collectRecord);
7849 0         0 $node003->addArrow($node005, 1, 0, 'size', \&collectSize);
7850 0         0 $node005->addArrow($node003, 1, 0, 'and');
7851 0         0 $node005->addArrow($node006, 1, 0, 'of');
7852 0         0 $node006->addArrow($node007, 1, 0, 'HASH', \&collectHash1);
7853 0         0 $node006->addArrow($node010, 1, 1, 'FILE', \&collectFile);
7854 0         0 $node006->addArrow($node010, 1, 0, 'HASH', \&collectHash2);
7855 0         0 $node006->addArrow($node010, 1, 0, 'OBJECT', \&collectObject1);
7856 0         0 $node007->addArrow($node008, 1, 0, 'on');
7857 0         0 $node007->addArrow($node009, 0, 0, 'from');
7858 0         0 $node008->addArrow($node010, 1, 0, 'STORE', \&collectStore);
7859 0         0 $node009->addArrow($node010, 0, 0, 'STORE', \&collectStore);
7860 0         0 $node010->addArrow($node011, 1, 0, 'decrypted');
7861 0         0 $node010->addDefault($node013);
7862 0         0 $node011->addArrow($node012, 1, 0, 'with');
7863 0         0 $node012->addArrow($node013, 1, 0, 'AESKEY', \&collectAeskey);
7864             }
7865              
7866             sub collectAeskey {
7867 0     0   0 my $o = shift;
7868 0         0 my $label = shift;
7869 0         0 my $value = shift;
7870              
7871 0         0 $o->{aesKey} = $value;
7872             }
7873              
7874             sub collectBytes {
7875 0     0   0 my $o = shift;
7876 0         0 my $label = shift;
7877 0         0 my $value = shift;
7878              
7879 0         0 $o->{showBytes} = 1;
7880             }
7881              
7882             sub collectData {
7883 0     0   0 my $o = shift;
7884 0         0 my $label = shift;
7885 0         0 my $value = shift;
7886              
7887 0         0 $o->{showData} = 1;
7888             }
7889              
7890             sub collectFile {
7891 0     0   0 my $o = shift;
7892 0         0 my $label = shift;
7893 0         0 my $value = shift;
7894              
7895 0         0 $o->{file} = $value;
7896             }
7897              
7898             sub collectHash {
7899 0     0   0 my $o = shift;
7900 0         0 my $label = shift;
7901 0         0 my $value = shift;
7902              
7903 0         0 $o->{showHash} = 1;
7904             }
7905              
7906             sub collectHash1 {
7907 0     0   0 my $o = shift;
7908 0         0 my $label = shift;
7909 0         0 my $value = shift;
7910              
7911 0         0 $o->{hash} = $value;
7912             }
7913              
7914             sub collectHash2 {
7915 0     0   0 my $o = shift;
7916 0         0 my $label = shift;
7917 0         0 my $value = shift;
7918              
7919 0         0 $o->{hash} = $value;
7920 0         0 $o->{store} = $o->{actor}->preferredStore;
7921             }
7922              
7923             sub collectHashes {
7924 0     0   0 my $o = shift;
7925 0         0 my $label = shift;
7926 0         0 my $value = shift;
7927              
7928 0         0 $o->{showHashes} = 1;
7929             }
7930              
7931             sub collectObject {
7932 0     0   0 my $o = shift;
7933 0         0 my $label = shift;
7934 0         0 my $value = shift;
7935              
7936 0         0 $o->{showHashes} = 1;
7937 0         0 $o->{showData} = 1;
7938             }
7939              
7940             sub collectObject1 {
7941 0     0   0 my $o = shift;
7942 0         0 my $label = shift;
7943 0         0 my $value = shift;
7944              
7945 0         0 $o->{hash} = $value->hash;
7946 0         0 $o->{store} = $value->cliStore;
7947             }
7948              
7949             sub collectRecord {
7950 0     0   0 my $o = shift;
7951 0         0 my $label = shift;
7952 0         0 my $value = shift;
7953              
7954 0         0 $o->{showRecord} = 1;
7955             }
7956              
7957             sub collectSize {
7958 0     0   0 my $o = shift;
7959 0         0 my $label = shift;
7960 0         0 my $value = shift;
7961              
7962 0         0 $o->{showSize} = 1;
7963             }
7964              
7965             sub collectStore {
7966 0     0   0 my $o = shift;
7967 0         0 my $label = shift;
7968 0         0 my $value = shift;
7969              
7970 0         0 $o->{store} = $value;
7971             }
7972              
7973             sub new {
7974 0     0   0 my $class = shift;
7975 0         0 my $actor = shift;
7976 0         0 bless {actor => $actor, ui => $actor->ui} }
7977              
7978             # END AUTOGENERATED
7979              
7980             # HTML FOLDER NAME show-object
7981             # HTML TITLE Show objects
7982             sub help {
7983 0     0   0 my $o = shift;
7984 0         0 my $cmd = shift;
7985              
7986 0         0 my $ui = $o->{ui};
7987 0         0 $ui->space;
7988 0         0 $ui->command('cds show record OBJECT');
7989 0         0 $ui->command('cds show record HASH on STORE');
7990 0         0 $ui->p('Downloads an object, and shows the containing record. The stores are tried in the order they are indicated, until one succeeds. If the object is not found, or not a valid Condensation object, the program quits with exit code 1.');
7991 0         0 $ui->space;
7992 0         0 $ui->line('The following object properties can be displayed:');
7993 0         0 $ui->line(' cds show hash of …');
7994 0         0 $ui->line(' cds show size of …');
7995 0         0 $ui->line(' cds show bytes of …');
7996 0         0 $ui->line(' cds show hashes of …');
7997 0         0 $ui->line(' cds show data of …');
7998 0         0 $ui->line(' cds show record …');
7999 0         0 $ui->space;
8000 0         0 $ui->p('Multiple properties may be combined with "and", e.g.:');
8001 0         0 $ui->line(' cds show size and hashes and record of …');
8002 0         0 $ui->space;
8003 0         0 $ui->command('cds show record HASH');
8004 0         0 $ui->p('As above, but uses the selected store.');
8005 0         0 $ui->space;
8006 0         0 $ui->command('cds show record FILE');
8007 0         0 $ui->p('As above, but loads the object from FILE rather than from an object store.');
8008 0         0 $ui->space;
8009 0         0 $ui->command('… decrypted with AESKEY');
8010 0         0 $ui->p('Decrypts the object after retrieval.');
8011 0         0 $ui->space;
8012 0         0 $ui->command('cds show object …');
8013 0         0 $ui->p('A shortcut for "cds show hashes and data of …".');
8014 0         0 $ui->space;
8015 0         0 $ui->title('Related commands');
8016 0         0 $ui->line('cds get OBJECT [decrypted with AESKEY]');
8017 0         0 $ui->line('cds save [data of] OBJECT [decrypted with AESKEY] as FILE');
8018 0         0 $ui->line('cds open envelope OBJECT [on STORE] [using KEYPAIR]');
8019 0         0 $ui->line('cds show document OBJECT [on STORE]');
8020 0         0 $ui->space;
8021             }
8022              
8023             sub show {
8024 0     0   0 my $o = shift;
8025 0         0 my $cmd = shift;
8026              
8027 0         0 $cmd->collect($o);
8028              
8029             # Get and decrypt the object
8030 0 0       0 $o->{object} = defined $o->{file} ? $o->loadObjectFromFile : $o->loadObjectFromStore;
8031 0 0       0 return if ! $o->{object};
8032 0 0       0 $o->{object} = $o->{object}->crypt($o->{aesKey}) if defined $o->{aesKey};
8033              
8034             # Show the desired information
8035 0 0       0 $o->showHash if $o->{showHash};
8036 0 0       0 $o->showSize if $o->{showSize};
8037 0 0       0 $o->showBytes if $o->{showBytes};
8038 0 0       0 $o->showHashes if $o->{showHashes};
8039 0 0       0 $o->showData if $o->{showData};
8040 0 0       0 $o->showRecord if $o->{showRecord};
8041 0         0 $o->{ui}->space;
8042             }
8043              
8044             sub loadObjectFromFile {
8045 0     0   0 my $o = shift;
8046              
8047 0   0     0 my $bytes = CDS->readBytesFromFile($o->{file}) // return $o->{ui}->error('Unable to read "', $o->{file}, '".');
8048 0   0     0 return CDS::Object->fromBytes($bytes) // return $o->{ui}->error('"', $o->{file}, '" does not contain a valid Condensation object.');
8049             }
8050              
8051             sub loadObjectFromStore {
8052 0     0   0 my $o = shift;
8053              
8054 0         0 return $o->{actor}->uiGetObject($o->{hash}, $o->{store}, $o->{actor}->preferredKeyPairToken);
8055             }
8056              
8057             sub loadCommand {
8058 0     0   0 my $o = shift;
8059              
8060 0 0       0 my $decryption = defined $o->{aesKey} ? ' decrypted with '.unpack('H*', $o->{aesKey}) : '';
8061 0 0       0 return $o->{file}.$decryption if defined $o->{file};
8062 0         0 return $o->{hash}->hex.' on '.$o->{actor}->storeReference($o->{store}).$decryption;
8063             }
8064              
8065             sub showHash {
8066 0     0   0 my $o = shift;
8067              
8068 0         0 $o->{ui}->space;
8069 0         0 $o->{ui}->title('Object hash');
8070 0         0 $o->{ui}->line($o->{object}->calculateHash->hex);
8071             }
8072              
8073             sub showSize {
8074 0     0   0 my $o = shift;
8075              
8076 0         0 $o->{ui}->space;
8077 0         0 $o->{ui}->title('Object size');
8078 0         0 $o->{ui}->line($o->{ui}->niceFileSize(length $o->{object}->bytes), ' total (', length $o->{object}->bytes, ' bytes)');
8079 0         0 $o->{ui}->line($o->{object}->hashesCount, ' hashes (', length $o->{object}->header, ' bytes)');
8080 0         0 $o->{ui}->line($o->{ui}->niceFileSize(length $o->{object}->data), ' data (', length $o->{object}->data, ' bytes)');
8081             }
8082              
8083             sub showBytes {
8084 0     0   0 my $o = shift;
8085              
8086 0         0 $o->{ui}->space;
8087 0         0 my $bytes = $o->{object}->bytes;
8088 0         0 $o->{ui}->title('Object bytes (', $o->{ui}->niceFileSize(length $bytes), ')');
8089 0 0       0 return if ! length $bytes;
8090              
8091 0         0 my $hexDump = $o->{ui}->hexDump($bytes);
8092 0         0 my $dataStart = $hexDump->styleHashList(0);
8093 0 0       0 my $end = $dataStart ? $hexDump->styleRecord($dataStart) : 0;
8094 0         0 $hexDump->changeStyle({at => $end, style => $hexDump->reset});
8095 0         0 $hexDump->display;
8096             }
8097              
8098             sub showHashes {
8099 0     0   0 my $o = shift;
8100              
8101 0         0 $o->{ui}->space;
8102 0         0 my $hashesCount = $o->{object}->hashesCount;
8103 0 0       0 $o->{ui}->title($hashesCount == 1 ? '1 hash' : $hashesCount.' hashes');
8104 0         0 my $count = 0;
8105 0         0 for my $hash ($o->{object}->hashes) {
8106 0         0 $o->{ui}->line($o->{ui}->violet(unpack('H4', pack('S>', $count))), ' ', $hash->hex);
8107 0         0 $count += 1;
8108             }
8109             }
8110              
8111             sub showData {
8112 0     0   0 my $o = shift;
8113              
8114 0         0 $o->{ui}->space;
8115 0         0 my $data = $o->{object}->data;
8116 0         0 $o->{ui}->title('Data (', $o->{ui}->niceFileSize(length $data), ')');
8117 0 0       0 return if ! length $data;
8118              
8119 0         0 my $hexDump = $o->{ui}->hexDump($data);
8120 0         0 my $end = $hexDump->styleRecord(0);
8121 0         0 $hexDump->changeStyle({at => $end, style => $hexDump->reset});
8122 0         0 $hexDump->display;
8123             }
8124              
8125             sub showRecord {
8126 0     0   0 my $o = shift;
8127              
8128             # Title
8129 0         0 $o->{ui}->space;
8130 0         0 $o->{ui}->title('Data interpreted as record');
8131              
8132             # Empty object (empty record)
8133 0 0       0 return $o->{ui}->line($o->{ui}->gray('(empty record)')) if ! length $o->{object}->data;
8134              
8135             # Record
8136 0         0 my $record = CDS::Record->new;
8137 0         0 my $reader = CDS::RecordReader->new($o->{object});
8138 0         0 $reader->readChildren($record);
8139 0 0       0 if ($reader->hasError) {
8140 0         0 $o->{ui}->pRed('This is not a record.');
8141 0         0 $o->{ui}->space;
8142 0         0 $o->{ui}->p('You may use one of the following commands to check out the content:');
8143 0         0 $o->{ui}->line($o->{ui}->gold(' cds show object ', $o->loadCommand));
8144 0         0 $o->{ui}->line($o->{ui}->gold(' cds show data of ', $o->loadCommand));
8145 0         0 $o->{ui}->line($o->{ui}->gold(' cds save data of ', $o->loadCommand, ' as FILENAME'));
8146 0         0 return;
8147             }
8148              
8149 0 0       0 $o->{ui}->recordChildren($record, $o->{store} ? $o->{actor}->blueStoreReference($o->{store}) : '');
8150              
8151             # Trailer
8152 0         0 my $trailer = $reader->trailer;
8153 0 0       0 if (length $trailer) {
8154 0         0 $o->{ui}->space;
8155 0         0 $o->{ui}->pRed('This is probably not a record, because ', length $trailer, ' bytes remain behind the record. Use "cds show data of …" to investigate the raw object content. If this object is encrypted, provide the decryption key using "… and decrypted with KEY".');
8156 0         0 $o->{ui}->space;
8157             }
8158             }
8159              
8160             # BEGIN AUTOGENERATED
8161             package CDS::Commands::ShowPrivateData;
8162              
8163             sub register {
8164 0     0   0 my $class = shift;
8165 0         0 my $cds = shift;
8166 0         0 my $help = shift;
8167              
8168 0         0 my $node000 = CDS::Parser::Node->new(0);
8169 0         0 my $node001 = CDS::Parser::Node->new(0);
8170 0         0 my $node002 = CDS::Parser::Node->new(0);
8171 0         0 my $node003 = CDS::Parser::Node->new(0);
8172 0         0 my $node004 = CDS::Parser::Node->new(0);
8173 0         0 my $node005 = CDS::Parser::Node->new(0);
8174 0         0 my $node006 = CDS::Parser::Node->new(0);
8175 0         0 my $node007 = CDS::Parser::Node->new(0);
8176 0         0 my $node008 = CDS::Parser::Node->new(0);
8177 0         0 my $node009 = CDS::Parser::Node->new(0);
8178 0         0 my $node010 = CDS::Parser::Node->new(0);
8179 0         0 my $node011 = CDS::Parser::Node->new(0);
8180 0         0 my $node012 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
8181 0         0 my $node013 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&showGroupData});
8182 0         0 my $node014 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&showLocalData});
8183 0         0 my $node015 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&showSentList});
8184 0         0 my $node016 = CDS::Parser::Node->new(0);
8185 0         0 my $node017 = CDS::Parser::Node->new(0);
8186 0         0 my $node018 = CDS::Parser::Node->new(0);
8187 0         0 my $node019 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&showSentList});
8188 0         0 $cds->addArrow($node006, 1, 0, 'show');
8189 0         0 $cds->addArrow($node007, 1, 0, 'show');
8190 0         0 $cds->addArrow($node008, 1, 0, 'show');
8191 0         0 $help->addArrow($node000, 1, 0, 'show');
8192 0         0 $help->addArrow($node001, 1, 0, 'show');
8193 0         0 $help->addArrow($node002, 1, 0, 'show');
8194 0         0 $node000->addArrow($node003, 1, 0, 'group');
8195 0         0 $node001->addArrow($node004, 1, 0, 'local');
8196 0         0 $node002->addArrow($node005, 1, 0, 'sent');
8197 0         0 $node003->addArrow($node012, 1, 0, 'data');
8198 0         0 $node004->addArrow($node012, 1, 0, 'data');
8199 0         0 $node005->addArrow($node012, 1, 0, 'list');
8200 0         0 $node006->addArrow($node009, 1, 0, 'group');
8201 0         0 $node007->addArrow($node010, 1, 0, 'local');
8202 0         0 $node008->addArrow($node011, 1, 0, 'sent');
8203 0         0 $node009->addArrow($node013, 1, 0, 'data');
8204 0         0 $node010->addArrow($node014, 1, 0, 'data');
8205 0         0 $node011->addArrow($node015, 1, 0, 'list');
8206 0         0 $node015->addArrow($node016, 1, 0, 'ordered');
8207 0         0 $node016->addArrow($node017, 1, 0, 'by');
8208 0         0 $node017->addArrow($node018, 1, 0, 'envelope');
8209 0         0 $node017->addArrow($node019, 1, 0, 'date', \&collectDate);
8210 0         0 $node017->addArrow($node019, 1, 0, 'id', \&collectId);
8211 0         0 $node018->addArrow($node019, 1, 0, 'hash', \&collectHash);
8212             }
8213              
8214             sub collectDate {
8215 0     0   0 my $o = shift;
8216 0         0 my $label = shift;
8217 0         0 my $value = shift;
8218              
8219 0         0 $o->{orderedBy} = 'date';
8220             }
8221              
8222             sub collectHash {
8223 0     0   0 my $o = shift;
8224 0         0 my $label = shift;
8225 0         0 my $value = shift;
8226              
8227 0         0 $o->{orderedBy} = 'envelope hash';
8228             }
8229              
8230             sub collectId {
8231 0     0   0 my $o = shift;
8232 0         0 my $label = shift;
8233 0         0 my $value = shift;
8234              
8235 0         0 $o->{orderedBy} = 'id';
8236             }
8237              
8238             sub new {
8239 0     0   0 my $class = shift;
8240 0         0 my $actor = shift;
8241 0         0 bless {actor => $actor, ui => $actor->ui} }
8242              
8243             # END AUTOGENERATED
8244              
8245             # HTML FOLDER NAME show-private-data
8246             # HTML TITLE Show the private data
8247             sub help {
8248 0     0   0 my $o = shift;
8249 0         0 my $cmd = shift;
8250              
8251 0         0 my $ui = $o->{ui};
8252 0         0 $ui->space;
8253 0         0 $ui->command('cds show group data');
8254 0         0 $ui->p('Shows the group document. This document is shared among all group members.');
8255 0         0 $ui->space;
8256 0         0 $ui->command('cds show local data');
8257 0         0 $ui->p('Shows the local document. This document is stored locally, and private to this actor.');
8258 0         0 $ui->space;
8259 0         0 $ui->command('cds show sent list');
8260 0         0 $ui->p('Shows the list of sent messages with their expiry date, envelope hash, and content hash.');
8261 0         0 $ui->space;
8262 0         0 $ui->command('… ordered by id');
8263 0         0 $ui->command('… ordered by date');
8264 0         0 $ui->command('… ordered by envelope hash');
8265 0         0 $ui->p('Sorts the list accordingly. By default, the list is sorted by id.');
8266 0         0 $ui->space;
8267             }
8268              
8269             sub showGroupData {
8270 0     0   0 my $o = shift;
8271 0         0 my $cmd = shift;
8272              
8273 0         0 $o->{ui}->space;
8274 0         0 $o->{ui}->selector($o->{actor}->groupRoot, 'Group data');
8275 0         0 $o->{ui}->space;
8276             }
8277              
8278             sub showLocalData {
8279 0     0   0 my $o = shift;
8280 0         0 my $cmd = shift;
8281              
8282 0         0 $o->{ui}->space;
8283 0         0 $o->{ui}->selector($o->{actor}->localRoot, 'Local data');
8284 0         0 $o->{ui}->space;
8285             }
8286              
8287             sub showSentList {
8288 0     0   0 my $o = shift;
8289 0         0 my $cmd = shift;
8290              
8291 0         0 $o->{orderedBy} = 'id';
8292 0         0 $cmd->collect($o);
8293              
8294 0         0 $o->{ui}->space;
8295 0         0 $o->{ui}->title('Sent list');
8296              
8297 0   0     0 $o->{actor}->procureSentList // return;
8298 0         0 my $sentList = $o->{actor}->sentList;
8299 0         0 my @items = sort { $a->id cmp $b->id } values %{$sentList->{items}};
  0         0  
  0         0  
8300 0 0       0 @items = sort { $a->envelopeHashBytes cmp $b->envelopeHashBytes } @items if $o->{orderedBy} eq 'envelope hash';
  0         0  
8301 0 0       0 @items = sort { $a->validUntil <=> $b->validUntil } @items if $o->{orderedBy} eq 'date';
  0         0  
8302 0         0 my $noHash = '-' x 64;
8303 0         0 for my $item (@items) {
8304 0         0 my $id = $item->id;
8305 0         0 my $envelopeHash = $item->envelopeHash;
8306 0         0 my $message = $item->message;
8307 0         0 my $label = $o->{ui}->niceBytes($id, 32);
8308 0 0       0 $o->{ui}->line($o->{ui}->gray($o->{ui}->niceDateTimeLocal($item->validUntil)), ' ', $envelopeHash ? $envelopeHash->hex : $noHash, ' ', $o->{ui}->blue($label));
8309 0         0 $o->{ui}->recordChildren($message);
8310             }
8311              
8312 0         0 $o->{ui}->space;
8313             }
8314              
8315             # BEGIN AUTOGENERATED
8316             package CDS::Commands::ShowTree;
8317              
8318             sub register {
8319 0     0   0 my $class = shift;
8320 0         0 my $cds = shift;
8321 0         0 my $help = shift;
8322              
8323 0         0 my $node000 = CDS::Parser::Node->new(0);
8324 0         0 my $node001 = CDS::Parser::Node->new(0);
8325 0         0 my $node002 = CDS::Parser::Node->new(0);
8326 0         0 my $node003 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
8327 0         0 my $node004 = CDS::Parser::Node->new(0);
8328 0         0 my $node005 = CDS::Parser::Node->new(0);
8329 0         0 my $node006 = CDS::Parser::Node->new(0);
8330 0         0 my $node007 = CDS::Parser::Node->new(0);
8331 0         0 my $node008 = CDS::Parser::Node->new(0);
8332 0         0 my $node009 = CDS::Parser::Node->new(0);
8333 0         0 my $node010 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&showTree});
8334 0         0 $cds->addArrow($node001, 1, 0, 'show');
8335 0         0 $cds->addArrow($node002, 0, 0, 'show');
8336 0         0 $help->addArrow($node000, 1, 0, 'show');
8337 0         0 $node000->addArrow($node003, 1, 0, 'tree');
8338 0         0 $node001->addArrow($node004, 1, 0, 'tree');
8339 0         0 $node002->addArrow($node004, 0, 0, 'trees');
8340 0         0 $node004->addDefault($node005);
8341 0         0 $node004->addDefault($node006);
8342 0         0 $node004->addDefault($node007);
8343 0         0 $node005->addArrow($node005, 1, 0, 'HASH', \&collectHash);
8344 0         0 $node005->addArrow($node010, 1, 0, 'HASH', \&collectHash);
8345 0         0 $node006->addArrow($node006, 1, 0, 'HASH', \&collectHash);
8346 0         0 $node006->addArrow($node008, 1, 0, 'HASH', \&collectHash);
8347 0         0 $node007->addArrow($node007, 1, 0, 'OBJECT', \&collectObject);
8348 0         0 $node007->addArrow($node010, 1, 0, 'OBJECT', \&collectObject);
8349 0         0 $node008->addArrow($node009, 1, 0, 'on');
8350 0         0 $node009->addArrow($node010, 1, 0, 'STORE', \&collectStore);
8351             }
8352              
8353             sub collectHash {
8354 0     0   0 my $o = shift;
8355 0         0 my $label = shift;
8356 0         0 my $value = shift;
8357              
8358 0         0 push @{$o->{hashes}}, $value;
  0         0  
8359             }
8360              
8361             sub collectObject {
8362 0     0   0 my $o = shift;
8363 0         0 my $label = shift;
8364 0         0 my $value = shift;
8365              
8366 0         0 push @{$o->{objectTokens}}, $value;
  0         0  
8367             }
8368              
8369             sub collectStore {
8370 0     0   0 my $o = shift;
8371 0         0 my $label = shift;
8372 0         0 my $value = shift;
8373              
8374 0         0 $o->{store} = $value;
8375             }
8376              
8377             sub new {
8378 0     0   0 my $class = shift;
8379 0         0 my $actor = shift;
8380 0         0 bless {actor => $actor, ui => $actor->ui} }
8381              
8382             # END AUTOGENERATED
8383              
8384             # HTML FOLDER NAME show-tree
8385             # HTML TITLE Show trees
8386             sub help {
8387 0     0   0 my $o = shift;
8388 0         0 my $cmd = shift;
8389              
8390 0         0 my $ui = $o->{ui};
8391 0         0 $ui->space;
8392 0         0 $ui->command('cds show tree OBJECT*');
8393 0         0 $ui->command('cds show tree HASH* on STORE');
8394 0         0 $ui->p('Downloads a tree, and shows the tree hierarchy. If an object has been traversed before, it is listed as "reported above".');
8395 0         0 $ui->space;
8396 0         0 $ui->command('cds show tree HASH*');
8397 0         0 $ui->p('As above, but uses the selected store.');
8398 0         0 $ui->space;
8399             }
8400              
8401             sub showTree {
8402 0     0   0 my $o = shift;
8403 0         0 my $cmd = shift;
8404              
8405 0         0 $o->{keyPairToken} = $o->{actor}->preferredKeyPairToken;
8406 0         0 $o->{objectTokens} = [];
8407 0         0 $o->{hashes} = [];
8408 0         0 $cmd->collect($o);
8409              
8410             # Process all trees
8411 0         0 for my $objectToken (@{$o->{objectTokens}}) {
  0         0  
8412 0         0 $o->{ui}->space;
8413 0         0 $o->process($objectToken->hash, $objectToken->cliStore);
8414             }
8415              
8416 0 0       0 if (scalar @{$o->{hashes}}) {
  0         0  
8417 0   0     0 my $store = $o->{store} // $o->{actor}->preferredStore;
8418 0         0 for my $hash (@{$o->{hashes}}) {
  0         0  
8419 0         0 $o->{ui}->space;
8420 0         0 $o->process($hash, $store);
8421             }
8422             }
8423              
8424             # Report the total size
8425 0         0 my $totalSize = 0;
8426 0         0 my $totalDataSize = 0;
8427 0         0 map { $totalSize += $_->{size} ; $totalDataSize += $_->{dataSize} } values %{$o->{objects}};
  0         0  
  0         0  
  0         0  
8428 0         0 $o->{ui}->space;
8429 0         0 $o->{ui}->p(scalar keys %{$o->{objects}}, ' unique objects ', $o->{ui}->bold($o->{ui}->niceFileSize($totalSize)), $o->{ui}->gray(' (', $o->{ui}->niceFileSize($totalSize - $totalDataSize), ' header and ', $o->{ui}->niceFileSize($totalDataSize), ' data)'));
  0         0  
8430 0 0       0 $o->{ui}->pRed(scalar keys %{$o->{missingObjects}}, ' or more objects are missing') if scalar keys %{$o->{missingObjects}};
  0         0  
  0         0  
8431 0         0 $o->{ui}->space;
8432             }
8433              
8434             sub process {
8435 0     0   0 my $o = shift;
8436 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
8437 0         0 my $store = shift;
8438              
8439 0         0 my $hashHex = $hash->hex;
8440              
8441             # Check if we retrieved this object before
8442 0 0       0 if (exists $o->{objects}->{$hashHex}) {
8443 0         0 $o->{ui}->line($hash->hex, ' reported above') ;
8444 0         0 return 1;
8445             }
8446              
8447             # Retrieve the object
8448 0         0 my ($object, $storeError) = $store->get($hash, $o->{keyPairToken}->keyPair);
8449 0 0       0 return if defined $storeError;
8450              
8451 0 0       0 if (! $object) {
8452 0         0 $o->{missingObjects}->{$hashHex} = 1;
8453 0         0 return $o->{ui}->line($hashHex, ' ', $o->{ui}->red('is missing'));
8454             }
8455              
8456             # Display
8457 0         0 my $size = $object->byteLength;
8458 0         0 $o->{objects}->{$hashHex} = {size => $size, dataSize => length $object->data};
8459 0         0 $o->{ui}->line($hashHex, ' ', $o->{ui}->bold($o->{ui}->niceFileSize($size)), ' ', $o->{ui}->gray($object->hashesCount, ' hashes'));
8460              
8461             # Process all children
8462 0         0 $o->{ui}->pushIndent;
8463 0         0 foreach my $hash ($object->hashes) {
8464 0   0     0 $o->process($hash, $store) // return;
8465             }
8466 0         0 $o->{ui}->popIndent;
8467 0         0 return 1;
8468             }
8469              
8470             # BEGIN AUTOGENERATED
8471             package CDS::Commands::StartHTTPServer;
8472              
8473             sub register {
8474 0     0   0 my $class = shift;
8475 0         0 my $cds = shift;
8476 0         0 my $help = shift;
8477              
8478 0         0 my $node000 = CDS::Parser::Node->new(0);
8479 0         0 my $node001 = CDS::Parser::Node->new(0);
8480 0         0 my $node002 = CDS::Parser::Node->new(0);
8481 0         0 my $node003 = CDS::Parser::Node->new(0);
8482 0         0 my $node004 = CDS::Parser::Node->new(0);
8483 0         0 my $node005 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
8484 0         0 my $node006 = CDS::Parser::Node->new(0);
8485 0         0 my $node007 = CDS::Parser::Node->new(0);
8486 0         0 my $node008 = CDS::Parser::Node->new(0);
8487 0         0 my $node009 = CDS::Parser::Node->new(1);
8488 0         0 my $node010 = CDS::Parser::Node->new(0);
8489 0         0 my $node011 = CDS::Parser::Node->new(1);
8490 0         0 my $node012 = CDS::Parser::Node->new(0);
8491 0         0 my $node013 = CDS::Parser::Node->new(0);
8492 0         0 my $node014 = CDS::Parser::Node->new(0);
8493 0         0 my $node015 = CDS::Parser::Node->new(0);
8494 0         0 my $node016 = CDS::Parser::Node->new(1);
8495 0         0 my $node017 = CDS::Parser::Node->new(0);
8496 0         0 my $node018 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&startHttpServer});
8497 0         0 $cds->addArrow($node001, 1, 0, 'start');
8498 0         0 $help->addArrow($node000, 1, 0, 'http');
8499 0         0 $node000->addArrow($node005, 1, 0, 'server');
8500 0         0 $node001->addArrow($node002, 1, 0, 'http');
8501 0         0 $node002->addArrow($node003, 1, 0, 'server');
8502 0         0 $node003->addArrow($node004, 1, 0, 'for');
8503 0         0 $node004->addArrow($node006, 1, 0, 'STORE', \&collectStore);
8504 0         0 $node006->addArrow($node007, 1, 0, 'on');
8505 0         0 $node007->addArrow($node008, 1, 0, 'port');
8506 0         0 $node008->addArrow($node009, 1, 0, 'PORT', \&collectPort);
8507 0         0 $node009->addArrow($node010, 1, 0, 'at');
8508 0         0 $node009->addDefault($node011);
8509 0         0 $node010->addArrow($node011, 1, 0, 'TEXT', \&collectText);
8510 0         0 $node011->addArrow($node012, 1, 0, 'with');
8511 0         0 $node011->addDefault($node016);
8512 0         0 $node012->addArrow($node013, 1, 0, 'static');
8513 0         0 $node013->addArrow($node014, 1, 0, 'files');
8514 0         0 $node014->addArrow($node015, 1, 0, 'from');
8515 0         0 $node015->addArrow($node016, 1, 0, 'FOLDER', \&collectFolder);
8516 0         0 $node016->addArrow($node017, 1, 0, 'for');
8517 0         0 $node016->addDefault($node018);
8518 0         0 $node017->addArrow($node018, 1, 0, 'everybody', \&collectEverybody);
8519             }
8520              
8521             sub collectEverybody {
8522 0     0   0 my $o = shift;
8523 0         0 my $label = shift;
8524 0         0 my $value = shift;
8525              
8526 0         0 $o->{corsAllowEverybody} = 1;
8527             }
8528              
8529             sub collectFolder {
8530 0     0   0 my $o = shift;
8531 0         0 my $label = shift;
8532 0         0 my $value = shift;
8533              
8534 0         0 $o->{staticFolder} = $value;
8535             }
8536              
8537             sub collectPort {
8538 0     0   0 my $o = shift;
8539 0         0 my $label = shift;
8540 0         0 my $value = shift;
8541              
8542 0         0 $o->{port} = $value;
8543             }
8544              
8545             sub collectStore {
8546 0     0   0 my $o = shift;
8547 0         0 my $label = shift;
8548 0         0 my $value = shift;
8549              
8550 0         0 $o->{store} = $value;
8551             }
8552              
8553             sub collectText {
8554 0     0   0 my $o = shift;
8555 0         0 my $label = shift;
8556 0         0 my $value = shift;
8557              
8558 0         0 $o->{root} = $value;
8559             }
8560              
8561             sub new {
8562 0     0   0 my $class = shift;
8563 0         0 my $actor = shift;
8564 0         0 bless {actor => $actor, ui => $actor->ui} }
8565              
8566             # END AUTOGENERATED
8567              
8568             # HTML FOLDER NAME start-http-server
8569             # HTML TITLE HTTP store server
8570             sub help {
8571 0     0   0 my $o = shift;
8572 0         0 my $cmd = shift;
8573              
8574 0         0 my $ui = $o->{ui};
8575 0         0 $ui->space;
8576 0         0 $ui->command('cds start http server for STORE on port PORT');
8577 0         0 $ui->p('Starts a simple HTTP server listening on port PORT. The server handles requests within /objects and /accounts, and uses STORE as backend. Requests on the root URL (/) deliver a short message.');
8578 0         0 $ui->p('You may need superuser (root) privileges to use the default HTTP port 80.');
8579 0         0 $ui->p('This server is very useful for small to medium-size projects, but not particularly efficient for large-scale applications. It makes no effort to use DMA or similar features to speed up delivery, and handles only one request at a time (single-threaded). However, when using a front-end web server with load-balancing capabilities, multiple HTTP servers for the same store may be started to handle multiple requests in parallel.');
8580 0         0 $ui->space;
8581 0         0 $ui->command('… at TEXT');
8582 0         0 $ui->p('As above, but makes the store accessible at /TEXT/objects and /TEXT/accounts.');
8583 0         0 $ui->space;
8584 0         0 $ui->command('… with static files from FOLDER');
8585 0         0 $ui->p('Delivers static files from FOLDER for URLs outside of /objects and /accounts. This is useful for self-contained web apps.');
8586 0         0 $ui->space;
8587 0         0 $ui->command('… for everybody');
8588 0         0 $ui->p('Sets CORS headers to allow everybody to access the store from within a web browser.');
8589 0         0 $ui->space;
8590 0         0 $ui->p('For more options, write a Perl script instantiating and configuring a CDS::HTTPServer.');
8591 0         0 $ui->space;
8592             }
8593              
8594             sub startHttpServer {
8595 0     0   0 my $o = shift;
8596 0         0 my $cmd = shift;
8597              
8598 0         0 $cmd->collect($o);
8599              
8600 0         0 my $httpServer = CDS::HTTPServer->new($o->{port});
8601 0         0 $httpServer->setLogger(CDS::Commands::StartHTTPServer::Logger->new($o->{ui}));
8602 0         0 $httpServer->setCorsAllowEverybody($o->{corsAllowEverybody});
8603 0   0     0 $httpServer->addHandler(CDS::HTTPServer::StoreHandler->new($o->{root} // '/', $o->{store}));
8604 0 0 0     0 $httpServer->addHandler(CDS::HTTPServer::IdentificationHandler->new($o->{root} // '/')) if ! defined $o->{staticFolder};
8605 0 0       0 $httpServer->addHandler(CDS::HTTPServer::StaticFilesHandler->new('/', $o->{staticFolder}, 'index.html')) if defined $o->{staticFolder};
8606 0         0 eval { $httpServer->run; };
  0         0  
8607 0 0       0 if ($@) {
8608 0         0 my $error = $@;
8609 0 0       0 $error = $1 if $error =~ /^(.*?)( at |\n)/;
8610 0         0 $o->{ui}->space;
8611 0         0 $o->{ui}->p('Failed to run server on port '.$o->{port}.': '.$error);
8612 0         0 $o->{ui}->space;
8613             }
8614             }
8615              
8616             package CDS::Commands::StartHTTPServer::Logger;
8617              
8618             sub new {
8619 0     0   0 my $class = shift;
8620 0         0 my $ui = shift;
8621              
8622 0         0 return bless {ui => $ui};
8623             }
8624              
8625             sub onServerStarts {
8626 0     0   0 my $o = shift;
8627 0         0 my $port = shift;
8628              
8629 0         0 my $ui = $o->{ui};
8630 0         0 $ui->space;
8631 0         0 $ui->line($o->{ui}->gray($ui->niceDateTimeLocal), ' ', $ui->green('Server ready at http://localhost:', $port));
8632             }
8633              
8634             sub onRequestStarts {
8635 0     0   0 my $o = shift;
8636 0         0 my $request = shift;
8637             }
8638              
8639             sub onRequestError {
8640 0     0   0 my $o = shift;
8641 0         0 my $request = shift;
8642              
8643 0         0 my $ui = $o->{ui};
8644 0         0 $ui->line($o->{ui}->gray($ui->niceDateTimeLocal), ' ', $ui->blue($ui->left(15, $request->peerAddress)), ' ', $request->method, ' ', $request->path, ' ', $ui->red(@_));
8645             }
8646              
8647             sub onRequestDone {
8648 0     0   0 my $o = shift;
8649 0         0 my $request = shift;
8650 0         0 my $responseCode = shift;
8651              
8652 0         0 my $ui = $o->{ui};
8653 0         0 $ui->line($o->{ui}->gray($ui->niceDateTimeLocal), ' ', $ui->blue($ui->left(15, $request->peerAddress)), ' ', $request->method, ' ', $request->path, ' ', $ui->bold($responseCode));
8654             }
8655              
8656             # BEGIN AUTOGENERATED
8657             package CDS::Commands::Transfer;
8658              
8659             sub register {
8660 0     0   0 my $class = shift;
8661 0         0 my $cds = shift;
8662 0         0 my $help = shift;
8663              
8664 0         0 my $node000 = CDS::Parser::Node->new(0);
8665 0         0 my $node001 = CDS::Parser::Node->new(0);
8666 0         0 my $node002 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
8667 0         0 my $node003 = CDS::Parser::Node->new(0);
8668 0         0 my $node004 = CDS::Parser::Node->new(0);
8669 0         0 my $node005 = CDS::Parser::Node->new(0);
8670 0         0 my $node006 = CDS::Parser::Node->new(0);
8671 0         0 my $node007 = CDS::Parser::Node->new(0);
8672 0         0 my $node008 = CDS::Parser::Node->new(0);
8673 0         0 my $node009 = CDS::Parser::Node->new(0);
8674 0         0 my $node010 = CDS::Parser::Node->new(0);
8675 0         0 my $node011 = CDS::Parser::Node->new(0);
8676 0         0 my $node012 = CDS::Parser::Node->new(0);
8677 0         0 my $node013 = CDS::Parser::Node->new(0);
8678 0         0 my $node014 = CDS::Parser::Node->new(0);
8679 0         0 my $node015 = CDS::Parser::Node->new(0);
8680 0         0 my $node016 = CDS::Parser::Node->new(0);
8681 0         0 my $node017 = CDS::Parser::Node->new(1);
8682 0         0 my $node018 = CDS::Parser::Node->new(0);
8683 0         0 my $node019 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&transfer});
8684 0         0 $cds->addArrow($node000, 1, 0, 'thoroughly');
8685 0         0 $cds->addArrow($node001, 0, 0, 'leniently');
8686 0         0 $cds->addDefault($node003);
8687 0         0 $cds->addArrow($node003, 1, 0, 'leniently', \&collectLeniently);
8688 0         0 $cds->addArrow($node003, 1, 0, 'thoroughly', \&collectThoroughly);
8689 0         0 $help->addArrow($node002, 1, 0, 'transfer');
8690 0         0 $node000->addArrow($node003, 1, 0, 'leniently', \&collectLeniently1);
8691 0         0 $node001->addArrow($node003, 0, 0, 'thoroughly', \&collectLeniently1);
8692 0         0 $node003->addArrow($node004, 1, 0, 'transfer');
8693 0         0 $node004->addDefault($node005);
8694 0         0 $node004->addDefault($node006);
8695 0         0 $node004->addDefault($node007);
8696 0         0 $node004->addDefault($node008);
8697 0         0 $node004->addArrow($node009, 1, 0, 'message');
8698 0         0 $node004->addDefault($node010);
8699 0         0 $node004->addArrow($node011, 1, 0, 'private');
8700 0         0 $node004->addArrow($node012, 1, 0, 'public');
8701 0         0 $node004->addArrow($node013, 1, 0, 'all', \&collectAll);
8702 0         0 $node004->addArrow($node013, 0, 0, 'messages', \&collectMessages);
8703 0         0 $node004->addArrow($node013, 0, 0, 'private', \&collectPrivate);
8704 0         0 $node004->addArrow($node013, 0, 0, 'public', \&collectPublic);
8705 0         0 $node005->addArrow($node005, 1, 0, 'HASH', \&collectHash);
8706 0         0 $node005->addArrow($node017, 1, 0, 'HASH', \&collectHash);
8707 0         0 $node006->addArrow($node006, 1, 0, 'OBJECT', \&collectObject);
8708 0         0 $node006->addArrow($node017, 1, 0, 'OBJECT', \&collectObject);
8709 0         0 $node007->addArrow($node007, 1, 0, 'ACCOUNT', \&collectAccount);
8710 0         0 $node007->addArrow($node017, 1, 0, 'ACCOUNT', \&collectAccount);
8711 0         0 $node008->addArrow($node008, 1, 0, 'BOX', \&collectBox);
8712 0         0 $node008->addArrow($node017, 1, 0, 'BOX', \&collectBox);
8713 0         0 $node009->addArrow($node013, 1, 0, 'box', \&collectMessages);
8714 0         0 $node010->addArrow($node010, 1, 0, 'HASH', \&collectHash);
8715 0         0 $node010->addArrow($node015, 1, 0, 'HASH', \&collectHash);
8716 0         0 $node011->addArrow($node013, 1, 0, 'box', \&collectPrivate);
8717 0         0 $node012->addArrow($node013, 1, 0, 'box', \&collectPublic);
8718 0         0 $node013->addArrow($node014, 1, 0, 'of');
8719 0         0 $node014->addArrow($node014, 1, 0, 'HASH', \&collectHash1);
8720 0         0 $node014->addArrow($node015, 1, 0, 'HASH', \&collectHash1);
8721 0         0 $node015->addArrow($node016, 1, 0, 'from');
8722 0         0 $node016->addArrow($node017, 1, 0, 'STORE', \&collectStore);
8723 0         0 $node017->addArrow($node018, 1, 0, 'to');
8724 0         0 $node018->addArrow($node018, 1, 0, 'STORE', \&collectStore1);
8725 0         0 $node018->addArrow($node019, 1, 0, 'STORE', \&collectStore1);
8726             }
8727              
8728             sub collectAccount {
8729 0     0   0 my $o = shift;
8730 0         0 my $label = shift;
8731 0         0 my $value = shift;
8732              
8733 0         0 push @{$o->{accountTokens}}, $value;
  0         0  
8734             }
8735              
8736             sub collectAll {
8737 0     0   0 my $o = shift;
8738 0         0 my $label = shift;
8739 0         0 my $value = shift;
8740              
8741 0         0 push @{$o->{boxLabels}}, 'public', 'private', 'messages';
  0         0  
8742             }
8743              
8744             sub collectBox {
8745 0     0   0 my $o = shift;
8746 0         0 my $label = shift;
8747 0         0 my $value = shift;
8748              
8749 0         0 push @{$o->{boxTokens}}, $value;
  0         0  
8750             }
8751              
8752             sub collectHash {
8753 0     0   0 my $o = shift;
8754 0         0 my $label = shift;
8755 0         0 my $value = shift;
8756              
8757 0         0 push @{$o->{objectHashes}}, $value;
  0         0  
8758             }
8759              
8760             sub collectHash1 {
8761 0     0   0 my $o = shift;
8762 0         0 my $label = shift;
8763 0         0 my $value = shift;
8764              
8765 0         0 push @{$o->{accountHashes}}, $value;
  0         0  
8766             }
8767              
8768             sub collectLeniently {
8769 0     0   0 my $o = shift;
8770 0         0 my $label = shift;
8771 0         0 my $value = shift;
8772              
8773 0         0 $o->{leniently} = 1;
8774             }
8775              
8776             sub collectLeniently1 {
8777 0     0   0 my $o = shift;
8778 0         0 my $label = shift;
8779 0         0 my $value = shift;
8780              
8781 0         0 $o->{leniently} = 1;
8782 0         0 $o->{thoroughly} = 1;
8783             }
8784              
8785             sub collectMessages {
8786 0     0   0 my $o = shift;
8787 0         0 my $label = shift;
8788 0         0 my $value = shift;
8789              
8790 0         0 push @{$o->{boxLabels}}, 'messages';
  0         0  
8791             }
8792              
8793             sub collectObject {
8794 0     0   0 my $o = shift;
8795 0         0 my $label = shift;
8796 0         0 my $value = shift;
8797              
8798 0         0 push @{$o->{objectTokens}}, $value;
  0         0  
8799             }
8800              
8801             sub collectPrivate {
8802 0     0   0 my $o = shift;
8803 0         0 my $label = shift;
8804 0         0 my $value = shift;
8805              
8806 0         0 push @{$o->{boxLabels}}, 'private';
  0         0  
8807             }
8808              
8809             sub collectPublic {
8810 0     0   0 my $o = shift;
8811 0         0 my $label = shift;
8812 0         0 my $value = shift;
8813              
8814 0         0 push @{$o->{boxLabels}}, 'public';
  0         0  
8815             }
8816              
8817             sub collectStore {
8818 0     0   0 my $o = shift;
8819 0         0 my $label = shift;
8820 0         0 my $value = shift;
8821              
8822 0         0 $o->{fromStore} = $value;
8823             }
8824              
8825             sub collectStore1 {
8826 0     0   0 my $o = shift;
8827 0         0 my $label = shift;
8828 0         0 my $value = shift;
8829              
8830 0         0 push @{$o->{toStores}}, $value;
  0         0  
8831             }
8832              
8833             sub collectThoroughly {
8834 0     0   0 my $o = shift;
8835 0         0 my $label = shift;
8836 0         0 my $value = shift;
8837              
8838 0         0 $o->{thoroughly} = 1;
8839             }
8840              
8841             sub new {
8842 0     0   0 my $class = shift;
8843 0         0 my $actor = shift;
8844 0         0 bless {actor => $actor, ui => $actor->ui} }
8845              
8846             # END AUTOGENERATED
8847              
8848             # HTML FOLDER NAME transfer
8849             # HTML TITLE Transfer
8850             sub help {
8851 0     0   0 my $o = shift;
8852 0         0 my $cmd = shift;
8853              
8854 0         0 my $ui = $o->{ui};
8855 0         0 $ui->space;
8856 0         0 $ui->command('cds transfer BOX* to STORE*');
8857 0         0 $ui->command('cds transfer ACCOUNT* to STORE*');
8858 0         0 $ui->command('cds transfer all of HASH* from STORE to STORE*');
8859 0         0 $ui->command('cds transfer BOXLABEL of HASH* from STORE to STORE*');
8860 0         0 $ui->p('Copies an account (or some of its boxes) including all referenced trees from one store to another. If the source store is omitted, the selected store is used.');
8861 0         0 $ui->space;
8862 0         0 $ui->command('cds transfer OBJECT* to STORE*');
8863 0         0 $ui->command('cds transfer HASH* from STORE to STORE*');
8864 0         0 $ui->p('Copies a tree from one store to another. If the source store is omitted, the selected store is used.');
8865 0         0 $ui->space;
8866 0         0 $ui->command('cds ', $ui->underlined('leniently'), ' transfer …');
8867 0         0 $ui->p('Warns about missing objects, but ignores them and proceeds with the rest.');
8868 0         0 $ui->space;
8869 0         0 $ui->command('cds ', $ui->underlined('thoroughly'), ' transfer …');
8870 0         0 $ui->p('Check subtrees of objects existing at the destination. This may be used to fix missing objects on the destination store.');
8871 0         0 $ui->space;
8872             }
8873              
8874             sub transfer {
8875 0     0   0 my $o = shift;
8876 0         0 my $cmd = shift;
8877              
8878             # Collect the arguments
8879 0         0 $o->{keyPairToken} = $o->{actor}->preferredKeyPairToken;
8880 0         0 $o->{accountTokens} = [];
8881 0         0 $o->{accountHashes} = [];
8882 0         0 $o->{boxTokens} = [];
8883 0         0 $o->{boxLabels} = [];
8884 0         0 $o->{objectTokens} = [];
8885 0         0 $o->{objectHashes} = [];
8886 0         0 $o->{toStores} = [];
8887 0         0 $cmd->collect($o);
8888              
8889             # Use the selected store
8890 0 0 0     0 $o->{fromStore} = $o->{actor}->preferredStore if (scalar @{$o->{accountHashes}} || scalar @{$o->{objectHashes}}) && ! $o->{fromStore};
      0        
8891              
8892             # Prepare the object tokens
8893 0         0 for my $hash (@{$o->{objectHashes}}) {
  0         0  
8894 0         0 push @{$o->{objectTokens}}, CDS::ObjectToken->new($o->{fromStore}, $hash);
  0         0  
8895             }
8896              
8897             # Prepare the account tokens
8898 0         0 for my $hash (@{$o->{accountHashes}}) {
  0         0  
8899 0         0 push @{$o->{accountTokens}}, CDS::AccountToken->new($o->{fromStore}, $hash);
  0         0  
8900             }
8901              
8902             # Prepare the box tokens
8903 0         0 for my $accountToken (@{$o->{accountTokens}}) {
  0         0  
8904 0         0 for my $boxLabel (@{$o->{boxLabels}}) {
  0         0  
8905 0         0 push @{$o->{boxTokens}}, CDS::BoxToken->new($accountToken, $boxLabel);
  0         0  
8906             }
8907             }
8908              
8909             # Copy the public key of every account first
8910 0         0 my %done;
8911 0         0 for my $boxToken (@{$o->{boxTokens}}) {
  0         0  
8912 0         0 my $actorHash = $boxToken->accountToken->actorHash;
8913 0 0       0 next if $done{$actorHash->bytes};
8914 0         0 $done{$actorHash->bytes} = 1;
8915 0         0 push @{$o->{objectTokens}}, CDS::ObjectToken->new($boxToken->accountToken->cliStore, $actorHash);
  0         0  
8916             }
8917              
8918             # Prepare the destination stores
8919 0         0 my $toStores = [];
8920 0         0 for my $toStore (@{$o->{toStores}}) {
  0         0  
8921 0         0 push @$toStores, {store => $toStore, storeError => undef, needed => [1]};
8922             }
8923              
8924             # Print the stores
8925 0         0 $o->{ui}->space;
8926 0         0 my $n = scalar @$toStores;
8927 0         0 for my $i (0 .. $n - 1) {
8928 0         0 my $toStore = $toStores->[$i];
8929 0         0 $o->{ui}->line($o->{ui}->gray(' │' x $i, ' ┌', '──' x ($n - $i), ' ', $toStore->{store}->url));
8930             }
8931              
8932             # Process all trees
8933 0         0 $o->{objects} = {};
8934 0         0 $o->{missingObjects} = {};
8935 0         0 for my $objectToken (@{$o->{objectTokens}}) {
  0         0  
8936 0         0 $o->{ui}->line($o->{ui}->gray(' │' x $n));
8937 0         0 $o->process($objectToken->hash, $objectToken->cliStore, $toStores, 1);
8938             }
8939              
8940             # Process all accounts
8941 0         0 my $keyPair = $o->{keyPairToken}->keyPair;
8942 0         0 for my $boxToken (@{$o->{boxTokens}}) {
  0         0  
8943 0         0 $o->{ui}->line($o->{ui}->gray(' │' x $n));
8944 0         0 $o->{ui}->line($o->{ui}->gray(' │' x $n, ' Transferring ', $boxToken->boxLabel, ' box of ', $boxToken->accountToken->actorHash->hex));
8945 0         0 my ($hashes, $listError) = $boxToken->accountToken->cliStore->list($boxToken->accountToken->actorHash, $boxToken->boxLabel, 0, $keyPair);
8946 0 0       0 next if $listError;
8947              
8948 0         0 for my $hash (@$hashes) {
8949 0   0     0 $o->process($hash, $boxToken->accountToken->cliStore, $toStores, 1) // next;
8950              
8951 0         0 for my $toStore (@$toStores) {
8952 0 0       0 next if defined $toStore->{storeError};
8953 0         0 $toStore->{storeError} = $toStore->{store}->add($boxToken->accountToken->actorHash, $boxToken->boxLabel, $hash, $keyPair);
8954             }
8955             }
8956             }
8957              
8958             # Print the stores again, with their errors
8959 0         0 $o->{ui}->line($o->{ui}->gray(' │' x $n));
8960 0         0 for my $i (reverse 0 .. $n - 1) {
8961 0         0 my $toStore = $toStores->[$i];
8962 0 0       0 $o->{ui}->line($o->{ui}->gray(' │' x $i, ' â””', '──' x ($n - $i), ' ', $toStore->{store}->url), ' ', defined $toStore->{storeError} ? $o->{ui}->red($toStore->{storeError}) : '');
8963             }
8964              
8965             # Report the total size
8966 0         0 my $totalSize = 0;
8967 0         0 my $totalDataSize = 0;
8968 0         0 map { $totalSize += $_->{size} ; $totalDataSize += $_->{dataSize} } values %{$o->{objects}};
  0         0  
  0         0  
  0         0  
8969 0         0 $o->{ui}->space;
8970 0         0 $o->{ui}->p(scalar keys %{$o->{objects}}, ' unique objects ', $o->{ui}->bold($o->{ui}->niceFileSize($totalSize)), ' ', $o->{ui}->gray($o->{ui}->niceFileSize($totalDataSize), ' data'));
  0         0  
8971 0 0       0 $o->{ui}->pOrange(scalar keys %{$o->{missingObjects}}, ' or more objects are missing') if scalar keys %{$o->{missingObjects}};
  0         0  
  0         0  
8972 0         0 $o->{ui}->space;
8973             }
8974              
8975             sub process {
8976 0     0   0 my $o = shift;
8977 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
8978 0         0 my $fromStore = shift;
8979 0         0 my $toStores = shift;
8980 0         0 my $depth = shift;
8981              
8982 0         0 my $hashHex = $hash->hex;
8983 0         0 my $keyPair = $o->{keyPairToken}->keyPair;
8984              
8985             # Check if we retrieved this object before
8986 0 0       0 if (exists $o->{objects}->{$hashHex}) {
8987 0         0 $o->report($hash->hex, $toStores, $depth, $o->{ui}->green('copied before'));
8988 0         0 return 1;
8989             }
8990              
8991             # Try to book the object on all active stores
8992 0         0 my $countNeeded = 0;
8993 0         0 my $hasActiveStore = 0;
8994 0         0 for my $toStore (@$toStores) {
8995 0 0       0 next if defined $toStore->{storeError};
8996 0         0 $hasActiveStore = 1;
8997 0 0 0     0 next if ! $o->{thoroughly} && ! $toStore->{needed}->[$depth - 1];
8998              
8999 0         0 my ($found, $bookError) = $toStore->{store}->book($hash);
9000 0 0       0 if (defined $bookError) {
9001 0         0 $toStore->{storeError} = $bookError;
9002 0         0 next;
9003             }
9004              
9005 0 0       0 next if $found;
9006 0         0 $toStore->{needed}->[$depth] = 1;
9007 0         0 $countNeeded += 1;
9008             }
9009              
9010             # Return if all stores reported an error
9011 0 0       0 return if ! $hasActiveStore;
9012              
9013             # Ignore existing subtrees at the destination unless "thoroughly" is set
9014 0 0 0     0 if (! $o->{thoroughly} && ! $countNeeded) {
9015 0         0 $o->report($hashHex, $toStores, $depth, $o->{ui}->gray('skipping subtree'));
9016 0         0 return 1;
9017             }
9018              
9019             # Retrieve the object
9020 0         0 my ($object, $getError) = $fromStore->get($hash, $keyPair);
9021 0 0       0 return if defined $getError;
9022              
9023 0 0       0 if (! defined $object) {
9024 0         0 $o->{missingObjects}->{$hashHex} = 1;
9025 0         0 $o->report($hashHex, $toStores, $depth, $o->{ui}->orange('is missing'));
9026 0 0       0 return if ! $o->{leniently};
9027             }
9028              
9029             # Display
9030 0         0 my $size = $object->byteLength;
9031 0         0 $o->{objects}->{$hashHex} = {needed => $countNeeded, size => $size, dataSize => length $object->data};
9032 0         0 $o->report($hashHex, $toStores, $depth, $o->{ui}->bold($o->{ui}->niceFileSize($size)), ' ', $o->{ui}->gray($object->hashesCount, ' hashes'));
9033              
9034             # Process all children
9035 0         0 foreach my $hash ($object->hashes) {
9036 0   0     0 $o->process($hash, $fromStore, $toStores, $depth + 1) // return;
9037             }
9038              
9039             # Write the object to all active stores
9040 0         0 for my $toStore (@$toStores) {
9041 0 0       0 next if defined $toStore->{storeError};
9042 0 0       0 next if ! $toStore->{needed}->[$depth];
9043 0         0 my $putError = $toStore->{store}->put($hash, $object, $keyPair);
9044 0 0       0 $toStore->{storeError} = $putError if $putError;
9045             }
9046              
9047 0         0 return 1;
9048             }
9049              
9050             sub report {
9051 0     0   0 my $o = shift;
9052 0         0 my $hashHex = shift;
9053 0         0 my $toStores = shift;
9054 0         0 my $depth = shift;
9055              
9056 0         0 my @text;
9057 0         0 for my $toStore (@$toStores) {
9058 0 0       0 if ($toStore->{storeError}) {
    0          
9059 0         0 push @text, $o->{ui}->red(' ⨯');
9060             } elsif ($toStore->{needed}->[$depth]) {
9061 0         0 push @text, $o->{ui}->green(' +');
9062             } else {
9063 0         0 push @text, $o->{ui}->green(' ‒');
9064             }
9065             }
9066              
9067 0         0 push @text, ' ', ' ' x ($depth - 1), $hashHex;
9068 0         0 push @text, ' ', @_;
9069 0         0 $o->{ui}->line(@text);
9070             }
9071              
9072             # BEGIN AUTOGENERATED
9073             package CDS::Commands::UseCache;
9074              
9075             sub register {
9076 0     0   0 my $class = shift;
9077 0         0 my $cds = shift;
9078 0         0 my $help = shift;
9079              
9080 0         0 my $node000 = CDS::Parser::Node->new(0);
9081 0         0 my $node001 = CDS::Parser::Node->new(0);
9082 0         0 my $node002 = CDS::Parser::Node->new(0);
9083 0         0 my $node003 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
9084 0         0 my $node004 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&useCache});
9085 0         0 my $node005 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&dropCache});
9086 0         0 my $node006 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&cache});
9087 0         0 $cds->addArrow($node000, 1, 0, 'use');
9088 0         0 $cds->addArrow($node002, 1, 0, 'drop');
9089 0         0 $cds->addArrow($node006, 1, 0, 'cache');
9090 0         0 $help->addArrow($node003, 1, 0, 'cache');
9091 0         0 $node000->addArrow($node001, 1, 0, 'cache');
9092 0         0 $node001->addArrow($node004, 1, 0, 'STORE', \&collectStore);
9093 0         0 $node002->addArrow($node005, 1, 0, 'cache');
9094             }
9095              
9096             sub collectStore {
9097 0     0   0 my $o = shift;
9098 0         0 my $label = shift;
9099 0         0 my $value = shift;
9100              
9101 0         0 $o->{store} = $value;
9102             }
9103              
9104             sub new {
9105 0     0   0 my $class = shift;
9106 0         0 my $actor = shift;
9107 0         0 bless {actor => $actor, ui => $actor->ui} }
9108              
9109             # END AUTOGENERATED
9110              
9111             # HTML FOLDER NAME use-cache
9112             # HTML TITLE Using a cache store
9113             sub help {
9114 0     0   0 my $o = shift;
9115 0         0 my $cmd = shift;
9116              
9117 0         0 my $ui = $o->{ui};
9118 0         0 $ui->space;
9119 0         0 $ui->command('cds use cache STORE');
9120 0         0 $ui->p('Uses STORE to cache objects, and speed up subsequent requests of the same object. This is particularly useful when working with (slow) remote stores. The cache store should be a fast store, such as a local folder store or an in-memory store.');
9121 0         0 $ui->p('Cached objects are not linked to any account, and may disappear with the next garbage collection. Most stores however keep objects for a least a few hours after their last use.');
9122 0         0 $ui->space;
9123 0         0 $ui->command('cds drop cache');
9124 0         0 $ui->p('Stops using the cache.');
9125 0         0 $ui->space;
9126 0         0 $ui->command('cds cache');
9127 0         0 $ui->p('Shows which cache store is used (if any).');
9128 0         0 $ui->space;
9129             }
9130              
9131             sub useCache {
9132 0     0   0 my $o = shift;
9133 0         0 my $cmd = shift;
9134              
9135 0         0 $cmd->collect($o);
9136              
9137 0         0 $o->{actor}->sessionRoot->child('use cache')->setText($o->{store}->url);
9138 0   0     0 $o->{actor}->saveOrShowError // return;
9139 0         0 $o->{ui}->pGreen('Using store "', $o->{store}->url, '" to cache objects.');
9140             }
9141              
9142             sub dropCache {
9143 0     0   0 my $o = shift;
9144 0         0 my $cmd = shift;
9145              
9146 0         0 $o->{actor}->sessionRoot->child('use cache')->clear;
9147 0   0     0 $o->{actor}->saveOrShowError // return;
9148 0         0 $o->{ui}->pGreen('Not using any cache any more.');
9149             }
9150              
9151             sub cache {
9152 0     0   0 my $o = shift;
9153 0         0 my $cmd = shift;
9154              
9155 0         0 my $storeUrl = $o->{actor}->sessionRoot->child('use cache')->textValue;
9156 0 0       0 return $o->{ui}->line('Not using any cache.') if ! length $storeUrl;
9157 0         0 return $o->{ui}->line('Using store "', $storeUrl, '" to cache objects.');
9158             }
9159              
9160             # BEGIN AUTOGENERATED
9161             package CDS::Commands::UseStore;
9162              
9163             sub register {
9164 0     0   0 my $class = shift;
9165 0         0 my $cds = shift;
9166 0         0 my $help = shift;
9167              
9168 0         0 my $node000 = CDS::Parser::Node->new(0);
9169 0         0 my $node001 = CDS::Parser::Node->new(0);
9170 0         0 my $node002 = CDS::Parser::Node->new(0);
9171 0         0 my $node003 = CDS::Parser::Node->new(0);
9172 0         0 my $node004 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
9173 0         0 my $node005 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&useStoreForMessaging});
9174 0         0 $cds->addArrow($node001, 1, 0, 'use');
9175 0         0 $help->addArrow($node000, 1, 0, 'messaging');
9176 0         0 $node000->addArrow($node004, 1, 0, 'store');
9177 0         0 $node001->addArrow($node002, 1, 0, 'STORE', \&collectStore);
9178 0         0 $node002->addArrow($node003, 1, 0, 'for');
9179 0         0 $node003->addArrow($node005, 1, 0, 'messaging');
9180             }
9181              
9182             sub collectStore {
9183 0     0   0 my $o = shift;
9184 0         0 my $label = shift;
9185 0         0 my $value = shift;
9186              
9187 0         0 $o->{store} = $value;
9188             }
9189              
9190             sub new {
9191 0     0   0 my $class = shift;
9192 0         0 my $actor = shift;
9193 0         0 bless {actor => $actor, ui => $actor->ui} }
9194              
9195             # END AUTOGENERATED
9196              
9197             # HTML FOLDER NAME use-store
9198             # HTML TITLE Set the messaging store
9199             sub help {
9200 0     0   0 my $o = shift;
9201 0         0 my $cmd = shift;
9202              
9203 0         0 my $ui = $o->{ui};
9204 0         0 $ui->space;
9205 0         0 $ui->command('cds use STORE for messaging');
9206 0         0 $ui->p('Uses STORE to send and receive messages.');
9207 0         0 $ui->space;
9208             }
9209              
9210             sub useStoreForMessaging {
9211 0     0   0 my $o = shift;
9212 0         0 my $cmd = shift;
9213              
9214 0         0 $cmd->collect($o);
9215              
9216 0         0 $o->{actor}->{configuration}->setMessagingStoreUrl($o->{store}->url);
9217 0         0 $o->{ui}->pGreen('The messaging store is now ', $o->{store}->url);
9218             }
9219              
9220             # BEGIN AUTOGENERATED
9221             package CDS::Commands::Welcome;
9222              
9223             sub register {
9224 0     0   0 my $class = shift;
9225 0         0 my $cds = shift;
9226 0         0 my $help = shift;
9227              
9228 0         0 my $node000 = CDS::Parser::Node->new(0);
9229 0         0 my $node001 = CDS::Parser::Node->new(0);
9230 0         0 my $node002 = CDS::Parser::Node->new(0);
9231 0         0 my $node003 = CDS::Parser::Node->new(0);
9232 0         0 my $node004 = CDS::Parser::Node->new(0);
9233 0         0 my $node005 = CDS::Parser::Node->new(0);
9234 0         0 my $node006 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
9235 0         0 my $node007 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&suppress});
9236 0         0 my $node008 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&enable});
9237 0         0 my $node009 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&show});
9238 0         0 $cds->addArrow($node000, 1, 0, 'suppress');
9239 0         0 $cds->addArrow($node002, 1, 0, 'enable');
9240 0         0 $cds->addArrow($node004, 1, 0, 'show');
9241 0         0 $help->addArrow($node006, 1, 0, 'welcome');
9242 0         0 $node000->addArrow($node001, 1, 0, 'welcome');
9243 0         0 $node001->addArrow($node007, 1, 0, 'message');
9244 0         0 $node002->addArrow($node003, 1, 0, 'welcome');
9245 0         0 $node003->addArrow($node008, 1, 0, 'message');
9246 0         0 $node004->addArrow($node005, 1, 0, 'welcome');
9247 0         0 $node005->addArrow($node009, 1, 0, 'message');
9248             }
9249              
9250             sub new {
9251 0     0   0 my $class = shift;
9252 0         0 my $actor = shift;
9253 0         0 bless {actor => $actor, ui => $actor->ui} }
9254              
9255             # END AUTOGENERATED
9256              
9257             # HTML FOLDER NAME welcome
9258             # HTML TITLE Welcome message
9259             sub help {
9260 0     0   0 my $o = shift;
9261 0         0 my $cmd = shift;
9262              
9263 0         0 my $ui = $o->{ui};
9264 0         0 $ui->space;
9265 0         0 $ui->command('cds suppress welcome message');
9266 0         0 $ui->p('Suppresses the welcome message when typing "cds".');
9267 0         0 $ui->space;
9268 0         0 $ui->command('cds enable welcome message');
9269 0         0 $ui->p('Enables the welcome message when typing "cds".');
9270 0         0 $ui->space;
9271 0         0 $ui->command('cds show welcome message');
9272 0         0 $ui->p('Shows the welcome message.');
9273 0         0 $ui->space;
9274             }
9275              
9276             sub suppress {
9277 0     0   0 my $o = shift;
9278 0         0 my $cmd = shift;
9279              
9280 0         0 $o->{actor}->localRoot->child('suppress welcome message')->setBoolean(1);
9281 0   0     0 $o->{actor}->saveOrShowError // return;
9282              
9283 0         0 $o->{ui}->space;
9284 0         0 $o->{ui}->p('The welcome message will not be shown any more.');
9285 0         0 $o->{ui}->space;
9286 0         0 $o->{ui}->line('You can manually display the message by typing:');
9287 0         0 $o->{ui}->line($o->{ui}->blue(' cds show welcome message'));
9288 0         0 $o->{ui}->line('or re-enable it using:');
9289 0         0 $o->{ui}->line($o->{ui}->blue(' cds enable welcome message'));
9290 0         0 $o->{ui}->space;
9291             }
9292              
9293             sub enable {
9294 0     0   0 my $o = shift;
9295 0         0 my $cmd = shift;
9296              
9297 0         0 $o->{actor}->localRoot->child('suppress welcome message')->clear;
9298 0   0     0 $o->{actor}->saveOrShowError // return;
9299              
9300 0         0 $o->{ui}->space;
9301 0         0 $o->{ui}->p('The welcome message will be shown when you type "cds".');
9302 0         0 $o->{ui}->space;
9303             }
9304              
9305             sub isEnabled {
9306 0     0   0 my $o = shift;
9307 0         0 ! $o->{actor}->localRoot->child('suppress welcome message')->isSet }
9308              
9309             sub show {
9310 0     0   0 my $o = shift;
9311 0         0 my $cmd = shift;
9312              
9313 0         0 my $ui = $o->{ui};
9314 0         0 $ui->space;
9315 0         0 $ui->title('Hi there!');
9316 0         0 $ui->p('This is the command line interface (CLI) of Condensation ', $CDS::VERSION, ', ', $CDS::releaseDate, '. Condensation is a distributed data system with conflict-free forward merging and end-to-end security. More information is available on https://condensation.io.');
9317 0         0 $ui->space;
9318 0         0 $ui->p('Commands resemble short english sentences. For example, the following "sentence" will show the record of an object:');
9319 0         0 $ui->line($ui->blue(' cds show record e5cbfc282e1f3e6fd0f3e5fffd41964c645f44d7fae8ef5cb350c2dfd2196c9f \\'));
9320 0         0 $ui->line($ui->blue(' from http://examples.condensation.io'));
9321 0         0 $ui->p('Type a "?" to explore possible commands, e.g.');
9322 0         0 $ui->line($ui->blue(' cds show ?'));
9323 0         0 $ui->p('or use TAB or TAB-TAB for command completion.');
9324 0         0 $ui->space;
9325 0         0 $ui->p('To get help, type');
9326 0         0 $ui->line($ui->blue(' cds help'));
9327 0         0 $ui->space;
9328 0         0 $ui->p('To suppress this welcome message, type');
9329 0         0 $ui->line($ui->blue(' cds suppress welcome message'));
9330 0         0 $ui->space;
9331             }
9332              
9333             package CDS::Commands::WhatIs;
9334              
9335             # BEGIN AUTOGENERATED
9336              
9337             sub register {
9338 0     0   0 my $class = shift;
9339 0         0 my $cds = shift;
9340 0         0 my $help = shift;
9341              
9342 0         0 my $node000 = CDS::Parser::Node->new(0);
9343 0         0 my $node001 = CDS::Parser::Node->new(0);
9344 0         0 my $node002 = CDS::Parser::Node->new(0);
9345 0         0 my $node003 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&help});
9346 0         0 my $node004 = CDS::Parser::Node->new(1, {constructor => \&new, function => \&whatIs});
9347 0         0 $cds->addArrow($node001, 1, 0, 'what');
9348 0         0 $help->addArrow($node000, 1, 0, 'what');
9349 0         0 $node000->addArrow($node003, 1, 0, 'is');
9350 0         0 $node001->addArrow($node002, 1, 0, 'is');
9351 0         0 $node002->addArrow($node004, 1, 0, 'TEXT', \&collectText);
9352             }
9353              
9354             sub collectText {
9355 0     0   0 my $o = shift;
9356 0         0 my $label = shift;
9357 0         0 my $value = shift;
9358              
9359 0         0 $o->{text} = $value;
9360             }
9361              
9362             sub new {
9363 0     0   0 my $class = shift;
9364 0         0 my $actor = shift;
9365 0         0 bless {actor => $actor, ui => $actor->ui} }
9366              
9367             # END AUTOGENERATED
9368              
9369             # HTML FOLDER NAME what-is
9370             # HTML TITLE What is
9371             sub help {
9372 0     0   0 my $o = shift;
9373 0         0 my $cmd = shift;
9374              
9375 0         0 my $ui = $o->{ui};
9376 0         0 $ui->space;
9377 0         0 $ui->command('cds what is TEXT');
9378 0         0 $ui->p('Tells what TEXT could be under the current configuration.');
9379 0         0 $ui->space;
9380             }
9381              
9382             sub whatIs {
9383 0     0   0 my $o = shift;
9384 0         0 my $cmd = shift;
9385              
9386 0         0 $cmd->collect($o);
9387 0         0 $o->{butNot} = [];
9388              
9389 0         0 $o->{ui}->space;
9390 0         0 $o->{ui}->title($o->{ui}->blue($o->{text}), ' may be …');
9391              
9392 0     0   0 $o->test('ACCOUNT', 'an ACCOUNT', sub { shift->url });
  0         0  
9393 0     0   0 $o->test('AESKEY', 'an AESKEY', sub { unpack('H*', shift) });
  0         0  
9394 0     0   0 $o->test('BOX', 'a BOX', sub { shift->url });
  0         0  
9395 0     0   0 $o->test('BOXLABEL', 'a BOXLABEL', sub { shift });
  0         0  
9396 0         0 $o->test('FILE', 'a FILE', \&fileResult);
9397 0         0 $o->test('FILENAME', 'a FILENAME', \&fileResult);
9398 0         0 $o->test('FOLDER', 'a FOLDER', \&fileResult);
9399 0     0   0 $o->test('GROUP', 'a GROUP on this system', sub { shift });
  0         0  
9400 0     0   0 $o->test('HASH', 'a HASH or ACTOR hash', sub { shift->hex });
  0         0  
9401 0         0 $o->test('KEYPAIR', 'a KEYPAIR', \&keyPairResult);
9402 0     0   0 $o->test('LABEL', 'a remembered LABEL', sub { shift });
  0         0  
9403 0     0   0 $o->test('OBJECT', 'an OBJECT', sub { shift->url });
  0         0  
9404 0         0 $o->test('OBJECTFILE', 'an OBJECTFILE', \&objectFileResult);
9405 0     0   0 $o->test('STORE', 'a STORE', sub { shift->url });
  0         0  
9406 0     0   0 $o->test('USER', 'a USER on this system', sub { shift });
  0         0  
9407              
9408 0         0 for my $butNot (@{$o->{butNot}}) {
  0         0  
9409 0         0 $o->{ui}->space;
9410 0         0 $o->{ui}->line('… but not ', $butNot->{text}, ', because:');
9411 0         0 for my $warning (@{$butNot->{warnings}}) {
  0         0  
9412 0         0 $o->{ui}->warning($warning);
9413             }
9414             }
9415              
9416 0         0 $o->{ui}->space;
9417             }
9418              
9419             sub test {
9420 0     0   0 my $o = shift;
9421 0         0 my $expect = shift;
9422 0         0 my $text = shift;
9423 0         0 my $resultHandler = shift;
9424              
9425 0         0 my $token = CDS::Parser::Token->new($o->{actor}, $o->{text});
9426 0         0 my $result = $token->produce($expect);
9427 0 0       0 if (defined $result) {
    0          
9428 0         0 my $whichOne = &$resultHandler($result);
9429 0         0 $o->{ui}->line('… ', $text, ' ', $o->{ui}->gray($whichOne));
9430 0         0 } elsif (scalar @{$token->{warnings}}) {
9431 0         0 push @{$o->{butNot}}, {text => $text, warnings => $token->{warnings}};
  0         0  
9432             }
9433             }
9434              
9435             sub keyPairResult {
9436 0     0   0 my $keyPairToken = shift;
9437              
9438 0         0 return $keyPairToken->file.' ('.$keyPairToken->keyPair->publicKey->hash->hex.')';
9439             }
9440              
9441             sub objectFileResult {
9442 0     0   0 my $objectFileToken = shift;
9443              
9444 0 0       0 return $objectFileToken->file if $objectFileToken->object->byteLength > 1024 * 1024;
9445 0         0 return $objectFileToken->file.' ('.$objectFileToken->object->calculateHash->hex.')';
9446             }
9447              
9448             sub fileResult {
9449 0     0   0 my $file = shift;
9450              
9451 0         0 my @s = stat $file;
9452 0 0       0 my $label =
    0          
    0          
    0          
    0          
    0          
    0          
    0          
9453             ! scalar @s ? ' (non-existing)' :
9454             Fcntl::S_ISDIR($s[2]) ? ' (folder)' :
9455             Fcntl::S_ISREG($s[2]) ? ' (file, '.$s[7].' bytes)' :
9456             Fcntl::S_ISLNK($s[2]) ? ' (symbolic link)' :
9457             Fcntl::S_ISBLK($s[2]) ? ' (block device)' :
9458             Fcntl::S_ISCHR($s[2]) ? ' (char device)' :
9459             Fcntl::S_ISSOCK($s[2]) ? ' (socket)' :
9460             Fcntl::S_ISFIFO($s[2]) ? ' (pipe)' : ' (unknown type)';
9461              
9462 0         0 return $file.$label;
9463             }
9464              
9465             package CDS::Configuration;
9466              
9467             our $xdgConfigurationFolder = ($ENV{XDG_CONFIG_HOME} || $ENV{HOME}.'/.config').'/condensation';
9468             our $xdgDataFolder = ($ENV{XDG_DATA_HOME} || $ENV{HOME}.'/.local/share').'/condensation';
9469              
9470             sub getOrCreateDefault {
9471 0     0   0 my $class = shift;
9472 0         0 my $ui = shift;
9473              
9474 0         0 my $configuration = $class->new($ui, $xdgConfigurationFolder, $xdgDataFolder);
9475 0         0 $configuration->createIfNecessary();
9476 0         0 return $configuration;
9477             }
9478              
9479             sub new {
9480 0     0   0 my $class = shift;
9481 0         0 my $ui = shift;
9482 0         0 my $folder = shift;
9483 0         0 my $defaultStoreFolder = shift;
9484              
9485 0         0 return bless {ui => $ui, folder => $folder, defaultStoreFolder => $defaultStoreFolder};
9486             }
9487              
9488 0     0   0 sub ui { shift->{ui} }
9489 0     0   0 sub folder { shift->{folder} }
9490              
9491             sub createIfNecessary {
9492 0     0   0 my $o = shift;
9493              
9494 0         0 my $keyPairFile = $o->{folder}.'/key-pair';
9495 0 0       0 return 1 if -f $keyPairFile;
9496              
9497 0         0 $o->{ui}->progress('Creating configuration folders …');
9498 0   0     0 $o->createFolder($o->{folder}) // return $o->{ui}->error('Failed to create the folder "', $o->{folder}, '".');
9499 0   0     0 $o->createFolder($o->{defaultStoreFolder}) // return $o->{ui}->error('Failed to create the folder "', $o->{defaultStoreFolder}, '".');
9500 0         0 CDS::FolderStore->new($o->{defaultStoreFolder})->createIfNecessary;
9501              
9502 0         0 $o->{ui}->progress('Generating key pair …');
9503 0         0 my $keyPair = CDS::KeyPair->generate;
9504 0   0     0 $keyPair->writeToFile($keyPairFile) // return $o->{ui}->error('Failed to write the configuration file "', $keyPairFile, '". Make sure that this location is writable.');
9505 0         0 $o->{ui}->removeProgress;
9506 0         0 return 1;
9507             }
9508              
9509             sub createFolder {
9510 0     0   0 my $o = shift;
9511 0         0 my $folder = shift;
9512              
9513 0         0 for my $path (CDS->intermediateFolders($folder)) {
9514 0         0 mkdir $path;
9515             }
9516              
9517 0         0 return -d $folder;
9518             }
9519              
9520             sub file {
9521 0     0   0 my $o = shift;
9522 0         0 my $filename = shift;
9523              
9524 0         0 return $o->{folder}.'/'.$filename;
9525             }
9526              
9527             sub messagingStoreUrl {
9528 0     0   0 my $o = shift;
9529              
9530 0   0     0 return $o->readFirstLine('messaging-store') // 'file://'.$o->{defaultStoreFolder};
9531             }
9532              
9533             sub storageStoreUrl {
9534 0     0   0 my $o = shift;
9535              
9536 0   0     0 return $o->readFirstLine('store') // 'file://'.$o->{defaultStoreFolder};
9537             }
9538              
9539             sub setMessagingStoreUrl {
9540 0     0   0 my $o = shift;
9541 0         0 my $storeUrl = shift;
9542              
9543 0         0 CDS->writeTextToFile($o->file('messaging-store'), $storeUrl);
9544             }
9545              
9546             sub setStorageStoreUrl {
9547 0     0   0 my $o = shift;
9548 0         0 my $storeUrl = shift;
9549              
9550 0         0 CDS->writeTextToFile($o->file('store'), $storeUrl);
9551             }
9552              
9553             sub keyPair {
9554 0     0   0 my $o = shift;
9555              
9556 0         0 return CDS::KeyPair->fromFile($o->file('key-pair'));
9557             }
9558              
9559             sub setKeyPair {
9560 0     0   0 my $o = shift;
9561 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
9562              
9563 0         0 $keyPair->writeToFile($o->file('key-pair'));
9564             }
9565              
9566             sub readFirstLine {
9567 0     0   0 my $o = shift;
9568 0         0 my $file = shift;
9569              
9570 0   0     0 my $content = CDS->readTextFromFile($o->file($file)) // return;
9571 0 0       0 $content = $1 if $content =~ /^(.*)\n/;
9572 0 0       0 $content = $1 if $content =~ /^\s*(.*?)\s*$/;
9573 0         0 return $content;
9574             }
9575              
9576             package CDS::DetachedDocument;
9577              
9578 1     1   29680 use parent -norequire, 'CDS::Document';
  1         3  
  1         5  
9579              
9580             sub new {
9581 0     0   0 my $class = shift;
9582 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
9583              
9584 0         0 return $class->SUPER::new($keyPair, CDS::InMemoryStore->create);
9585             }
9586              
9587             sub savingDone {
9588 0     0   0 my $o = shift;
9589 0         0 my $revision = shift;
9590 0         0 my $newPart = shift;
9591 0         0 my $obsoleteParts = shift;
9592              
9593             # We don't do anything
9594 0         0 $o->{unsaved}->savingDone;
9595             }
9596              
9597             package CDS::DiscoverActorGroup;
9598              
9599             sub discover {
9600 0     0   0 my $class = shift;
9601 0 0 0     0 my $builder = shift; die 'wrong type '.ref($builder).' for $builder' if defined $builder && ref $builder ne 'CDS::ActorGroupBuilder';
  0         0  
9602 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
9603 0         0 my $delegate = shift;
9604              
9605 0         0 my $o = bless {
9606             knownPublicKeys => $builder->knownPublicKeys, # A hashref of known public keys (e.g. from the existing actor group)
9607             keyPair => $keyPair,
9608             delegate => $delegate, # The delegate
9609             nodesByUrl => {}, # Nodes on which this actor group is active, by URL
9610             coverage => {}, # Hashes that belong to this actor group
9611             };
9612              
9613             # Add all active members
9614 0         0 for my $member ($builder->members) {
9615 0 0       0 next if $member->status ne 'active';
9616 0         0 my $node = $o->node($member->hash, $member->storeUrl);
9617 0 0       0 if ($node->{revision} < $member->revision) {
9618 0         0 $node->{revision} = $member->revision;
9619 0         0 $node->{status} = 'active';
9620             }
9621              
9622 0         0 $o->{coverage}->{$member->hash->bytes} = 1;
9623             }
9624              
9625             # Determine the revision at start
9626 0         0 my $revisionAtStart = 0;
9627 0         0 for my $node (values %{$o->{nodesByUrl}}) {
  0         0  
9628 0 0       0 $revisionAtStart = $node->{revision} if $revisionAtStart < $node->{revision};
9629             }
9630              
9631             # Reload the cards of all known accounts
9632 0         0 for my $node (values %{$o->{nodesByUrl}}) {
  0         0  
9633 0         0 $node->discover;
9634             }
9635              
9636             # From here, try extending to other accounts
9637 0         0 while ($o->extend) {}
9638              
9639             # Compile the list of actors and cards
9640 0         0 my @members;
9641             my @cards;
9642 0         0 for my $node (values %{$o->{nodesByUrl}}) {
  0         0  
9643 0 0       0 next if ! $node->{reachable};
9644 0 0       0 next if ! $node->{attachedToUs};
9645 0 0       0 next if ! $node->{actorOnStore};
9646 0 0       0 next if ! $node->isActiveOrIdle;
9647             #-- member ++ $node->{actorHash}->hex ++ $node->{cardsRead} ++ $node->{cards} // 'undef' ++ $node->{actorOnStore} // 'undef'
9648 0         0 push @members, CDS::ActorGroup::Member->new($node->{actorOnStore}, $node->{storeUrl}, $node->{revision}, $node->isActive);
9649 0         0 push @cards, @{$node->{cards}};
  0         0  
9650             }
9651              
9652             # Get the newest list of entrusted actors
9653 0         0 my $parser = CDS::ActorGroupBuilder->new;
9654 0         0 for my $card (@cards) {
9655 0         0 $parser->parseEntrustedActors($card->card->child('entrusted actors'), 0);
9656             }
9657              
9658             # Get the entrusted actors
9659 0         0 my $entrustedActors = [];
9660 0         0 for my $actor ($parser->entrustedActors) {
9661 0         0 my $store = $o->{delegate}->onDiscoverActorGroupVerifyStore($actor->storeUrl);
9662 0 0       0 next if ! $store;
9663              
9664 0         0 my $knownPublicKey = $o->{knownPublicKeys}->{$actor->hash->bytes};
9665 0 0       0 if ($knownPublicKey) {
9666 0         0 push @$entrustedActors, CDS::ActorGroup::EntrustedActor->new(CDS::ActorOnStore->new($knownPublicKey, $store), $actor->storeUrl);
9667 0         0 next;
9668             }
9669              
9670 0         0 my ($publicKey, $invalidReason, $storeError) = $keyPair->getPublicKey($actor->hash, $store);
9671              
9672 0 0       0 if (defined $invalidReason) {
9673 0         0 $o->{discoverer}->{delegate}->onDiscoverActorGroupInvalidPublicKey($actor->hash, $store, $invalidReason);
9674 0         0 next;
9675             }
9676              
9677 0 0       0 if (defined $storeError) {
9678 0         0 $o->{discoverer}->{delegate}->onDiscoverActorGroupStoreError($store, $storeError);
9679 0         0 next;
9680             }
9681              
9682 0         0 push @$entrustedActors, CDS::ActorGroup::EntrustedActor->new(CDS::ActorOnStore->new($publicKey, $store), $actor->storeUrl);
9683             }
9684              
9685 0 0       0 my $members = [sort { $b->{revision} <=> $a->{revision} || $b->{status} cmp $a->{status} } @members];
  0         0  
9686 0         0 return CDS::ActorGroup->new($members, $parser->entrustedActorsRevision, $entrustedActors), [@cards], [grep { $_->{attachedToUs} } values %{$o->{nodesByUrl}}];
  0         0  
  0         0  
9687             }
9688              
9689             sub node {
9690 0     0   0 my $o = shift;
9691 0 0 0     0 my $actorHash = shift; die 'wrong type '.ref($actorHash).' for $actorHash' if defined $actorHash && ref $actorHash ne 'CDS::Hash';
  0         0  
9692 0         0 my $storeUrl = shift;
9693             # private
9694 0         0 my $url = $storeUrl.'/accounts/'.$actorHash->hex;
9695 0         0 my $node = $o->{nodesByUrl}->{$url};
9696 0 0       0 return $node if $node;
9697 0         0 return $o->{nodesByUrl}->{$url} = CDS::DiscoverActorGroup::Node->new($o, $actorHash, $storeUrl);
9698             }
9699              
9700             sub covers {
9701 0     0   0 my $o = shift;
9702 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
9703 0         0 $o->{coverage}->{$hash->bytes} }
9704              
9705             sub extend {
9706 0     0   0 my $o = shift;
9707              
9708             # Start with the newest node
9709 0         0 my $mainNode;
9710 0         0 my $mainRevision = -1;
9711 0         0 for my $node (values %{$o->{nodesByUrl}}) {
  0         0  
9712 0 0       0 next if ! $node->{attachedToUs};
9713 0 0       0 next if $node->{revision} <= $mainRevision;
9714 0         0 $mainNode = $node;
9715 0         0 $mainRevision = $node->{revision};
9716             }
9717              
9718 0 0       0 return 0 if ! $mainNode;
9719              
9720             # Reset the reachable flag
9721 0         0 for my $node (values %{$o->{nodesByUrl}}) {
  0         0  
9722 0         0 $node->{reachable} = 0;
9723             }
9724 0         0 $mainNode->{reachable} = 1;
9725              
9726             # Traverse the graph along active links to find accounts to discover.
9727 0         0 my @toDiscover;
9728 0         0 my @toCheck = ($mainNode);
9729 0         0 while (1) {
9730 0   0     0 my $currentNode = shift(@toCheck) // last;
9731 0         0 for my $link (@{$currentNode->{links}}) {
  0         0  
9732 0         0 my $node = $link->{node};
9733 0 0       0 next if $node->{reachable};
9734 0 0       0 my $prospectiveStatus = $link->{revision} > $node->{revision} ? $link->{status} : $node->{status};
9735 0 0       0 next if $prospectiveStatus ne 'active';
9736 0         0 $node->{reachable} = 1;
9737 0 0       0 push @toCheck, $node if $node->{attachedToUs};
9738 0 0       0 push @toDiscover, $node if ! $node->{attachedToUs};
9739             }
9740             }
9741              
9742             # Discover these accounts
9743 0         0 my $hasChanges = 0;
9744 0         0 for my $node (sort { $b->{revision} <=> $a->{revision} } @toDiscover) {
  0         0  
9745 0         0 $node->discover;
9746 0 0       0 next if ! $node->{attachedToUs};
9747 0         0 $hasChanges = 1;
9748             }
9749              
9750 0         0 return $hasChanges;
9751             }
9752              
9753             package CDS::DiscoverActorGroup::Card;
9754              
9755             sub new {
9756 0     0   0 my $class = shift;
9757 0         0 my $storeUrl = shift;
9758 0 0 0     0 my $actorOnStore = shift; die 'wrong type '.ref($actorOnStore).' for $actorOnStore' if defined $actorOnStore && ref $actorOnStore ne 'CDS::ActorOnStore';
  0         0  
9759 0 0 0     0 my $envelopeHash = shift; die 'wrong type '.ref($envelopeHash).' for $envelopeHash' if defined $envelopeHash && ref $envelopeHash ne 'CDS::Hash';
  0         0  
9760 0 0 0     0 my $envelope = shift; die 'wrong type '.ref($envelope).' for $envelope' if defined $envelope && ref $envelope ne 'CDS::Record';
  0         0  
9761 0 0 0     0 my $cardHash = shift; die 'wrong type '.ref($cardHash).' for $cardHash' if defined $cardHash && ref $cardHash ne 'CDS::Hash';
  0         0  
9762 0         0 my $card = shift;
9763              
9764 0         0 return bless {
9765             storeUrl => $storeUrl,
9766             actorOnStore => $actorOnStore,
9767             envelopeHash => $envelopeHash,
9768             envelope => $envelope,
9769             cardHash => $cardHash,
9770             card => $card,
9771             };
9772             }
9773              
9774 0     0   0 sub storeUrl { shift->{storeUrl} }
9775 0     0   0 sub actorOnStore { shift->{actorOnStore} }
9776 0     0   0 sub envelopeHash { shift->{envelopeHash} }
9777 0     0   0 sub envelope { shift->{envelope} }
9778 0     0   0 sub cardHash { shift->{cardHash} }
9779 0     0   0 sub card { shift->{card} }
9780              
9781             package CDS::DiscoverActorGroup::Link;
9782              
9783             sub new {
9784 0     0   0 my $class = shift;
9785 0         0 my $node = shift;
9786 0         0 my $revision = shift;
9787 0         0 my $status = shift;
9788              
9789 0         0 bless {
9790             node => $node,
9791             revision => $revision,
9792             status => $status,
9793             };
9794             }
9795              
9796 0     0   0 sub node { shift->{node} }
9797 0     0   0 sub revision { shift->{revision} }
9798 0     0   0 sub status { shift->{status} }
9799              
9800             package CDS::DiscoverActorGroup::Node;
9801              
9802             sub new {
9803 0     0   0 my $class = shift;
9804 0         0 my $discoverer = shift;
9805 0 0 0     0 my $actorHash = shift; die 'wrong type '.ref($actorHash).' for $actorHash' if defined $actorHash && ref $actorHash ne 'CDS::Hash';
  0         0  
9806 0         0 my $storeUrl = shift;
9807              
9808 0         0 return bless {
9809             discoverer => $discoverer,
9810             actorHash => $actorHash,
9811             storeUrl => $storeUrl,
9812             revision => -1,
9813             status => 'idle',
9814             reachable => 0, # whether this node is reachable from the main node
9815             store => undef,
9816             actorOnStore => undef,
9817             links => [], # all links found in the cards
9818             attachedToUs => 0, # whether the account belongs to us
9819             cardsRead => 0,
9820             cards => [],
9821             };
9822             }
9823              
9824             sub cards {
9825 0     0   0 my $o = shift;
9826 0         0 @{$o->{cards}} }
  0         0  
9827             sub isActive {
9828 0     0   0 my $o = shift;
9829 0         0 $o->{status} eq 'active' }
9830             sub isActiveOrIdle {
9831 0     0   0 my $o = shift;
9832 0 0       0 $o->{status} eq 'active' || $o->{status} eq 'idle' }
9833              
9834 0     0   0 sub actorHash { shift->{actorHash} }
9835 0     0   0 sub storeUrl { shift->{storeUrl} }
9836 0     0   0 sub revision { shift->{revision} }
9837 0     0   0 sub status { shift->{status} }
9838 0     0   0 sub attachedToUs { shift->{attachedToUs} }
9839             sub links {
9840 0     0   0 my $o = shift;
9841 0         0 @{$o->{links}} }
  0         0  
9842              
9843             sub discover {
9844 0     0   0 my $o = shift;
9845              
9846             #-- discover ++ $o->{actorHash}->hex
9847 0         0 $o->readCards;
9848 0         0 $o->attach;
9849             }
9850              
9851             sub readCards {
9852 0     0   0 my $o = shift;
9853              
9854 0 0       0 return if $o->{cardsRead};
9855 0         0 $o->{cardsRead} = 1;
9856             #-- read cards of ++ $o->{actorHash}->hex
9857              
9858             # Get the store
9859 0   0     0 my $store = $o->{discoverer}->{delegate}->onDiscoverActorGroupVerifyStore($o->{storeUrl}, $o->{actorHash}) // return;
9860              
9861             # Get the public key if necessary
9862 0 0       0 if (! $o->{actorOnStore}) {
9863 0         0 my $publicKey = $o->{discoverer}->{knownPublicKeys}->{$o->{actorHash}->bytes};
9864 0 0       0 if (! $publicKey) {
9865 0         0 my ($downloadedPublicKey, $invalidReason, $storeError) = $o->{discoverer}->{keyPair}->getPublicKey($o->{actorHash}, $store);
9866 0 0       0 return $o->{discoverer}->{delegate}->onDiscoverActorGroupStoreError($store, $storeError) if defined $storeError;
9867 0 0       0 return $o->{discoverer}->{delegate}->onDiscoverActorGroupInvalidPublicKey($o->{actorHash}, $store, $invalidReason) if defined $invalidReason;
9868 0         0 $publicKey = $downloadedPublicKey;
9869             }
9870              
9871 0         0 $o->{actorOnStore} = CDS::ActorOnStore->new($publicKey, $store);
9872             }
9873              
9874             # List the public box
9875 0         0 my ($hashes, $storeError) = $store->list($o->{actorHash}, 'public', 0, $o->{discoverer}->{keyPair});
9876 0 0       0 return $o->{discoverer}->{delegate}->onDiscoverActorGroupStoreError($store, $storeError) if defined $storeError;
9877              
9878 0         0 for my $envelopeHash (@$hashes) {
9879             # Open the envelope
9880 0         0 my ($object, $storeError) = $store->get($envelopeHash, $o->{discoverer}->{keyPair});
9881 0 0       0 return $o->{discoverer}->{delegate}->onDiscoverActorGroupStoreError($store, $storeError) if defined $storeError;
9882 0 0       0 if (! $object) {
9883 0         0 $o->{discoverer}->{delegate}->onDiscoverActorGroupInvalidCard($o->{actorOnStore}, $envelopeHash, 'Envelope object not found.');
9884 0         0 next;
9885             }
9886              
9887 0         0 my $envelope = CDS::Record->fromObject($object);
9888 0 0       0 if (! $envelope) {
9889 0         0 $o->{discoverer}->{delegate}->onDiscoverActorGroupInvalidCard($o->{actorOnStore}, $envelopeHash, 'Envelope is not a record.');
9890 0         0 next;
9891             }
9892              
9893 0         0 my $cardHash = $envelope->child('content')->hashValue;
9894 0 0       0 if (! $cardHash) {
9895 0         0 $o->{discoverer}->{delegate}->onDiscoverActorGroupInvalidCard($o->{actorOnStore}, $envelopeHash, 'Missing content hash.');
9896 0         0 next;
9897             }
9898              
9899 0 0       0 if (! CDS->verifyEnvelopeSignature($envelope, $o->{actorOnStore}->publicKey, $cardHash)) {
9900 0         0 $o->{discoverer}->{delegate}->onDiscoverActorGroupInvalidCard($o->{actorOnStore}, $envelopeHash, 'Invalid signature.');
9901 0         0 next;
9902             }
9903              
9904             # Read the card
9905 0         0 my ($cardObject, $storeError1) = $store->get($cardHash, $o->{discoverer}->{keyPair});
9906 0 0       0 return $o->{discoverer}->{delegate}->onDiscoverActorGroupStoreError($store, $storeError) if defined $storeError1;
9907 0 0       0 if (! $cardObject) {
9908 0         0 $o->{discoverer}->{delegate}->onDiscoverActorGroupInvalidCard($o->{actorOnStore}, $envelopeHash, 'Card object not found.');
9909 0         0 next;
9910             }
9911              
9912 0         0 my $card = CDS::Record->fromObject($cardObject);
9913 0 0       0 if (! $card) {
9914 0         0 $o->{discoverer}->{delegate}->onDiscoverActorGroupInvalidCard($o->{actorOnStore}, $envelopeHash, 'Card is not a record.');
9915 0         0 next;
9916             }
9917              
9918             # Add the card to the list of cards
9919 0         0 push @{$o->{cards}}, CDS::DiscoverActorGroup::Card->new($o->{storeUrl}, $o->{actorOnStore}, $envelopeHash, $envelope, $cardHash, $card);
  0         0  
9920              
9921             # Parse the account list
9922 0         0 my $builder = CDS::ActorGroupBuilder->new;
9923 0         0 $builder->parseMembers($card->child('actor group'), 0);
9924 0         0 for my $member ($builder->members) {
9925 0         0 my $node = $o->{discoverer}->node($member->hash, $member->storeUrl);
9926             #-- new link ++ $o->{actorHash}->hex ++ $status ++ $hash->hex
9927 0         0 push @{$o->{links}}, CDS::DiscoverActorGroup::Link->new($node, $member->revision, $member->status);
  0         0  
9928             }
9929             }
9930             }
9931              
9932             sub attach {
9933 0     0   0 my $o = shift;
9934              
9935 0 0       0 return if $o->{attachedToUs};
9936 0 0       0 return if ! $o->hasLinkToUs;
9937              
9938             # Attach this node
9939 0         0 $o->{attachedToUs} = 1;
9940              
9941             # Merge all links
9942 0         0 for my $link (@{$o->{links}}) {
  0         0  
9943 0         0 $link->{node}->merge($link->{revision}, $link->{status});
9944             }
9945              
9946             # Add the hash to the coverage
9947 0         0 $o->{discoverer}->{coverage}->{$o->{actorHash}->bytes} = 1;
9948             }
9949              
9950             sub merge {
9951 0     0   0 my $o = shift;
9952 0         0 my $revision = shift;
9953 0         0 my $status = shift;
9954              
9955 0 0       0 return if $o->{revision} >= $revision;
9956 0         0 $o->{revision} = $revision;
9957 0         0 $o->{status} = $status;
9958             }
9959              
9960             sub hasLinkToUs {
9961 0     0   0 my $o = shift;
9962              
9963 0 0       0 return 1 if $o->{discoverer}->covers($o->{actorHash});
9964 0         0 for my $link (@{$o->{links}}) {
  0         0  
9965 0 0       0 return 1 if $o->{discoverer}->covers($link->{node}->{actorHash});
9966             }
9967 0         0 return;
9968             }
9969              
9970             package CDS::Document;
9971              
9972             sub new {
9973 0     0   0 my $class = shift;
9974 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
9975 0         0 my $store = shift;
9976              
9977 0         0 my $o = bless {
9978             keyPair => $keyPair,
9979             unsaved => CDS::Unsaved->new($store),
9980             itemsBySelector => {},
9981             parts => {},
9982             hasPartsToMerge => 0,
9983             }, $class;
9984              
9985 0         0 $o->{root} = CDS::Selector->root($o);
9986 0         0 $o->{changes} = CDS::Document::Part->new;
9987 0         0 return $o;
9988             }
9989              
9990 0     0   0 sub keyPair { shift->{keyPair} }
9991 0     0   0 sub unsaved { shift->{unsaved} }
9992             sub parts {
9993 0     0   0 my $o = shift;
9994 0         0 values %{$o->{parts}} }
  0         0  
9995 0     0   0 sub hasPartsToMerge { shift->{hasPartsToMerge} }
9996              
9997             ### Items
9998              
9999 0     0   0 sub root { shift->{root} }
10000             sub rootItem {
10001 0     0   0 my $o = shift;
10002 0         0 $o->getOrCreate($o->{root}) }
10003              
10004             sub get {
10005 0     0   0 my $o = shift;
10006 0 0 0     0 my $selector = shift; die 'wrong type '.ref($selector).' for $selector' if defined $selector && ref $selector ne 'CDS::Selector';
  0         0  
10007 0         0 $o->{itemsBySelector}->{$selector->{id}} }
10008              
10009             sub getOrCreate {
10010 0     0   0 my $o = shift;
10011 0 0 0     0 my $selector = shift; die 'wrong type '.ref($selector).' for $selector' if defined $selector && ref $selector ne 'CDS::Selector';
  0         0  
10012              
10013 0         0 my $item = $o->{itemsBySelector}->{$selector->{id}};
10014 0 0       0 $o->{itemsBySelector}->{$selector->{id}} = $item = CDS::Document::Item->new($selector) if ! $item;
10015 0         0 return $item;
10016             }
10017              
10018             sub prune {
10019 0     0   0 my $o = shift;
10020 0         0 $o->rootItem->pruneTree; }
10021              
10022             ### Merging
10023              
10024             sub merge {
10025 0     0   0 my $o = shift;
10026              
10027 0         0 for my $hashAndKey (@_) {
10028 0 0       0 next if ! $hashAndKey;
10029 0 0       0 next if $o->{parts}->{$hashAndKey->hash->bytes};
10030 0         0 my $part = CDS::Document::Part->new;
10031 0         0 $part->{hashAndKey} = $hashAndKey;
10032 0         0 $o->{parts}->{$hashAndKey->hash->bytes} = $part;
10033 0         0 $o->{hasPartsToMerge} = 1;
10034             }
10035             }
10036              
10037             sub read {
10038 0     0   0 my $o = shift;
10039              
10040 0 0       0 return 1 if ! $o->{hasPartsToMerge};
10041              
10042             # Load the parts
10043 0         0 for my $part (values %{$o->{parts}}) {
  0         0  
10044 0 0       0 next if $part->{isMerged};
10045 0 0       0 next if $part->{loadedRecord};
10046              
10047 0         0 my ($record, $object, $invalidReason, $storeError) = $o->{keyPair}->getAndDecryptRecord($part->{hashAndKey}, $o->{unsaved});
10048 0 0       0 return if defined $storeError;
10049              
10050 0 0       0 delete $o->{parts}->{$part->{hashAndKey}->hash->bytes} if defined $invalidReason;
10051 0         0 $part->{loadedRecord} = $record;
10052             }
10053              
10054             # Merge the loaded parts
10055 0         0 for my $part (values %{$o->{parts}}) {
  0         0  
10056 0 0       0 next if $part->{isMerged};
10057 0 0       0 next if ! $part->{loadedRecord};
10058 0 0       0 my $oldFormat = $part->{loadedRecord}->child('client')->textValue =~ /0.19/ ? 1 : 0;
10059 0         0 $o->mergeNode($part, $o->{root}, $part->{loadedRecord}->child('root'), $oldFormat);
10060 0         0 delete $part->{loadedRecord};
10061 0         0 $part->{isMerged} = 1;
10062             }
10063              
10064 0         0 $o->{hasPartsToMerge} = 0;
10065 0         0 return 1;
10066             }
10067              
10068             sub mergeNode {
10069 0     0   0 my $o = shift;
10070 0         0 my $part = shift;
10071 0 0 0     0 my $selector = shift; die 'wrong type '.ref($selector).' for $selector' if defined $selector && ref $selector ne 'CDS::Selector';
  0         0  
10072 0 0 0     0 my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0         0  
10073 0         0 my $oldFormat = shift;
10074              
10075             # Prepare
10076 0         0 my @children = $record->children;
10077 0 0       0 return if ! scalar @children;
10078 0         0 my $item = $o->getOrCreate($selector);
10079              
10080             # Merge value
10081 0         0 my $valueRecord = shift @children;
10082 0 0       0 $valueRecord = $valueRecord->firstChild if $oldFormat;
10083 0         0 $item->mergeValue($part, $valueRecord->asInteger, $valueRecord);
10084              
10085             # Merge children
10086 0         0 for my $child (@children) { $o->mergeNode($part, $selector->child($child->bytes), $child, $oldFormat); }
  0         0  
10087             }
10088              
10089             # *** Saving
10090             # Call $document->save at any time to save the current state (if necessary).
10091              
10092             # This is called by the items whenever some data changes.
10093             sub dataChanged {
10094 0     0   0 my $o = shift;
10095             }
10096              
10097             sub save {
10098 0     0   0 my $o = shift;
10099              
10100 0         0 $o->{unsaved}->startSaving;
10101 0         0 my $revision = CDS->now;
10102 0         0 my $newPart = undef;
10103              
10104             #-- saving ++ $o->{changes}->{count}
10105 0 0       0 if ($o->{changes}->{count}) {
10106             # Take the changes
10107 0         0 $newPart = $o->{changes};
10108 0         0 $o->{changes} = CDS::Document::Part->new;
10109              
10110             # Select all parts smaller than 2 * changes
10111 0         0 $newPart->{selected} = 1;
10112 0         0 my $count = $newPart->{count};
10113 0         0 while (1) {
10114 0         0 my $addedPart = 0;
10115 0         0 for my $part (values %{$o->{parts}}) {
  0         0  
10116             #-- candidate ++ $part->{count} ++ $count
10117 0 0 0     0 next if ! $part->{isMerged} || $part->{selected} || $part->{count} >= $count * 2;
      0        
10118 0         0 $count += $part->{count};
10119 0         0 $part->{selected} = 1;
10120 0         0 $addedPart = 1;
10121             }
10122              
10123 0 0       0 last if ! $addedPart;
10124             }
10125              
10126             # Include the selected items
10127 0         0 for my $item (values %{$o->{itemsBySelector}}) {
  0         0  
10128 0 0       0 next if ! $item->{part}->{selected};
10129 0         0 $item->setPart($newPart);
10130 0         0 $item->createSaveRecord;
10131             }
10132              
10133 0         0 my $record = CDS::Record->new;
10134 0         0 $record->add('created')->addInteger($revision);
10135 0         0 $record->add('client')->add(CDS->version);
10136 0         0 $record->addRecord($o->rootItem->createSaveRecord);
10137              
10138             # Detach the save records
10139 0         0 for my $item (values %{$o->{itemsBySelector}}) {
  0         0  
10140 0         0 $item->detachSaveRecord;
10141             }
10142              
10143             # Serialize and encrypt the record
10144 0         0 my $key = CDS->randomKey;
10145 0         0 my $newObject = $record->toObject->crypt($key);
10146 0         0 $newPart->{hashAndKey} = CDS::HashAndKey->new($newObject->calculateHash, $key);
10147 0         0 $newPart->{isMerged} = 1;
10148 0         0 $newPart->{selected} = 0;
10149 0         0 $o->{parts}->{$newPart->{hashAndKey}->hash->bytes} = $newPart;
10150             #-- added ++ $o->{parts} ++ scalar keys %{$o->{parts}} ++ $newPart->{count}
10151 0         0 $o->{unsaved}->{savingState}->addObject($newPart->{hashAndKey}->hash, $newObject);
10152             }
10153              
10154             # Remove obsolete parts
10155 0         0 my $obsoleteParts = [];
10156 0         0 for my $part (values %{$o->{parts}}) {
  0         0  
10157 0 0       0 next if ! $part->{isMerged};
10158 0 0       0 next if $part->{count};
10159 0         0 push @$obsoleteParts, $part;
10160 0         0 delete $o->{parts}->{$part->{hashAndKey}->hash->bytes};
10161             }
10162              
10163             # Commit
10164             #-- saving done ++ $revision ++ $newPart ++ $obsoleteParts
10165 0         0 return $o->savingDone($revision, $newPart, $obsoleteParts);
10166             }
10167              
10168             package CDS::Document::Item;
10169              
10170             sub new {
10171 0     0   0 my $class = shift;
10172 0 0 0     0 my $selector = shift; die 'wrong type '.ref($selector).' for $selector' if defined $selector && ref $selector ne 'CDS::Selector';
  0         0  
10173              
10174 0         0 my $parentSelector = $selector->parent;
10175 0 0       0 my $parent = $parentSelector ? $selector->document->getOrCreate($parentSelector) : undef;
10176              
10177 0         0 my $o = bless {
10178             document => $selector->document,
10179             selector => $selector,
10180             parent => $parent,
10181             children => [],
10182             part => undef,
10183             revision => 0,
10184             record => CDS::Record->new
10185             };
10186              
10187 0 0       0 push @{$parent->{children}}, $o if $parent;
  0         0  
10188 0         0 return $o;
10189             }
10190              
10191             sub pruneTree {
10192 0     0   0 my $o = shift;
10193              
10194             # Try to remove children
10195 0         0 for my $child (@{$o->{children}}) { $child->pruneTree; }
  0         0  
  0         0  
10196              
10197             # Don't remove the root item
10198 0 0       0 return if ! $o->{parent};
10199              
10200             # Don't remove if the item has children, or a value
10201 0 0       0 return if scalar @{$o->{children}};
  0         0  
10202 0 0       0 return if $o->{revision} > 0;
10203              
10204             # Remove this from the tree
10205 0         0 $o->{parent}->{children} = [grep { $_ != $o } @{$o->{parent}->{children}}];
  0         0  
  0         0  
10206              
10207             # Remove this from the document hash
10208 0         0 delete $o->{document}->{itemsBySelector}->{$o->{selector}->{id}};
10209             }
10210              
10211             # Low-level part change.
10212             sub setPart {
10213 0     0   0 my $o = shift;
10214 0         0 my $part = shift;
10215              
10216 0 0       0 $o->{part}->{count} -= 1 if $o->{part};
10217 0         0 $o->{part} = $part;
10218 0 0       0 $o->{part}->{count} += 1 if $o->{part};
10219             }
10220              
10221             # Merge a value
10222              
10223             sub mergeValue {
10224 0     0   0 my $o = shift;
10225 0         0 my $part = shift;
10226 0         0 my $revision = shift;
10227 0 0 0     0 my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0         0  
10228              
10229 0 0       0 return if $revision <= 0;
10230 0 0       0 return if $revision < $o->{revision};
10231 0 0 0     0 return if $revision == $o->{revision} && $part->{size} < $o->{part}->{size};
10232 0         0 $o->setPart($part);
10233 0         0 $o->{revision} = $revision;
10234 0         0 $o->{record} = $record;
10235 0         0 $o->{document}->dataChanged;
10236 0         0 return 1;
10237             }
10238              
10239             sub forget {
10240 0     0   0 my $o = shift;
10241              
10242 0 0       0 return if $o->{revision} <= 0;
10243 0         0 $o->{revision} = 0;
10244 0         0 $o->{record} = CDS::Record->new;
10245 0         0 $o->setPart;
10246             }
10247              
10248             # Saving
10249              
10250             sub createSaveRecord {
10251 0     0   0 my $o = shift;
10252              
10253 0 0       0 return $o->{saveRecord} if $o->{saveRecord};
10254 0 0       0 $o->{saveRecord} = $o->{parent} ? $o->{parent}->createSaveRecord->add($o->{selector}->{label}) : CDS::Record->new('root');
10255 0 0       0 if ($o->{part}->{selected}) {
10256 0 0       0 CDS->log('Item saving zero revision of ', $o->{selector}->label) if $o->{revision} <= 0;
10257 0         0 $o->{saveRecord}->addInteger($o->{revision})->addRecord($o->{record}->children);
10258             } else {
10259 0         0 $o->{saveRecord}->add('');
10260             }
10261 0         0 return $o->{saveRecord};
10262             }
10263              
10264             sub detachSaveRecord {
10265 0     0   0 my $o = shift;
10266              
10267 0 0       0 return if ! $o->{saveRecord};
10268 0         0 delete $o->{saveRecord};
10269 0 0       0 $o->{parent}->detachSaveRecord if $o->{parent};
10270             }
10271              
10272             package CDS::Document::Part;
10273              
10274             sub new {
10275 0     0   0 my $class = shift;
10276              
10277 0         0 return bless {
10278             isMerged => 0,
10279             hashAndKey => undef,
10280             size => 0,
10281             count => 0,
10282             selected => 0,
10283             };
10284             }
10285              
10286             # In this implementation, we only keep track of the number of values of the list, but
10287             # not of the corresponding items. This saves memory (~100 MiB for 1M items), but takes
10288             # more time (0.5 s for 1M items) when saving. Since command line programs usually write
10289             # the document only once, this is acceptable. Reading the tree anyway takes about 10
10290             # times more time.
10291              
10292             package CDS::ErrorHandlingStore;
10293              
10294 1     1   4477 use parent -norequire, 'CDS::Store';
  1         2  
  1         5  
10295              
10296             sub new {
10297 0     0   0 my $class = shift;
10298 0         0 my $store = shift;
10299 0         0 my $url = shift;
10300 0         0 my $errorHandler = shift;
10301              
10302 0         0 return bless {
10303             store => $store,
10304             url => $url,
10305             errorHandler => $errorHandler,
10306             }
10307             }
10308              
10309 0     0   0 sub store { shift->{store} }
10310 0     0   0 sub url { shift->{url} }
10311 0     0   0 sub errorHandler { shift->{errorHandler} }
10312              
10313             sub id {
10314 0     0   0 my $o = shift;
10315 0         0 'Error handling'."\n ".$o->{store}->id }
10316              
10317             sub get {
10318 0     0   0 my $o = shift;
10319 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
10320 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
10321              
10322 0 0       0 return undef, 'Store disabled.' if $o->{errorHandler}->hasStoreError($o, 'GET');
10323              
10324 0         0 my ($object, $error) = $o->{store}->get($hash, $keyPair);
10325 0 0       0 if (defined $error) {
10326 0         0 $o->{errorHandler}->onStoreError($o, 'GET', $error);
10327 0         0 return undef, $error;
10328             }
10329              
10330 0         0 $o->{errorHandler}->onStoreSuccess($o, 'GET');
10331 0         0 return $object, $error;
10332             }
10333              
10334             sub book {
10335 0     0   0 my $o = shift;
10336 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
10337 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
10338              
10339 0 0       0 return undef, 'Store disabled.' if $o->{errorHandler}->hasStoreError($o, 'BOOK');
10340              
10341 0         0 my ($booked, $error) = $o->{store}->book($hash, $keyPair);
10342 0 0       0 if (defined $error) {
10343 0         0 $o->{errorHandler}->onStoreError($o, 'BOOK', $error);
10344 0         0 return undef, $error;
10345             }
10346              
10347 0         0 $o->{errorHandler}->onStoreSuccess($o, 'BOOK');
10348 0         0 return $booked;
10349             }
10350              
10351             sub put {
10352 0     0   0 my $o = shift;
10353 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
10354 0 0 0     0 my $object = shift; die 'wrong type '.ref($object).' for $object' if defined $object && ref $object ne 'CDS::Object';
  0         0  
10355 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
10356              
10357 0 0       0 return 'Store disabled.' if $o->{errorHandler}->hasStoreError($o, 'PUT');
10358              
10359 0         0 my $error = $o->{store}->put($hash, $object, $keyPair);
10360 0 0       0 if (defined $error) {
10361 0         0 $o->{errorHandler}->onStoreError($o, 'PUT', $error);
10362 0         0 return $error;
10363             }
10364              
10365 0         0 $o->{errorHandler}->onStoreSuccess($o, 'PUT');
10366 0         0 return;
10367             }
10368              
10369             sub list {
10370 0     0   0 my $o = shift;
10371 0 0 0     0 my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0         0  
10372 0         0 my $boxLabel = shift;
10373 0         0 my $timeout = shift;
10374 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
10375              
10376 0 0       0 return undef, 'Store disabled.' if $o->{errorHandler}->hasStoreError($o, 'LIST');
10377              
10378 0         0 my ($hashes, $error) = $o->{store}->list($accountHash, $boxLabel, $timeout, $keyPair);
10379 0 0       0 if (defined $error) {
10380 0         0 $o->{errorHandler}->onStoreError($o, 'LIST', $error);
10381 0         0 return undef, $error;
10382             }
10383              
10384 0         0 $o->{errorHandler}->onStoreSuccess($o, 'LIST');
10385 0         0 return $hashes;
10386             }
10387              
10388             sub add {
10389 0     0   0 my $o = shift;
10390 0 0 0     0 my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0         0  
10391 0         0 my $boxLabel = shift;
10392 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
10393 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
10394              
10395 0 0       0 return 'Store disabled.' if $o->{errorHandler}->hasStoreError($o, 'ADD');
10396              
10397 0         0 my $error = $o->{store}->add($accountHash, $boxLabel, $hash, $keyPair);
10398 0 0       0 if (defined $error) {
10399 0         0 $o->{errorHandler}->onStoreError($o, 'ADD', $error);
10400 0         0 return $error;
10401             }
10402              
10403 0         0 $o->{errorHandler}->onStoreSuccess($o, 'ADD');
10404 0         0 return;
10405             }
10406              
10407             sub remove {
10408 0     0   0 my $o = shift;
10409 0 0 0     0 my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0         0  
10410 0         0 my $boxLabel = shift;
10411 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
10412 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
10413              
10414 0 0       0 return 'Store disabled.' if $o->{errorHandler}->hasStoreError($o, 'REMOVE');
10415              
10416 0         0 my $error = $o->{store}->remove($accountHash, $boxLabel, $hash, $keyPair);
10417 0 0       0 if (defined $error) {
10418 0         0 $o->{errorHandler}->onStoreError($o, 'REMOVE', $error);
10419 0         0 return $error;
10420             }
10421              
10422 0         0 $o->{errorHandler}->onStoreSuccess($o, 'REMOVE');
10423 0         0 return;
10424             }
10425              
10426             sub modify {
10427 0     0   0 my $o = shift;
10428 0         0 my $modifications = shift;
10429 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
10430              
10431 0 0       0 return 'Store disabled.' if $o->{errorHandler}->hasStoreError($o, 'MODIFY');
10432              
10433 0         0 my $error = $o->{store}->modify($modifications, $keyPair);
10434 0 0       0 if (defined $error) {
10435 0         0 $o->{errorHandler}->onStoreError($o, 'MODIFY', $error);
10436 0         0 return $error;
10437             }
10438              
10439 0         0 $o->{errorHandler}->onStoreSuccess($o, 'MODIFY');
10440 0         0 return;
10441             }
10442              
10443             # A Condensation store on a local folder.
10444             package CDS::FolderStore;
10445              
10446 1     1   1205 use parent -norequire, 'CDS::Store';
  1         36  
  1         13  
10447              
10448             sub forUrl {
10449 0     0   0 my $class = shift;
10450 0         0 my $url = shift;
10451              
10452 0 0       0 return if substr($url, 0, 8) ne 'file:///';
10453 0         0 return $class->new(substr($url, 7));
10454             }
10455              
10456             sub new {
10457 0     0   0 my $class = shift;
10458 0         0 my $folder = shift;
10459              
10460 0         0 return bless {
10461             folder => $folder,
10462             permissions => CDS::FolderStore::PosixPermissions->forFolder($folder.'/accounts'),
10463             };
10464             }
10465              
10466             sub id {
10467 0     0   0 my $o = shift;
10468 0         0 'file://'.$o->{folder} }
10469 0     0   0 sub folder { shift->{folder} }
10470              
10471 0     0   0 sub permissions { shift->{permissions} }
10472             sub setPermissions {
10473 0     0   0 my $o = shift;
10474 0         0 my $permissions = shift;
10475 0         0 $o->{permissions} = $permissions; }
10476              
10477             sub get {
10478 0     0   0 my $o = shift;
10479 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
10480 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
10481              
10482 0         0 my $hashHex = $hash->hex;
10483 0         0 my $file = $o->{folder}.'/objects/'.substr($hashHex, 0, 2).'/'.substr($hashHex, 2);
10484 0         0 return CDS::Object->fromBytes(CDS->readBytesFromFile($file));
10485             }
10486              
10487             sub book {
10488 0     0   0 my $o = shift;
10489 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
10490 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
10491              
10492             # Book the object if it exists
10493 0         0 my $hashHex = $hash->hex;
10494 0         0 my $folder = $o->{folder}.'/objects/'.substr($hashHex, 0, 2);
10495 0         0 my $file = $folder.'/'.substr($hashHex, 2);
10496 0 0 0     0 return 1 if -e $file && utime(undef, undef, $file);
10497 0         0 return;
10498             }
10499              
10500             sub put {
10501 0     0   0 my $o = shift;
10502 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
10503 0 0 0     0 my $object = shift; die 'wrong type '.ref($object).' for $object' if defined $object && ref $object ne 'CDS::Object';
  0         0  
10504 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
10505              
10506             # Book the object if it exists
10507 0         0 my $hashHex = $hash->hex;
10508 0         0 my $folder = $o->{folder}.'/objects/'.substr($hashHex, 0, 2);
10509 0         0 my $file = $folder.'/'.substr($hashHex, 2);
10510 0 0 0     0 return if -e $file && utime(undef, undef, $file);
10511              
10512             # Write the file, set the permissions, and move it to the right place
10513 0         0 my $permissions = $o->{permissions};
10514 0         0 $permissions->mkdir($folder, $permissions->objectFolderMode);
10515 0   0     0 my $temporaryFile = $permissions->writeTemporaryFile($folder, $permissions->objectFileMode, $object->bytes) // return 'Failed to write object';
10516 0 0       0 rename($temporaryFile, $file) || return 'Failed to rename object.';
10517 0         0 return;
10518             }
10519              
10520             sub list {
10521 0     0   0 my $o = shift;
10522 0 0 0     0 my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0         0  
10523 0         0 my $boxLabel = shift;
10524 0         0 my $timeout = shift;
10525 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
10526              
10527 0 0       0 return undef, 'Invalid box label.' if ! CDS->isValidBoxLabel($boxLabel);
10528              
10529             # Prepare
10530 0         0 my $boxFolder = $o->{folder}.'/accounts/'.$accountHash->hex.'/'.$boxLabel;
10531              
10532             # List
10533 0 0       0 return $o->listFolder($boxFolder) if ! $timeout;
10534              
10535             # Watch
10536 0         0 my $hashes;
10537 0         0 my $watcher = CDS::FolderStore::Watcher->new($boxFolder);
10538 0         0 my $watchUntil = CDS->now + $timeout;
10539 0         0 while (1) {
10540             # List
10541 0         0 $hashes = $o->listFolder($boxFolder);
10542 0 0       0 last if scalar @$hashes;
10543              
10544             # Wait
10545 0   0     0 $watcher->wait($watchUntil - CDS->now, $watchUntil) // last;
10546             }
10547              
10548 0         0 $watcher->done;
10549 0         0 return $hashes;
10550             }
10551              
10552             sub listFolder {
10553 0     0   0 my $o = shift;
10554 0         0 my $boxFolder = shift;
10555             # private
10556 0         0 my $hashes = [];
10557 0         0 for my $file (CDS->listFolder($boxFolder)) {
10558 0   0     0 push @$hashes, CDS::Hash->fromHex($file) // next;
10559             }
10560              
10561 0         0 return $hashes;
10562             }
10563              
10564             sub add {
10565 0     0   0 my $o = shift;
10566 0 0 0     0 my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0         0  
10567 0         0 my $boxLabel = shift;
10568 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
10569 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
10570              
10571 0         0 my $permissions = $o->{permissions};
10572              
10573 0 0       0 next if ! CDS->isValidBoxLabel($boxLabel);
10574 0         0 my $accountFolder = $o->{folder}.'/accounts/'.$accountHash->hex;
10575 0         0 $permissions->mkdir($accountFolder, $permissions->accountFolderMode);
10576 0         0 my $boxFolder = $accountFolder.'/'.$boxLabel;
10577 0         0 $permissions->mkdir($boxFolder, $permissions->boxFolderMode($boxLabel));
10578 0         0 my $boxFileMode = $permissions->boxFileMode($boxLabel);
10579              
10580 0   0     0 my $temporaryFile = $permissions->writeTemporaryFile($boxFolder, $boxFileMode, '') // return 'Failed to write file.';
10581 0 0       0 rename($temporaryFile, $boxFolder.'/'.$hash->hex) || return 'Failed to rename file.';
10582 0         0 return;
10583             }
10584              
10585             sub remove {
10586 0     0   0 my $o = shift;
10587 0 0 0     0 my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0         0  
10588 0         0 my $boxLabel = shift;
10589 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
10590 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
10591              
10592 0 0       0 next if ! CDS->isValidBoxLabel($boxLabel);
10593 0         0 my $accountFolder = $o->{folder}.'/accounts/'.$accountHash->hex;
10594 0         0 my $boxFolder = $accountFolder.'/'.$boxLabel;
10595 0 0       0 next if ! -d $boxFolder;
10596 0         0 unlink $boxFolder.'/'.$hash->hex;
10597 0         0 return;
10598             }
10599              
10600             sub modify {
10601 0     0   0 my $o = shift;
10602 0         0 my $modifications = shift;
10603 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
10604              
10605 0         0 return $modifications->executeIndividually($o, $keyPair);
10606             }
10607              
10608             # Store administration functions
10609              
10610             sub exists {
10611 0     0   0 my $o = shift;
10612              
10613 0   0     0 return -d $o->{folder}.'/accounts' && -d $o->{folder}.'/objects';
10614             }
10615              
10616             # Creates the store if it does not exist. The store folder itself must exist.
10617             sub createIfNecessary {
10618 0     0   0 my $o = shift;
10619              
10620 0         0 my $accountsFolder = $o->{folder}.'/accounts';
10621 0         0 my $objectsFolder = $o->{folder}.'/objects';
10622 0         0 $o->{permissions}->mkdir($accountsFolder, $o->{permissions}->baseFolderMode);
10623 0         0 $o->{permissions}->mkdir($objectsFolder, $o->{permissions}->baseFolderMode);
10624 0   0     0 return -d $accountsFolder && -d $objectsFolder;
10625             }
10626              
10627             # Lists accounts. This is a non-standard extension.
10628             sub accounts {
10629 0     0   0 my $o = shift;
10630              
10631 0         0 return grep { defined $_ }
10632 0         0 map { CDS::Hash->fromHex($_) }
10633 0         0 CDS->listFolder($o->{folder}.'/accounts');
10634             }
10635              
10636             # Adds an account. This is a non-standard extension.
10637             sub addAccount {
10638 0     0   0 my $o = shift;
10639 0 0 0     0 my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0         0  
10640              
10641 0         0 my $accountFolder = $o->{folder}.'/accounts/'.$accountHash->hex;
10642 0         0 $o->{permissions}->mkdir($accountFolder, $o->{permissions}->accountFolderMode);
10643 0         0 return -d $accountFolder;
10644             }
10645              
10646             # Removes an account. This is a non-standard extension.
10647             sub removeAccount {
10648 0     0   0 my $o = shift;
10649 0 0 0     0 my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0         0  
10650              
10651 0         0 my $accountFolder = $o->{folder}.'/accounts/'.$accountHash->hex;
10652 0         0 my $trashFolder = $o->{folder}.'/accounts/.deleted-'.CDS->randomHex(16);
10653 0         0 rename $accountFolder, $trashFolder;
10654 0         0 system('rm', '-rf', $trashFolder);
10655 0         0 return ! -d $accountFolder;
10656             }
10657              
10658             # Checks (and optionally fixes) the POSIX permissions of all files and folders. This is a non-standard extension.
10659             sub checkPermissions {
10660 0     0   0 my $o = shift;
10661 0         0 my $logger = shift;
10662              
10663 0         0 my $permissions = $o->{permissions};
10664              
10665             # Check the accounts folder
10666 0         0 my $accountsFolder = $o->{folder}.'/accounts';
10667 0 0       0 $permissions->checkPermissions($accountsFolder, $permissions->baseFolderMode, $logger) || return;
10668              
10669             # Check the account folders
10670 0         0 for my $account (sort { $a cmp $b } CDS->listFolder($accountsFolder)) {
  0         0  
10671 0 0       0 next if $account !~ /^[0-9a-f]{64}$/;
10672 0         0 my $accountFolder = $accountsFolder.'/'.$account;
10673 0 0       0 $permissions->checkPermissions($accountFolder, $permissions->accountFolderMode, $logger) || return;
10674              
10675             # Check the box folders
10676 0         0 for my $boxLabel (sort { $a cmp $b } CDS->listFolder($accountFolder)) {
  0         0  
10677 0 0       0 next if $boxLabel =~ /^\./;
10678 0         0 my $boxFolder = $accountFolder.'/'.$boxLabel;
10679 0 0       0 $permissions->checkPermissions($boxFolder, $permissions->boxFolderMode($boxLabel), $logger) || return;
10680              
10681             # Check each file
10682 0         0 my $filePermissions = $permissions->boxFileMode($boxLabel);
10683 0         0 for my $file (sort { $a cmp $b } CDS->listFolder($boxFolder)) {
  0         0  
10684 0 0       0 next if $file !~ /^[0-9a-f]{64}/;
10685 0 0       0 $permissions->checkPermissions($boxFolder.'/'.$file, $filePermissions, $logger) || return;
10686             }
10687             }
10688             }
10689              
10690             # Check the objects folder
10691 0         0 my $objectsFolder = $o->{folder}.'/objects';
10692 0         0 my $fileMode = $permissions->objectFileMode;
10693 0         0 my $folderMode = $permissions->objectFolderMode;
10694 0 0       0 $permissions->checkPermissions($objectsFolder, $folderMode, $logger) || return;
10695              
10696             # Check the 256 sub folders
10697 0         0 for my $sub (sort { $a cmp $b } CDS->listFolder($objectsFolder)) {
  0         0  
10698 0 0       0 next if $sub !~ /^[0-9a-f][0-9a-f]$/;
10699 0         0 my $subFolder = $objectsFolder.'/'.$sub;
10700 0 0       0 $permissions->checkPermissions($subFolder, $folderMode, $logger) || return;
10701              
10702 0         0 for my $file (sort { $a cmp $b } CDS->listFolder($subFolder)) {
  0         0  
10703 0 0       0 next if $file !~ /^[0-9a-f]{62}/;
10704 0 0       0 $permissions->checkPermissions($subFolder.'/'.$file, $fileMode, $logger) || return;
10705             }
10706             }
10707              
10708 0         0 return 1;
10709             }
10710              
10711             # Handles POSIX permissions (user, group, and mode).
10712             package CDS::FolderStore::PosixPermissions;
10713              
10714             # Returns the permissions set corresponding to the mode, uid, and gid of the base folder.
10715             # If the permissions are ambiguous, the more restrictive set is chosen.
10716             sub forFolder {
10717 0     0   0 my $class = shift;
10718 0         0 my $folder = shift;
10719              
10720 0         0 my @s = stat $folder;
10721 0   0     0 my $mode = $s[2] // 0;
10722              
10723             return
10724 0 0       0 ($mode & 077) == 077 ? CDS::FolderStore::PosixPermissions::World->new :
    0          
10725             ($mode & 070) == 070 ? CDS::FolderStore::PosixPermissions::Group->new($s[5]) :
10726             CDS::FolderStore::PosixPermissions::User->new($s[4]);
10727             }
10728              
10729 0     0   0 sub uid { shift->{uid} }
10730 0     0   0 sub gid { shift->{gid} }
10731              
10732             sub user {
10733 0     0   0 my $o = shift;
10734              
10735 0   0     0 my $uid = $o->{uid} // return;
10736 0   0     0 return getpwuid($uid) // $uid;
10737             }
10738              
10739             sub group {
10740 0     0   0 my $o = shift;
10741              
10742 0   0     0 my $gid = $o->{gid} // return;
10743 0   0     0 return getgrgid($gid) // $gid;
10744             }
10745              
10746             sub writeTemporaryFile {
10747 0     0   0 my $o = shift;
10748 0         0 my $folder = shift;
10749 0         0 my $mode = shift;
10750              
10751             # Write the file
10752 0         0 my $temporaryFile = $folder.'/.'.CDS->randomHex(16);
10753 0 0       0 open(my $fh, '>:bytes', $temporaryFile) || return;
10754 0         0 print $fh @_;
10755 0         0 close $fh;
10756              
10757             # Set the permissions
10758 0         0 chmod $mode, $temporaryFile;
10759 0         0 my $uid = $o->uid;
10760 0         0 my $gid = $o->gid;
10761 0 0 0     0 chown $uid // -1, $gid // -1, $temporaryFile if defined $uid && $uid != $< || defined $gid && $gid != $(;
      0        
      0        
      0        
      0        
10762 0         0 return $temporaryFile;
10763             }
10764              
10765             sub mkdir {
10766 0     0   0 my $o = shift;
10767 0         0 my $folder = shift;
10768 0         0 my $mode = shift;
10769              
10770 0 0       0 return if -d $folder;
10771              
10772             # Create the folder (note: mode is altered by umask)
10773 0         0 my $success = mkdir $folder, $mode;
10774              
10775             # Set the permissions
10776 0         0 chmod $mode, $folder;
10777 0         0 my $uid = $o->uid;
10778 0         0 my $gid = $o->gid;
10779 0 0 0     0 chown $uid // -1, $gid // -1, $folder if defined $uid && $uid != $< || defined $gid && $gid != $(;
      0        
      0        
      0        
      0        
10780 0         0 return $success;
10781             }
10782              
10783             # Check the permissions of a file or folder, and fix them if desired.
10784             # A logger object is called for the different cases (access error, correct permissions, wrong permissions, error fixing permissions).
10785             sub checkPermissions {
10786 0     0   0 my $o = shift;
10787 0         0 my $item = shift;
10788 0         0 my $expectedMode = shift;
10789 0         0 my $logger = shift;
10790              
10791 0         0 my $expectedUid = $o->uid;
10792 0         0 my $expectedGid = $o->gid;
10793              
10794             # Stat the item
10795 0         0 my @s = stat $item;
10796 0 0       0 return $logger->accessError($item) if ! scalar @s;
10797 0         0 my $mode = $s[2] & 07777;
10798 0         0 my $uid = $s[4];
10799 0         0 my $gid = $s[5];
10800              
10801             # Check
10802 0   0     0 my $wrongUid = defined $expectedUid && $uid != $expectedUid;
10803 0   0     0 my $wrongGid = defined $expectedGid && $gid != $expectedGid;
10804 0         0 my $wrongMode = $mode != $expectedMode;
10805 0 0 0     0 if ($wrongUid || $wrongGid || $wrongMode) {
      0        
10806             # Something is wrong
10807 0 0       0 $logger->wrong($item, $uid, $gid, $mode, $expectedUid, $expectedGid, $expectedMode) || return 1;
10808              
10809             # Fix uid and gid
10810 0 0 0     0 if ($wrongUid || $wrongGid) {
10811 0   0     0 my $count = chown $expectedUid // -1, $expectedGid // -1, $item;
      0        
10812 0 0       0 return $logger->setError($item) if $count < 1;
10813             }
10814              
10815             # Fix mode
10816 0 0       0 if ($wrongMode) {
10817 0         0 my $count = chmod $expectedMode, $item;
10818 0 0       0 return $logger->setError($item) if $count < 1;
10819             }
10820             } else {
10821             # Everything is OK
10822 0         0 $logger->correct($item, $mode, $uid, $gid);
10823             }
10824              
10825 0         0 return 1;
10826             }
10827              
10828             # The store belongs to a group. Every user belonging to the group is treated equivalent, and users are supposed to trust each other to some extent.
10829             # The resulting store will have files belonging to multiple users, but the same group.
10830             package CDS::FolderStore::PosixPermissions::Group;
10831              
10832 1     1   2684 use parent -norequire, 'CDS::FolderStore::PosixPermissions';
  1         2  
  1         4  
10833              
10834             sub new {
10835 0     0   0 my $class = shift;
10836 0         0 my $gid = shift;
10837              
10838 0   0     0 return bless {gid => $gid // $(};
10839             }
10840              
10841             sub target {
10842 0     0   0 my $o = shift;
10843 0         0 'members of the group '.$o->group }
10844 0     0   0 sub baseFolderMode { 0771 }
10845 0     0   0 sub objectFolderMode { 0771 }
10846 0     0   0 sub objectFileMode { 0664 }
10847 0     0   0 sub accountFolderMode { 0771 }
10848             sub boxFolderMode {
10849 0     0   0 my $o = shift;
10850 0         0 my $boxLabel = shift;
10851 0 0       0 $boxLabel eq 'public' ? 0775 : 0770 }
10852             sub boxFileMode {
10853 0     0   0 my $o = shift;
10854 0         0 my $boxLabel = shift;
10855 0 0       0 $boxLabel eq 'public' ? 0664 : 0660 }
10856              
10857             # The store belongs to a single user. Other users shall only be able to read objects and the public box, and post to the message box.
10858             package CDS::FolderStore::PosixPermissions::User;
10859              
10860 1     1   249 use parent -norequire, 'CDS::FolderStore::PosixPermissions';
  1         1  
  1         5  
10861              
10862             sub new {
10863 0     0   0 my $class = shift;
10864 0         0 my $uid = shift;
10865              
10866 0   0     0 return bless {uid => $uid // $<};
10867             }
10868              
10869             sub target {
10870 0     0   0 my $o = shift;
10871 0         0 'user '.$o->user }
10872 0     0   0 sub baseFolderMode { 0711 }
10873 0     0   0 sub objectFolderMode { 0711 }
10874 0     0   0 sub objectFileMode { 0644 }
10875 0     0   0 sub accountFolderMode { 0711 }
10876             sub boxFolderMode {
10877 0     0   0 my $o = shift;
10878 0         0 my $boxLabel = shift;
10879 0 0       0 $boxLabel eq 'public' ? 0755 : 0700 }
10880             sub boxFileMode {
10881 0     0   0 my $o = shift;
10882 0         0 my $boxLabel = shift;
10883 0 0       0 $boxLabel eq 'public' ? 0644 : 0600 }
10884              
10885             # The store is open to everybody. This does not usually make sense, but is offered here for completeness.
10886             # This is the simplest permission scheme.
10887             package CDS::FolderStore::PosixPermissions::World;
10888              
10889 1     1   232 use parent -norequire, 'CDS::FolderStore::PosixPermissions';
  1         2  
  1         3  
10890              
10891             sub new {
10892 0     0   0 my $class = shift;
10893              
10894 0         0 return bless {};
10895             }
10896              
10897 0     0   0 sub target { 'everybody' }
10898 0     0   0 sub baseFolderMode { 0777 }
10899 0     0   0 sub objectFolderMode { 0777 }
10900 0     0   0 sub objectFileMode { 0666 }
10901 0     0   0 sub accountFolderMode { 0777 }
10902 0     0   0 sub boxFolderMode { 0777 }
10903 0     0   0 sub boxFileMode { 0666 }
10904              
10905             package CDS::FolderStore::Watcher;
10906              
10907             sub new {
10908 0     0   0 my $class = shift;
10909 0         0 my $folder = shift;
10910              
10911 0         0 return bless {folder => $folder};
10912             }
10913              
10914             sub wait {
10915 0     0   0 my $o = shift;
10916 0         0 my $remaining = shift;
10917 0         0 my $until = shift;
10918              
10919 0 0       0 return if $remaining <= 0;
10920 0         0 sleep 1;
10921 0         0 return 1;
10922             }
10923              
10924             sub done {
10925 0     0   0 my $o = shift;
10926             }
10927              
10928             package CDS::GroupDataSharer;
10929              
10930             sub new {
10931 0     0   0 my $class = shift;
10932 0         0 my $actor = shift;
10933              
10934 0         0 my $o = bless {
10935             actor => $actor,
10936             label => 'shared group data',
10937             dataHandlers => {},
10938             messageChannel => CDS::MessageChannel->new($actor, 'group data', CDS->MONTH),
10939             revision => 0,
10940             version => '',
10941             }, $class;
10942              
10943 0         0 $actor->storagePrivateRoot->addDataHandler($o->{label}, $o);
10944 0         0 return $o;
10945             }
10946              
10947             ### Group data handlers
10948              
10949             sub addDataHandler {
10950 0     0   0 my $o = shift;
10951 0         0 my $label = shift;
10952 0         0 my $dataHandler = shift;
10953              
10954 0         0 $o->{dataHandlers}->{$label} = $dataHandler;
10955             }
10956              
10957             sub removeDataHandler {
10958 0     0   0 my $o = shift;
10959 0         0 my $label = shift;
10960 0         0 my $dataHandler = shift;
10961              
10962 0         0 my $registered = $o->{dataHandlers}->{$label};
10963 0 0       0 return if $registered != $dataHandler;
10964 0         0 delete $o->{dataHandlers}->{$label};
10965             }
10966              
10967             ### MergeableData interface
10968              
10969             sub addDataTo {
10970 0     0   0 my $o = shift;
10971 0 0 0     0 my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0         0  
10972              
10973 0 0       0 return if ! $o->{revision};
10974 0         0 $record->addInteger($o->{revision})->add($o->{version});
10975             }
10976              
10977             sub mergeData {
10978 0     0   0 my $o = shift;
10979 0 0 0     0 my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0         0  
10980              
10981 0         0 for my $child ($record->children) {
10982 0         0 my $revision = $child->asInteger;
10983 0 0       0 next if $revision <= $o->{revision};
10984              
10985 0         0 $o->{revision} = $revision;
10986 0         0 $o->{version} = $child->bytesValue;
10987             }
10988             }
10989              
10990             sub mergeExternalData {
10991 0     0   0 my $o = shift;
10992 0         0 my $store = shift;
10993 0 0 0     0 my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0         0  
10994 0 0 0     0 my $source = shift; die 'wrong type '.ref($source).' for $source' if defined $source && ref $source ne 'CDS::Source';
  0         0  
10995              
10996 0         0 $o->mergeData($record);
10997 0 0       0 return if ! $source;
10998 0         0 $source->keep;
10999 0         0 $o->{actor}->storagePrivateRoot->unsaved->state->addMergedSource($source);
11000             }
11001              
11002             ### Sending messages
11003              
11004             sub createMessage {
11005 0     0   0 my $o = shift;
11006              
11007 0         0 my $message = CDS::Record->new;
11008 0         0 my $data = $message->add('group data');
11009 0         0 for my $label (keys %{$o->{dataHandlers}}) {
  0         0  
11010 0         0 my $dataHandler = $o->{dataHandlers}->{$label};
11011 0         0 $dataHandler->addDataTo($data->add($label));
11012             }
11013 0         0 return $message;
11014             }
11015              
11016             sub share {
11017 0     0   0 my $o = shift;
11018              
11019             # Get the group data members
11020 0   0     0 my $members = $o->{actor}->getGroupDataMembers // return;
11021 0 0       0 return 1 if ! scalar @$members;
11022              
11023             # Create the group data message, and check if it changed
11024 0         0 my $message = $o->createMessage;
11025 0         0 my $versionHash = $message->toObject->calculateHash;
11026 0 0       0 return if $versionHash->bytes eq $o->{version};
11027              
11028 0         0 $o->{revision} = CDS->now;
11029 0         0 $o->{version} = $versionHash->bytes;
11030 0         0 $o->{actor}->storagePrivateRoot->dataChanged;
11031              
11032             # Procure the sent list
11033 0   0     0 $o->{actor}->procureSentList // return;
11034              
11035             # Get the entrusted keys
11036 0   0     0 my $entrustedKeys = $o->{actor}->getEntrustedKeys // return;
11037              
11038             # Transfer the data
11039 0         0 $o->{messageChannel}->addTransfer([$message->dependentHashes], $o->{actor}->storagePrivateRoot->unsaved, 'group data message');
11040              
11041             # Send the message
11042 0         0 $o->{messageChannel}->setRecipients($members, $entrustedKeys);
11043 0         0 my ($submission, $missingObject) = $o->{messageChannel}->submit($message, $o);
11044 0 0       0 $o->{actor}->onMissingObject($missingObject) if $missingObject;
11045 0 0       0 return if ! $submission;
11046 0         0 return 1;
11047             }
11048              
11049             sub onMessageChannelSubmissionCancelled {
11050 0     0   0 my $o = shift;
11051             }
11052              
11053             sub onMessageChannelSubmissionRecipientDone {
11054 0     0   0 my $o = shift;
11055 0 0 0     0 my $recipientActorOnStore = shift; die 'wrong type '.ref($recipientActorOnStore).' for $recipientActorOnStore' if defined $recipientActorOnStore && ref $recipientActorOnStore ne 'CDS::ActorOnStore';
  0         0  
11056             }
11057              
11058             sub onMessageChannelSubmissionRecipientFailed {
11059 0     0   0 my $o = shift;
11060 0 0 0     0 my $recipientActorOnStore = shift; die 'wrong type '.ref($recipientActorOnStore).' for $recipientActorOnStore' if defined $recipientActorOnStore && ref $recipientActorOnStore ne 'CDS::ActorOnStore';
  0         0  
11061             }
11062              
11063             sub onMessageChannelSubmissionDone {
11064 0     0   0 my $o = shift;
11065 0         0 my $succeeded = shift;
11066 0         0 my $failed = shift;
11067             }
11068              
11069             ### Receiving messages
11070              
11071             sub processGroupDataMessage {
11072 0     0   0 my $o = shift;
11073 0         0 my $message = shift;
11074 0         0 my $section = shift;
11075              
11076 0 0       0 if (! $o->{actor}->isGroupMember($message->sender->publicKey->hash)) {
11077             # TODO:
11078             # If the sender is not a known group member, we should run actor group discovery on the sender. He may be part of us, but we don't know that yet.
11079             # At the very least, we should keep this message, and reconsider it if the actor group changes within the next few minutes (e.g. through another message).
11080 0         0 return;
11081             }
11082              
11083 0         0 for my $child ($section->children) {
11084 0   0     0 my $dataHandler = $o->{dataHandlers}->{$child->bytes} // next;
11085 0         0 $dataHandler->mergeExternalData($message->sender->store, $child, $message->source);
11086             }
11087              
11088 0         0 return 1;
11089             }
11090              
11091             package CDS::HTTPServer;
11092              
11093 1     1   1208 use parent -norequire, 'HTTP::Server::Simple';
  1         2  
  1         4  
11094              
11095             sub new {
11096 0     0   0 my $class = shift;
11097              
11098 0         0 my $o = $class->SUPER::new(@_);
11099 0         0 $o->{logger} = CDS::HTTPServer::Logger->new(*STDERR);
11100 0         0 $o->{handlers} = [];
11101 0         0 return $o;
11102             }
11103              
11104             sub addHandler {
11105 0     0   0 my $o = shift;
11106 0         0 my $handler = shift;
11107              
11108 0         0 push @{$o->{handlers}}, $handler;
  0         0  
11109             }
11110              
11111             sub setLogger {
11112 0     0   0 my $o = shift;
11113 0         0 my $logger = shift;
11114              
11115 0         0 $o->{logger} = $logger;
11116             }
11117              
11118 0     0   0 sub logger { shift->{logger} }
11119              
11120             sub setCorsAllowEverybody {
11121 0     0   0 my $o = shift;
11122 0         0 my $value = shift;
11123              
11124 0         0 $o->{corsAllowEverybody} = $value;
11125             }
11126              
11127 0     0   0 sub corsAllowEverybody { shift->{corsAllowEverybody} }
11128              
11129             # *** HTTP::Server::Simple interface
11130              
11131             sub print_banner {
11132 0     0   0 my $o = shift;
11133              
11134 0         0 $o->{logger}->onServerStarts($o->port);
11135             }
11136              
11137             sub setup {
11138 0     0   0 my $o = shift;
11139              
11140 0         0 my %parameters = @_;
11141             $o->{request} = CDS::HTTPServer::Request->new({
11142             logger => $o->logger,
11143             method => $parameters{method},
11144             path => $parameters{path},
11145             protocol => $parameters{protocol},
11146             queryString => $parameters{query_string},
11147             peerAddress => $parameters{peeraddr},
11148             peerPort => $parameters{peerport},
11149 0         0 headers => {},
11150             corsAllowEverybody => $o->corsAllowEverybody,
11151             });
11152             }
11153              
11154             sub headers {
11155 0     0   0 my $o = shift;
11156 0         0 my $headers = shift;
11157              
11158 0         0 while (scalar @$headers) {
11159 0         0 my $key = shift @$headers;
11160 0         0 my $value = shift @$headers;
11161 0         0 $o->{request}->setHeader($key, $value);
11162             }
11163              
11164             # Read the content length
11165 0   0     0 $o->{request}->setRemainingData($o->{request}->header('content-length') // 0);
11166             }
11167              
11168             sub handler {
11169 0     0   0 my $o = shift;
11170              
11171             # Start writing the log line
11172 0         0 $o->{logger}->onRequestStarts($o->{request});
11173              
11174             # Process the request
11175 0         0 my $responseCode = $o->process;
11176 0         0 $o->{logger}->onRequestDone($o->{request}, $responseCode);
11177              
11178             # Wrap up
11179 0         0 $o->{request}->dropData;
11180 0         0 $o->{request} = undef;
11181 0         0 return;
11182             }
11183              
11184             sub process {
11185 0     0   0 my $o = shift;
11186              
11187             # Run the handler
11188 0         0 for my $handler (@{$o->{handlers}}) {
  0         0  
11189 0   0     0 my $responseCode = $handler->process($o->{request}) || next;
11190 0         0 return $responseCode;
11191             }
11192              
11193             # Default handler
11194 0         0 return $o->{request}->reply404;
11195             }
11196              
11197             sub bad_request {
11198 0     0   0 my $o = shift;
11199              
11200 0         0 my $content = 'Bad Request';
11201 0         0 print 'HTTP/1.1 400 Bad Request', "\r\n";
11202 0         0 print 'Content-Length: ', length $content, "\r\n";
11203 0         0 print 'Content-Type: text/plain; charset=utf-8', "\r\n";
11204 0         0 print "\r\n";
11205 0         0 print $content;
11206 0         0 $o->{request} = undef;
11207             }
11208              
11209             package CDS::HTTPServer::IdentificationHandler;
11210              
11211             sub new {
11212 0     0   0 my $class = shift;
11213 0         0 my $root = shift;
11214              
11215 0         0 return bless {root => $root};
11216             }
11217              
11218             sub process {
11219 0     0   0 my $o = shift;
11220 0         0 my $request = shift;
11221              
11222 0   0     0 my $path = $request->pathAbove($o->{root}) // return;
11223 0 0       0 return if $path ne '/';
11224              
11225             # Options
11226 0 0       0 return $request->replyOptions('HEAD', 'GET') if $request->method eq 'OPTIONS';
11227              
11228             # Get
11229 0 0 0     0 return $request->reply200HTML('Condensation HTTP StoreThis is a Condensation HTTP Store server.') if $request->method eq 'HEAD' || $request->method eq 'GET';
11230              
11231 0         0 return $request->reply405;
11232             }
11233              
11234             package CDS::HTTPServer::Logger;
11235              
11236             sub new {
11237 0     0   0 my $class = shift;
11238 0         0 my $fileHandle = shift;
11239              
11240 0         0 return bless {
11241             fileHandle => $fileHandle,
11242             lineStarted => 0,
11243             };
11244             }
11245              
11246             sub onServerStarts {
11247 0     0   0 my $o = shift;
11248 0         0 my $port = shift;
11249              
11250 0         0 my $fh = $o->{fileHandle};
11251 0         0 my @t = localtime(time);
11252 0         0 printf $fh '%04d-%02d-%02d %02d:%02d:%02d ', $t[5] + 1900, $t[4] + 1, $t[3], $t[2], $t[1], $t[0];
11253 0         0 print $fh 'Server ready at http://localhost:', $port, "\n";
11254             }
11255              
11256             sub onRequestStarts {
11257 0     0   0 my $o = shift;
11258 0         0 my $request = shift;
11259              
11260 0         0 my $fh = $o->{fileHandle};
11261 0         0 my @t = localtime(time);
11262 0         0 printf $fh '%04d-%02d-%02d %02d:%02d:%02d ', $t[5] + 1900, $t[4] + 1, $t[3], $t[2], $t[1], $t[0];
11263 0         0 print $fh $request->peerAddress, ' ', $request->method, ' ', $request->path;
11264 0         0 $o->{lineStarted} = 1;
11265             }
11266              
11267             sub onRequestError {
11268 0     0   0 my $o = shift;
11269 0         0 my $request = shift;
11270              
11271 0         0 my $fh = $o->{fileHandle};
11272 0 0       0 print $fh "\n" if $o->{lineStarted};
11273 0         0 print $fh ' ', @_, "\n";
11274 0         0 $o->{lineStarted} = 0;
11275             }
11276              
11277             sub onRequestDone {
11278 0     0   0 my $o = shift;
11279 0         0 my $request = shift;
11280 0         0 my $responseCode = shift;
11281              
11282 0         0 my $fh = $o->{fileHandle};
11283 0 0       0 print $fh ' ===> ' if ! $o->{lineStarted};
11284 0         0 print $fh ' ', $responseCode, "\n";
11285 0         0 $o->{lineStarted} = 0;
11286             }
11287              
11288             package CDS::HTTPServer::MessageGatewayHandler;
11289              
11290             sub new {
11291 0     0   0 my $class = shift;
11292 0         0 my $root = shift;
11293 0         0 my $actor = shift;
11294 0         0 my $store = shift;
11295 0 0 0     0 my $recipientHash = shift; die 'wrong type '.ref($recipientHash).' for $recipientHash' if defined $recipientHash && ref $recipientHash ne 'CDS::Hash';
  0         0  
11296              
11297 0         0 return bless {root => $root, actor => $actor, store => $store, recipientHash => $recipientHash};
11298             }
11299              
11300             sub process {
11301 0     0   0 my $o = shift;
11302 0         0 my $request = shift;
11303              
11304 0   0     0 my $path = $request->pathAbove($o->{root}) // return;
11305 0 0       0 return if $path ne '/';
11306              
11307             # Options
11308 0 0       0 return $request->replyOptions('HEAD', 'GET', 'PUT', 'POST', 'DELETE') if $request->method eq 'OPTIONS';
11309              
11310             # Prepare a message
11311 0         0 my $message = CDS::Record->new;
11312 0         0 $message->add('time')->addInteger(CDS->now);
11313 0         0 $message->add('ip')->add($request->peerAddress);
11314 0         0 $message->add('method')->add($request->method);
11315 0         0 $message->add('path')->add($request->path);
11316 0         0 $message->add('query string')->add($request->queryString);
11317              
11318 0         0 my $headersRecord = $message->add('headers');
11319 0         0 my $headers = $request->headers;
11320 0         0 for my $key (keys %$headers) {
11321 0         0 $headersRecord->add($key)->add($headers->{$key});
11322             }
11323              
11324             # Prepare a channel
11325 0         0 my $channel = CDS::MessageChannel->new($o->{actor}, CDS->randomBytes(8), CDS->WEEK);
11326 0         0 $o->{messageChannel}->setRecipients([$o->{recipientHash}], []);
11327              
11328             # Add the data
11329 0 0       0 if ($request->remainingData > 1024) {
    0          
11330             # Store the data as a separate object
11331 0         0 my $object = CDS::Object->create(CDS::Object->emptyHeader, $request->readData);
11332 0         0 my $key = CDS->randomKey;
11333 0         0 my $encryptedObject = $object->crypt($key);
11334 0         0 my $hash = $encryptedObject->calculateHash;
11335 0         0 $message->add('data')->addHash($hash);
11336 0         0 $channel->addObject($hash, $encryptedObject);
11337             } elsif ($request->remainingData) {
11338 0         0 $message->add('data')->add($request->readData)
11339             }
11340              
11341             # Submit
11342 0         0 my ($submission, $missingObject) = $channel->submit($message, $o);
11343 0         0 $o->{actor}->sendMessages;
11344              
11345 0 0       0 return $submission ? $request->reply200 : $request->reply500('Unable to send the message.');
11346             }
11347              
11348             sub onMessageChannelSubmissionCancelled {
11349 0     0   0 my $o = shift;
11350             }
11351              
11352             sub onMessageChannelSubmissionRecipientDone {
11353 0     0   0 my $o = shift;
11354 0 0 0     0 my $recipientActorOnStore = shift; die 'wrong type '.ref($recipientActorOnStore).' for $recipientActorOnStore' if defined $recipientActorOnStore && ref $recipientActorOnStore ne 'CDS::ActorOnStore';
  0         0  
11355             }
11356              
11357             sub onMessageChannelSubmissionRecipientFailed {
11358 0     0   0 my $o = shift;
11359 0 0 0     0 my $recipientActorOnStore = shift; die 'wrong type '.ref($recipientActorOnStore).' for $recipientActorOnStore' if defined $recipientActorOnStore && ref $recipientActorOnStore ne 'CDS::ActorOnStore';
  0         0  
11360             }
11361              
11362             sub onMessageChannelSubmissionDone {
11363 0     0   0 my $o = shift;
11364 0         0 my $succeeded = shift;
11365 0         0 my $failed = shift;
11366             }
11367              
11368             package CDS::HTTPServer::Request;
11369              
11370             sub new {
11371 0     0   0 my $class = shift;
11372 0         0 my $parameters = shift;
11373              
11374 0         0 return bless $parameters;
11375             }
11376              
11377 0     0   0 sub logger { shift->{logger} }
11378 0     0   0 sub method { shift->{method} }
11379 0     0   0 sub path { shift->{path} }
11380 0     0   0 sub queryString { shift->{queryString} }
11381 0     0   0 sub peerAddress { shift->{peerAddress} }
11382 0     0   0 sub peerPort { shift->{peerPort} }
11383 0     0   0 sub headers { shift->{headers} }
11384 0     0   0 sub remainingData { shift->{remainingData} }
11385 0     0   0 sub corsAllowEverybody { shift->{corsAllowEverybody} }
11386              
11387             # *** Path
11388              
11389             sub pathAbove {
11390 0     0   0 my $o = shift;
11391 0         0 my $root = shift;
11392              
11393 0 0       0 $root .= '/' if $root !~ /\/$/;
11394 0 0       0 return if substr($o->{path}, 0, length $root) ne $root;
11395 0         0 return substr($o->{path}, length($root) - 1);
11396             }
11397              
11398             # *** Request data
11399              
11400             sub setRemainingData {
11401 0     0   0 my $o = shift;
11402 0         0 my $remainingData = shift;
11403              
11404 0         0 $o->{remainingData} = $remainingData;
11405             }
11406              
11407             # Reads the request data
11408             sub readData {
11409 0     0   0 my $o = shift;
11410              
11411 0         0 my @buffers;
11412 0         0 while ($o->{remainingData} > 0) {
11413 0   0     0 my $read = sysread(STDIN, my $buffer, $o->{remainingData}) || return;
11414 0         0 $o->{remainingData} -= $read;
11415 0         0 push @buffers, $buffer;
11416             }
11417              
11418 0         0 return join('', @buffers);
11419             }
11420              
11421             # Read the request data and writes it directly to a file handle
11422             sub copyDataAndCalculateHash {
11423 0     0   0 my $o = shift;
11424 0         0 my $fh = shift;
11425              
11426 0         0 my $sha = Digest::SHA->new(256);
11427 0         0 while ($o->{remainingData} > 0) {
11428 0   0     0 my $read = sysread(STDIN, my $buffer, $o->{remainingData}) || return;
11429 0         0 $o->{remainingData} -= $read;
11430 0         0 $sha->add($buffer);
11431 0         0 print $fh $buffer;
11432             }
11433              
11434 0         0 return $sha->digest;
11435             }
11436              
11437             # Reads and drops the request data
11438             sub dropData {
11439 0     0   0 my $o = shift;
11440              
11441 0         0 while ($o->{remainingData} > 0) {
11442 0   0     0 $o->{remainingData} -= read(STDIN, my $buffer, $o->{remainingData}) || return;
11443             }
11444             }
11445              
11446             # *** Headers
11447              
11448             sub setHeader {
11449 0     0   0 my $o = shift;
11450 0         0 my $key = shift;
11451 0         0 my $value = shift;
11452              
11453 0         0 $o->{headers}->{lc($key)} = $value;
11454             }
11455              
11456             sub header {
11457 0     0   0 my $o = shift;
11458 0         0 my $key = shift;
11459              
11460 0         0 return $o->{headers}->{lc($key)};
11461             }
11462              
11463             # *** Query string
11464              
11465             sub parseQueryString {
11466 0     0   0 my $o = shift;
11467              
11468 0 0       0 return {} if ! defined $o->{queryString};
11469              
11470 0         0 my $values = {};
11471 0         0 for my $pair (split /&/, $o->{queryString}) {
11472 0 0       0 if ($pair =~ /^(.*?)=(.*)$/) {
11473 0         0 my $key = $1;
11474 0         0 my $value = $2;
11475 0         0 $values->{&uri_decode($key)} = &uri_decode($value);
11476             } else {
11477 0         0 $values->{&uri_decode($pair)} = 1;
11478             }
11479             }
11480              
11481 0         0 return $values;
11482             }
11483              
11484             sub uri_decode {
11485 0     0   0 my $encoded = shift;
11486              
11487 0         0 $encoded =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
  0         0  
11488 0         0 return $encoded;
11489             }
11490              
11491             # *** Condensation signature
11492              
11493             sub checkSignature {
11494 0     0   0 my $o = shift;
11495 0         0 my $store = shift;
11496 0         0 my $contentBytesToSign = shift;
11497              
11498             # Check the date
11499 0   0     0 my $dateString = $o->{headers}->{'condensation-date'} // $o->{headers}->{'date'} // return;
      0        
11500 0   0     0 my $date = HTTP::Date::str2time($dateString) // return;
11501 0         0 my $now = time;
11502 0 0 0     0 return if $date < $now - 120 || $date > $now + 60;
11503              
11504             # Get and check the actor
11505 0   0     0 my $actorHash = CDS::Hash->fromHex($o->{headers}->{'condensation-actor'}) // return;
11506 0         0 my ($publicKeyObject, $error) = $store->get($actorHash);
11507 0 0       0 return if ! $publicKeyObject;
11508 0 0       0 return if ! $publicKeyObject->calculateHash->equals($actorHash);
11509 0   0     0 my $publicKey = CDS::PublicKey->fromObject($publicKeyObject) // return;
11510              
11511             # Text to sign
11512 0         0 my $bytesToSign = $dateString."\0".uc($o->{method})."\0".$o->{headers}->{'host'}.$o->{path};
11513 0 0       0 $bytesToSign .= "\0".$contentBytesToSign if defined $contentBytesToSign;
11514 0         0 my $hashToSign = CDS::Hash->calculateFor($bytesToSign);
11515              
11516             # Check the signature
11517 0   0     0 my $signatureString = $o->{headers}->{'condensation-signature'} // return;
11518 0   0     0 $signatureString =~ /^\s*([0-9a-z]{512,512})\s*$/ // return;
11519 0         0 my $signature = pack('H*', $1);
11520 0 0       0 return if ! $publicKey->verifyHash($hashToSign, $signature);
11521              
11522             # Return the verified actor hash
11523 0         0 return $actorHash;
11524             }
11525              
11526             # *** Reply functions
11527              
11528             sub reply200 {
11529 0     0   0 my $o = shift;
11530 0   0     0 my $content = shift // '';
11531              
11532 0 0       0 return length $content ? $o->reply(200, 'OK', &textContentType, $content) : $o->reply(204, 'No Content', {});
11533             }
11534              
11535             sub reply200Bytes {
11536 0     0   0 my $o = shift;
11537 0   0     0 my $content = shift // '';
11538              
11539 0 0       0 return length $content ? $o->reply(200, 'OK', {'Content-Type' => 'application/octet-stream'}, $content) : $o->reply(204, 'No Content', {});
11540             }
11541              
11542             sub reply200HTML {
11543 0     0   0 my $o = shift;
11544 0   0     0 my $content = shift // '';
11545              
11546 0 0       0 return length $content ? $o->reply(200, 'OK', {'Content-Type' => 'text/html; charset=utf-8'}, $content) : $o->reply(204, 'No Content', {});
11547             }
11548              
11549             sub replyOptions {
11550 0     0   0 my $o = shift;
11551              
11552 0         0 my $headers = {};
11553 0         0 $headers->{'Allow'} = join(', ', @_, 'OPTIONS');
11554 0 0 0     0 $headers->{'Access-Control-Allow-Methods'} = join(', ', @_, 'OPTIONS') if $o->corsAllowEverybody && $o->{headers}->{'origin'};
11555 0         0 return $o->reply(200, 'OK', $headers);
11556             }
11557              
11558             sub replyFatalError {
11559 0     0   0 my $o = shift;
11560              
11561 0         0 $o->{logger}->onRequestError($o, @_);
11562 0         0 return $o->reply500;
11563             }
11564              
11565             sub reply303 {
11566 0     0   0 my $o = shift;
11567 0         0 my $location = shift;
11568 0         0 $o->reply(303, 'See Other', {'Location' => $location}) }
11569 0     0   0 sub reply400 { shift->reply(400, 'Bad Request', &textContentType, @_) }
11570 0     0   0 sub reply403 { shift->reply(403, 'Forbidden', &textContentType, @_) }
11571 0     0   0 sub reply404 { shift->reply(404, 'Not Found', &textContentType, @_) }
11572 0     0   0 sub reply405 { shift->reply(405, 'Method Not Allowed', &textContentType, @_) }
11573 0     0   0 sub reply500 { shift->reply(500, 'Internal Server Error', &textContentType, @_) }
11574 0     0   0 sub reply503 { shift->reply(503, 'Service Not Available', &textContentType, @_) }
11575              
11576             sub reply {
11577 0     0   0 my $o = shift;
11578 0         0 my $responseCode = shift;
11579 0         0 my $responseLabel = shift;
11580 0   0     0 my $headers = shift // {};
11581 0   0     0 my $content = shift // '';
11582              
11583             # Content-related headers
11584 0         0 $headers->{'Content-Length'} = length($content);
11585              
11586             # Origin
11587 0 0 0     0 if ($o->corsAllowEverybody && (my $origin = $o->{headers}->{'origin'})) {
11588 0         0 $headers->{'Access-Control-Allow-Origin'} = $origin;
11589 0         0 $headers->{'Access-Control-Allow-Headers'} = 'Content-Type';
11590 0         0 $headers->{'Access-Control-Max-Age'} = '86400';
11591             }
11592              
11593             # Write the reply
11594 0         0 print 'HTTP/1.1 ', $responseCode, ' ', $responseLabel, "\r\n";
11595 0         0 for my $key (keys %$headers) {
11596 0         0 print $key, ': ', $headers->{$key}, "\r\n";
11597             }
11598 0         0 print "\r\n";
11599 0 0       0 print $content if $o->{method} ne 'HEAD';
11600              
11601             # Return the response code
11602 0         0 return $responseCode;
11603             }
11604              
11605 0     0   0 sub textContentType { {'Content-Type' => 'text/plain; charset=utf-8'} }
11606              
11607             package CDS::HTTPServer::StaticContentHandler;
11608              
11609             sub new {
11610 0     0   0 my $class = shift;
11611 0         0 my $path = shift;
11612 0         0 my $content = shift;
11613 0         0 my $contentType = shift;
11614              
11615 0         0 return bless {
11616             path => $path,
11617             content => $content,
11618             contentType => $contentType,
11619             };
11620             }
11621              
11622             sub process {
11623 0     0   0 my $o = shift;
11624 0         0 my $request = shift;
11625              
11626 0 0       0 return if $request->path ne $o->{path};
11627              
11628             # Options
11629 0 0       0 return $request->replyOptions('HEAD', 'GET') if $request->method eq 'OPTIONS';
11630              
11631             # GET
11632 0 0       0 return $request->reply(200, 'OK', {'Content-Type' => $o->{contentType}}, $o->{content}) if $request->method eq 'GET';
11633              
11634             # Everything else
11635 0         0 return $request->reply405;
11636             }
11637              
11638             package CDS::HTTPServer::StaticFilesHandler;
11639              
11640             sub new {
11641 0     0   0 my $class = shift;
11642 0         0 my $root = shift;
11643 0         0 my $folder = shift;
11644 0   0     0 my $defaultFile = shift // '';
11645              
11646 0         0 return bless {
11647             root => $root,
11648             folder => $folder,
11649             defaultFile => $defaultFile,
11650             mimeTypesByExtension => {
11651             'css' => 'text/css',
11652             'gif' => 'image/gif',
11653             'html' => 'text/html',
11654             'jpg' => 'image/jpeg',
11655             'jpeg' => 'image/jpeg',
11656             'js' => 'application/javascript',
11657             'mp4' => 'video/mp4',
11658             'ogg' => 'video/ogg',
11659             'pdf' => 'application/pdf',
11660             'png' => 'image/png',
11661             'svg' => 'image/svg+xml',
11662             'txt' => 'text/plain',
11663             'webm' => 'video/webm',
11664             'zip' => 'application/zip',
11665             },
11666             };
11667             }
11668              
11669 0     0   0 sub folder { shift->{folder} }
11670 0     0   0 sub defaultFile { shift->{defaultFile} }
11671 0     0   0 sub mimeTypesByExtension { shift->{mimeTypesByExtension} }
11672              
11673             sub setContentType {
11674 0     0   0 my $o = shift;
11675 0         0 my $extension = shift;
11676 0         0 my $contentType = shift;
11677              
11678 0         0 $o->{mimeTypesByExtension}->{$extension} = $contentType;
11679             }
11680              
11681             sub process {
11682 0     0   0 my $o = shift;
11683 0         0 my $request = shift;
11684              
11685             # Options
11686 0 0       0 return $request->replyOptions('HEAD', 'GET') if $request->method eq 'OPTIONS';
11687              
11688             # Get
11689 0 0 0     0 return $o->get($request) if $request->method eq 'GET' || $request->method eq 'HEAD';
11690              
11691             # Anything else
11692 0         0 return $request->reply405;
11693             }
11694              
11695             sub get {
11696 0     0   0 my $o = shift;
11697 0         0 my $request = shift;
11698              
11699 0   0     0 my $path = $request->pathAbove($o->{root}) // return;
11700 0         0 return $o->deliverFileForPath($request, $path);
11701             }
11702              
11703             sub deliverFileForPath {
11704 0     0   0 my $o = shift;
11705 0         0 my $request = shift;
11706 0         0 my $path = shift;
11707              
11708             # Hidden files (starting with a dot), as well as "." and ".." never exist
11709 0         0 for my $segment (split /\/+/, $path) {
11710 0 0       0 return $request->reply404 if $segment =~ /^\./;
11711             }
11712              
11713             # If a folder is requested, we serve the default file
11714 0         0 my $file = $o->{folder}.$path;
11715 0 0       0 if (-d $file) {
11716 0 0       0 return $request->reply404 if ! length $o->{defaultFile};
11717 0 0       0 return $request->reply303($request->path.'/') if $file !~ /\/$/;
11718 0         0 $file .= $o->{defaultFile};
11719             }
11720              
11721 0         0 return $o->deliverFile($request, $file);
11722             }
11723              
11724             sub deliverFile {
11725 0     0   0 my $o = shift;
11726 0         0 my $request = shift;
11727 0         0 my $file = shift;
11728 0   0     0 my $contentType = shift // $o->guessContentType($file);
11729              
11730 0   0     0 my $bytes = $o->readFile($file) // return $request->reply404;
11731 0         0 return $request->reply(200, 'OK', {'Content-Type' => $contentType}, $bytes);
11732             }
11733              
11734             # Guesses the content type from the extension
11735             sub guessContentType {
11736 0     0   0 my $o = shift;
11737 0         0 my $file = shift;
11738              
11739 0 0       0 my $extension = $file =~ /\.([A-Za-z0-9]*)$/ ? lc($1) : '';
11740 0   0     0 return $o->{mimeTypesByExtension}->{$extension} // 'application/octet-stream';
11741             }
11742              
11743             # Reads a file
11744             sub readFile {
11745 0     0   0 my $o = shift;
11746 0         0 my $file = shift;
11747              
11748 0 0       0 open(my $fh, '<:bytes', $file) || return;
11749 0 0       0 if (! -f $fh) {
11750 0         0 close $fh;
11751 0         0 return;
11752             }
11753              
11754 0         0 local $/ = undef;
11755 0         0 my $bytes = <$fh>;
11756 0         0 close $fh;
11757 0         0 return $bytes;
11758             }
11759              
11760             package CDS::HTTPServer::StoreHandler;
11761              
11762             sub new {
11763 0     0   0 my $class = shift;
11764 0         0 my $root = shift;
11765 0         0 my $store = shift;
11766 0         0 my $checkPutHash = shift;
11767 0   0     0 my $checkSignatures = shift // 1;
11768              
11769 0         0 return bless {
11770             root => $root,
11771             store => $store,
11772             checkPutHash => $checkPutHash,
11773             checkEnvelopeHash => $checkPutHash,
11774             checkSignatures => $checkSignatures,
11775             maximumWatchTimeout => 0,
11776             };
11777             }
11778              
11779             sub process {
11780 0     0   0 my $o = shift;
11781 0         0 my $request = shift;
11782              
11783 0   0     0 my $path = $request->pathAbove($o->{root}) // return;
11784              
11785             # Objects request
11786 0 0       0 if ($request->path =~ /^\/objects\/([0-9a-f]{64})$/) {
11787 0         0 my $hash = CDS::Hash->fromHex($1);
11788 0         0 return $o->objects($request, $hash);
11789             }
11790              
11791             # Box request
11792 0 0       0 if ($request->path =~ /^\/accounts\/([0-9a-f]{64})\/(messages|private|public)$/) {
11793 0         0 my $accountHash = CDS::Hash->fromHex($1);
11794 0         0 my $boxLabel = $2;
11795 0         0 return $o->box($request, $accountHash, $boxLabel);
11796             }
11797              
11798             # Box entry request
11799 0 0       0 if ($request->path =~ /^\/accounts\/([0-9a-f]{64})\/(messages|private|public)\/([0-9a-f]{64})$/) {
11800 0         0 my $accountHash = CDS::Hash->fromHex($1);
11801 0         0 my $boxLabel = $2;
11802 0         0 my $hash = CDS::Hash->fromHex($3);
11803 0         0 return $o->boxEntry($request, $accountHash, $boxLabel, $hash);
11804             }
11805              
11806             # Account request
11807 0 0       0 if ($request->path =~ /^\/accounts\/([0-9a-f]{64})$/) {
11808 0 0       0 return $request->replyOptions if $request->method eq 'OPTIONS';
11809 0         0 return $request->reply405;
11810             }
11811              
11812             # Accounts request
11813 0 0       0 if ($request->path =~ /^\/accounts$/) {
11814 0         0 return $o->accounts($request);
11815             }
11816              
11817             # Other requests on /objects or /accounts
11818 0 0       0 if ($request->path =~ /^\/(accounts|objects)(\/|$)/) {
11819 0         0 return $request->reply404;
11820             }
11821              
11822             # Nothing for us
11823 0         0 return;
11824             }
11825              
11826             sub objects {
11827 0     0   0 my $o = shift;
11828 0         0 my $request = shift;
11829 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
11830              
11831             # Options
11832 0 0       0 if ($request->method eq 'OPTIONS') {
11833 0         0 return $request->replyOptions('HEAD', 'GET', 'PUT', 'POST');
11834             }
11835              
11836             # Retrieve object
11837 0 0 0     0 if ($request->method eq 'HEAD' || $request->method eq 'GET') {
11838 0         0 my ($object, $error) = $o->{store}->get($hash);
11839 0 0       0 return $request->replyFatalError($error) if defined $error;
11840 0 0       0 return $request->reply404 if ! $object;
11841             # We don't check the SHA256 sum here - this should be done by the client
11842 0         0 return $request->reply200Bytes($object->bytes);
11843             }
11844              
11845             # Put object
11846 0 0       0 if ($request->method eq 'PUT') {
11847 0   0     0 my $bytes = $request->readData // return $request->reply400('No data received.');
11848 0   0     0 my $object = CDS::Object->fromBytes($bytes) // return $request->reply400('Not a Condensation object.');
11849 0 0 0     0 return $request->reply400('SHA256 sum does not match hash.') if $o->{checkPutHash} && ! $object->calculateHash->equals($hash);
11850              
11851 0 0       0 if ($o->{checkSignatures}) {
11852 0         0 my $checkSignatureStore = CDS::CheckSignatureStore->new($o->{store});
11853 0         0 $checkSignatureStore->put($hash, $object);
11854 0 0       0 return $request->reply403 if ! $request->checkSignature($checkSignatureStore);
11855             }
11856              
11857 0         0 my $error = $o->{store}->put($hash, $object);
11858 0 0       0 return $request->replyFatalError($error) if defined $error;
11859 0         0 return $request->reply200;
11860             }
11861              
11862             # Book object
11863 0 0       0 if ($request->method eq 'POST') {
11864 0 0 0     0 return $request->reply403 if $o->{checkSignatures} && ! $request->checkSignature($o->{store});
11865 0 0       0 return $request->reply400('You cannot send data when booking an object.') if $request->remainingData;
11866 0         0 my ($booked, $error) = $o->{store}->book($hash);
11867 0 0       0 return $request->replyFatalError($error) if defined $error;
11868 0 0       0 return $booked ? $request->reply200 : $request->reply404;
11869             }
11870              
11871 0         0 return $request->reply405;
11872             }
11873              
11874             sub box {
11875 0     0   0 my $o = shift;
11876 0         0 my $request = shift;
11877 0 0 0     0 my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0         0  
11878 0         0 my $boxLabel = shift;
11879              
11880             # Options
11881 0 0       0 if ($request->method eq 'OPTIONS') {
11882 0         0 return $request->replyOptions('HEAD', 'GET', 'PUT', 'POST');
11883             }
11884              
11885             # List box
11886 0 0 0     0 if ($request->method eq 'HEAD' || $request->method eq 'GET') {
11887 0   0     0 my $watch = $request->headers->{'condensation-watch'} // '';
11888 0 0       0 my $timeout = $watch =~ /^(\d+)\s*ms$/ ? $1 + 0 : 0;
11889 0 0       0 $timeout = $o->{maximumWatchTimeout} if $timeout > $o->{maximumWatchTimeout};
11890 0         0 my ($hashes, $error) = $o->{store}->list($accountHash, $boxLabel, $timeout);
11891 0 0       0 return $request->replyFatalError($error) if defined $error;
11892 0         0 return $request->reply200Bytes(join('', map { $_->bytes } @$hashes));
  0         0  
11893             }
11894              
11895 0         0 return $request->reply405;
11896             }
11897              
11898             sub boxEntry {
11899 0     0   0 my $o = shift;
11900 0         0 my $request = shift;
11901 0 0 0     0 my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0         0  
11902 0         0 my $boxLabel = shift;
11903 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
11904              
11905             # Options
11906 0 0       0 if ($request->method eq 'OPTIONS') {
11907 0         0 return $request->replyOptions('HEAD', 'PUT', 'DELETE');
11908             }
11909              
11910             # Add
11911 0 0       0 if ($request->method eq 'PUT') {
11912 0 0       0 if ($o->{checkSignatures}) {
11913 0         0 my $actorHash = $request->checkSignature($o->{store});
11914 0 0       0 return $request->reply403 if ! $actorHash;
11915 0 0       0 return $request->reply403 if ! $o->verifyAddition($actorHash, $accountHash, $boxLabel, $hash);
11916             }
11917              
11918 0         0 my $error = $o->{store}->add($accountHash, $boxLabel, $hash);
11919 0 0       0 return $request->replyFatalError($error) if defined $error;
11920 0         0 return $request->reply200;
11921             }
11922              
11923             # Remove
11924 0 0       0 if ($request->method eq 'DELETE') {
11925 0 0       0 if ($o->{checkSignatures}) {
11926 0         0 my $actorHash = $request->checkSignature($o->{store});
11927 0 0       0 return $request->reply403 if ! $actorHash;
11928 0 0       0 return $request->reply403 if ! $o->verifyRemoval($actorHash, $accountHash, $boxLabel, $hash);
11929             }
11930              
11931 0         0 my ($booked, $error) = $o->{store}->remove($accountHash, $boxLabel, $hash);
11932 0 0       0 return $request->replyFatalError($error) if defined $error;
11933 0         0 return $request->reply200;
11934             }
11935              
11936 0         0 return $request->reply405;
11937             }
11938              
11939             sub accounts {
11940 0     0   0 my $o = shift;
11941 0         0 my $request = shift;
11942              
11943             # Options
11944 0 0       0 if ($request->method eq 'OPTIONS') {
11945 0         0 return $request->replyOptions('POST');
11946             }
11947              
11948             # Modify boxes
11949 0 0       0 if ($request->method eq 'POST') {
11950 0   0     0 my $bytes = $request->readData // return $request->reply400('No data received.');
11951 0         0 my $modifications = CDS::StoreModifications->fromBytes($bytes);
11952 0 0       0 return $request->reply400('Invalid modifications.') if ! $modifications;
11953              
11954 0 0       0 if ($o->{checkSignatures}) {
11955 0         0 my $actorHash = $request->checkSignature(CDS::CheckSignatureStore->new($o->{store}, $modifications->objects), $bytes);
11956 0 0       0 return $request->reply403 if ! $actorHash;
11957 0 0       0 return $request->reply403 if ! $o->verifyModifications($actorHash, $modifications);
11958             }
11959              
11960 0         0 my $error = $o->{store}->modify($modifications);
11961 0 0       0 return $request->replyFatalError($error) if defined $error;
11962 0         0 return $request->reply200;
11963             }
11964              
11965 0         0 return $request->reply405;
11966             }
11967              
11968             sub verifyModifications {
11969 0     0   0 my $o = shift;
11970 0 0 0     0 my $actorHash = shift; die 'wrong type '.ref($actorHash).' for $actorHash' if defined $actorHash && ref $actorHash ne 'CDS::Hash';
  0         0  
11971 0         0 my $modifications = shift;
11972              
11973 0         0 for my $operation (@{$modifications->additions}) {
  0         0  
11974 0 0       0 return if ! $o->verifyAddition($actorHash, $operation->{accountHash}, $operation->{boxLabel}, $operation->{hash});
11975             }
11976              
11977 0         0 for my $operation (@{$modifications->removals}) {
  0         0  
11978 0 0       0 return if ! $o->verifyRemoval($actorHash, $operation->{accountHash}, $operation->{boxLabel}, $operation->{hash});
11979             }
11980              
11981 0         0 return 1;
11982             }
11983              
11984             sub verifyAddition {
11985 0     0   0 my $o = shift;
11986 0 0 0     0 my $actorHash = shift; die 'wrong type '.ref($actorHash).' for $actorHash' if defined $actorHash && ref $actorHash ne 'CDS::Hash';
  0         0  
11987 0 0 0     0 my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0         0  
11988 0         0 my $boxLabel = shift;
11989 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
11990              
11991 0 0       0 return 1 if $accountHash->equals($actorHash);
11992 0 0       0 return 1 if $boxLabel eq 'messages';
11993 0         0 return;
11994             }
11995              
11996             sub verifyRemoval {
11997 0     0   0 my $o = shift;
11998 0 0 0     0 my $actorHash = shift; die 'wrong type '.ref($actorHash).' for $actorHash' if defined $actorHash && ref $actorHash ne 'CDS::Hash';
  0         0  
11999 0 0 0     0 my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0         0  
12000 0         0 my $boxLabel = shift;
12001 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
12002              
12003 0 0       0 return 1 if $accountHash->equals($actorHash);
12004              
12005             # Get the envelope
12006 0         0 my ($bytes, $error) = $o->{store}->get($hash);
12007 0 0       0 return if defined $error;
12008 0 0       0 return 1 if ! defined $bytes;
12009 0   0     0 my $record = CDS::Record->fromObject(CDS::Object->fromBytes($bytes)) // return;
12010              
12011             # Allow anyone listed under "updated by"
12012 0         0 my $actorHashBytes24 = substr($actorHash->bytes, 0, 24);
12013 0         0 for my $child ($record->child('updated by')->children) {
12014 0         0 my $hashBytes24 = $child->bytes;
12015 0 0       0 next if length $hashBytes24 != 24;
12016 0 0       0 return 1 if $hashBytes24 eq $actorHashBytes24;
12017             }
12018              
12019 0         0 return;
12020             }
12021              
12022             # A Condensation store accessed through HTTP or HTTPS.
12023             package CDS::HTTPStore;
12024              
12025 1     1   6052 use parent -norequire, 'CDS::Store';
  1         1  
  1         4  
12026              
12027             sub forUrl {
12028 1     1   3 my $class = shift;
12029 1         2 my $url = shift;
12030              
12031 1 50       8 $url =~ /^(http|https):\/\// || return;
12032 1         4 return $class->new($url);
12033             }
12034              
12035             sub new {
12036 1     1   1 my $class = shift;
12037 1         2 my $url = shift;
12038              
12039 1         6 return bless {url => $url};
12040             }
12041              
12042             sub id {
12043 0     0   0 my $o = shift;
12044 0         0 $o->{url} }
12045              
12046             sub get {
12047 0     0   0 my $o = shift;
12048 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
12049 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
12050              
12051 0         0 my $response = $o->request('GET', $o->{url}.'/objects/'.$hash->hex, HTTP::Headers->new);
12052 0 0       0 return if $response->code == 404;
12053 0 0       0 return undef, 'get ==> HTTP '.$response->status_line if ! $response->is_success;
12054 0         0 return CDS::Object->fromBytes($response->decoded_content(charset => 'none'));
12055             }
12056              
12057             sub put {
12058 0     0   0 my $o = shift;
12059 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
12060 0 0 0     0 my $object = shift; die 'wrong type '.ref($object).' for $object' if defined $object && ref $object ne 'CDS::Object';
  0         0  
12061 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
12062              
12063 0         0 my $headers = HTTP::Headers->new;
12064 0         0 $headers->header('Content-Type' => 'application/condensation-object');
12065 0         0 my $response = $o->request('PUT', $o->{url}.'/objects/'.$hash->hex, $headers, $keyPair, $object->bytes);
12066 0 0       0 return if $response->is_success;
12067 0         0 return 'put ==> HTTP '.$response->status_line;
12068             }
12069              
12070             sub book {
12071 0     0   0 my $o = shift;
12072 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
12073 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
12074              
12075 0         0 my $response = $o->request('POST', $o->{url}.'/objects/'.$hash->hex, HTTP::Headers->new, $keyPair);
12076 0 0       0 return if $response->code == 404;
12077 0 0       0 return 1 if $response->is_success;
12078 0         0 return undef, 'book ==> HTTP '.$response->status_line;
12079             }
12080              
12081             sub list {
12082 0     0   0 my $o = shift;
12083 0 0 0     0 my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0         0  
12084 0         0 my $boxLabel = shift;
12085 0         0 my $timeout = shift;
12086 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
12087              
12088 0         0 my $boxUrl = $o->{url}.'/accounts/'.$accountHash->hex.'/'.$boxLabel;
12089 0         0 my $headers = HTTP::Headers->new;
12090 0 0       0 $headers->header('Condensation-Watch' => $timeout.' ms') if $timeout > 0;
12091 0         0 my $response = $o->request('GET', $boxUrl, $headers);
12092 0 0       0 return undef, 'list ==> HTTP '.$response->status_line if ! $response->is_success;
12093 0         0 my $bytes = $response->decoded_content(charset => 'none');
12094              
12095 0 0       0 if (length($bytes) % 32 != 0) {
12096 0         0 print STDERR 'old procotol', "\n";
12097 0         0 my $hashes = [];
12098 0         0 for my $line (split /\n/, $bytes) {
12099 0   0     0 push @$hashes, CDS::Hash->fromHex($line) // next;
12100             }
12101 0         0 return $hashes;
12102             }
12103              
12104 0         0 my $countHashes = int(length($bytes) / 32);
12105 0         0 return [map { CDS::Hash->fromBytes(substr($bytes, $_ * 32, 32)) } 0 .. $countHashes - 1];
  0         0  
12106             }
12107              
12108             sub add {
12109 0     0   0 my $o = shift;
12110 0 0 0     0 my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0         0  
12111 0         0 my $boxLabel = shift;
12112 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
12113 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
12114              
12115 0         0 my $headers = HTTP::Headers->new;
12116 0         0 my $response = $o->request('PUT', $o->{url}.'/accounts/'.$accountHash->hex.'/'.$boxLabel.'/'.$hash->hex, $headers, $keyPair);
12117 0 0       0 return if $response->is_success;
12118 0         0 return 'add ==> HTTP '.$response->status_line;
12119             }
12120              
12121             sub remove {
12122 0     0   0 my $o = shift;
12123 0 0 0     0 my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0         0  
12124 0         0 my $boxLabel = shift;
12125 0 0 0     0 my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0         0  
12126 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
12127              
12128 0         0 my $headers = HTTP::Headers->new;
12129 0         0 my $response = $o->request('DELETE', $o->{url}.'/accounts/'.$accountHash->hex.'/'.$boxLabel.'/'.$hash->hex, $headers, $keyPair);
12130 0 0       0 return if $response->is_success;
12131 0         0 return 'remove ==> HTTP '.$response->status_line;
12132             }
12133              
12134             sub modify {
12135 0     0   0 my $o = shift;
12136 0         0 my $modifications = shift;
12137 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
12138              
12139 0         0 my $bytes = $modifications->toRecord->toObject->bytes;
12140 0         0 my $headers = HTTP::Headers->new;
12141 0         0 $headers->header('Content-Type' => 'application/condensation-modifications');
12142 0         0 my $response = $o->request('POST', $o->{url}.'/accounts', $headers, $keyPair, $bytes, 1);
12143 0 0       0 return if $response->is_success;
12144 0         0 return 'modify ==> HTTP '.$response->status_line;
12145             }
12146              
12147             # Executes a HTTP request.
12148             sub request {
12149 0     0   0 my $class = shift;
12150 0         0 my $method = shift;
12151 0         0 my $url = shift;
12152 0         0 my $headers = shift;
12153 0 0 0     0 my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0         0  
12154 0         0 my $data = shift;
12155 0         0 my $signData = shift;
12156             # private
12157 0         0 $headers->date(time);
12158 0         0 $headers->header('User-Agent' => CDS->version);
12159              
12160 0 0       0 if ($keyPair) {
12161 0 0       0 my $hostAndPath = $url =~ /^https?:\/\/(.*)$/ ? $1 : $url;
12162 0         0 my $date = CDS::ISODate->millisecondString;
12163 0         0 my $bytesToSign = $date."\0".uc($method)."\0".$hostAndPath;
12164 0 0       0 $bytesToSign .= "\0".$data if $signData;
12165 0         0 my $hashBytesToSign = Digest::SHA::sha256($bytesToSign);
12166 0         0 my $signature = $keyPair->sign($hashBytesToSign);
12167 0         0 $headers->header('Condensation-Date' => $date);
12168 0         0 $headers->header('Condensation-Actor' => $keyPair->publicKey->hash->hex);
12169 0         0 $headers->header('Condensation-Signature' => unpack('H*', $signature));
12170             }
12171              
12172 0         0 return LWP::UserAgent->new->request(HTTP::Request->new($method, $url, $headers, $data));
12173             }
12174              
12175             # Models a hash, and offers binary and hexadecimal representation.
12176             package CDS::Hash;
12177              
12178             sub fromBytes {
12179 0     0   0 my $class = shift;
12180 0   0     0 my $hashBytes = shift // return;
12181              
12182 0 0       0 return if length $hashBytes != 32;
12183 0         0 return bless \$hashBytes;
12184             }
12185              
12186             sub fromHex {
12187 4     4   85 my $class = shift;
12188 4   50     12 my $hashHex = shift // return;
12189              
12190 4 100       28 $hashHex =~ /^\s*([a-fA-F0-9]{64,64})\s*$/ || return;
12191 2         16 my $hashBytes = pack('H*', $hashHex);
12192 2         10 return bless \$hashBytes;
12193             }
12194              
12195             sub calculateFor {
12196 0     0     my $class = shift;
12197 0           my $bytes = shift;
12198              
12199             # The Perl built-in SHA256 implementation is a tad faster than our SHA256 implementation.
12200             #return $class->fromBytes(CDS::C::sha256($bytes));
12201 0           return $class->fromBytes(Digest::SHA::sha256($bytes));
12202             }
12203              
12204             sub hex {
12205 0     0     my $o = shift;
12206              
12207 0           return unpack('H*', $$o);
12208             }
12209              
12210             sub shortHex {
12211 0     0     my $o = shift;
12212              
12213 0           return unpack('H*', substr($$o, 0, 8)) . '…';
12214             }
12215              
12216             sub bytes {
12217 0     0     my $o = shift;
12218 0           $$o }
12219              
12220             sub equals {
12221 0     0     my $this = shift;
12222 0           my $that = shift;
12223              
12224 0 0 0       return 1 if ! defined $this && ! defined $that;
12225 0 0 0       return if ! defined $this || ! defined $that;
12226 0           return $$this eq $$that;
12227             }
12228              
12229             sub cmp {
12230 0     0     my $this = shift;
12231 0           my $that = shift;
12232 0           $$this cmp $$that }
12233              
12234             # A hash with an AES key.
12235             package CDS::HashAndKey;
12236              
12237             sub new {
12238 0     0     my $class = shift;
12239 0 0 0       my $hash = shift // return; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0   0        
12240 0   0       my $key = shift // return;
12241              
12242 0           return bless {
12243             hash => $hash,
12244             key => $key,
12245             };
12246             }
12247              
12248 0     0     sub hash { shift->{hash} }
12249 0     0     sub key { shift->{key} }
12250              
12251             package CDS::ISODate;
12252              
12253             # Parses a date accepting various ISO variants, and calculates the timestamp using Time::Local
12254             sub parse {
12255 0     0     my $class = shift;
12256 0   0       my $dateString = shift // return;
12257              
12258 0 0         if ($dateString =~ /^(\d\d\d\d)-(\d\d)-(\d\d)$/) {
    0          
    0          
    0          
    0          
    0          
12259 0           return (timegm(0, 0, 0, $3, $2 - 1, $1 - 1900) + 86400 - 30) * 1000;
12260             } elsif ($dateString =~ /^(\d\d\d\d)-(\d\d)-(\d\d)(T|\s+)(\d\d):(\d\d):(\d\d|\d\d\.\d*)$/) {
12261 0           return (timelocal(0, $6, $5, $3, $2 - 1, $1 - 1900) + $7) * 1000;
12262             } elsif ($dateString =~ /^(\d\d\d\d)-(\d\d)-(\d\d)(T|\s+)(\d\d):(\d\d):(\d\d|\d\d\.\d*)Z$/) {
12263 0           return (timegm(0, $6, $5, $3, $2 - 1, $1 - 1900) + $7) * 1000;
12264             } elsif ($dateString =~ /^(\d\d\d\d)-(\d\d)-(\d\d)(T|\s+)(\d\d):(\d\d):(\d\d|\d\d\.\d*)+(\d\d):(\d\d)$/) {
12265 0           return (timegm(0, $6, $5, $3, $2 - 1, $1 - 1900) + $7 - $8 * 3600 - $9 * 60) * 1000;
12266             } elsif ($dateString =~ /^(\d\d\d\d)-(\d\d)-(\d\d)(T|\s+)(\d\d):(\d\d):(\d\d|\d\d\.\d*)-(\d\d):(\d\d)$/) {
12267 0           return (timegm(0, $6, $5, $3, $2 - 1, $1 - 1900) + $7 + $8 * 3600 + $9 * 60) * 1000;
12268             } elsif ($dateString =~ /^\s*(\d+)\s*$/) {
12269 0           return $1;
12270             } else {
12271 0           return;
12272             }
12273             }
12274              
12275             # Returns a properly formatted string with a precision of 1 day (i.e., the "date" only)
12276             sub dayString {
12277 0     0     my $class = shift;
12278 0   0       my $time = shift // 1000 * time;
12279              
12280 0           my @t = gmtime($time / 1000);
12281 0           return sprintf('%04d-%02d-%02d', $t[5] + 1900, $t[4] + 1, $t[3]);
12282             }
12283              
12284             # Returns a properly formatted string with a precision of 1 second (i.e., "time of day" and "date") using UTC
12285             sub secondString {
12286 0     0     my $class = shift;
12287 0   0       my $time = shift // 1000 * time;
12288              
12289 0           my @t = gmtime($time / 1000);
12290 0           return sprintf('%04d-%02d-%02dT%02d:%02d:%02dZ', $t[5] + 1900, $t[4] + 1, $t[3], $t[2], $t[1], $t[0]);
12291             }
12292              
12293             # Returns a properly formatted string with a precision of 1 second (i.e., "time of day" and "date") using UTC
12294             sub millisecondString {
12295 0     0     my $class = shift;
12296 0   0       my $time = shift // 1000 * time;
12297              
12298 0           my @t = gmtime($time / 1000);
12299 0           return sprintf('%04d-%02d-%02dT%02d:%02d:%02d.%03dZ', $t[5] + 1900, $t[4] + 1, $t[3], $t[2], $t[1], $t[0], int($time) % 1000);
12300             }
12301              
12302             # Returns a properly formatted string with a precision of 1 second (i.e., "time of day" and "date") using local time
12303             sub localSecondString {
12304 0     0     my $class = shift;
12305 0   0       my $time = shift // 1000 * time;
12306              
12307 0           my @t = localtime($time / 1000);
12308 0           return sprintf('%04d-%02d-%02dT%02d:%02d:%02d', $t[5] + 1900, $t[4] + 1, $t[3], $t[2], $t[1], $t[0]);
12309             }
12310              
12311             package CDS::InMemoryStore;
12312              
12313             sub create {
12314 0     0     my $class = shift;
12315              
12316 0           return CDS::InMemoryStore->new('inMemoryStore:'.unpack('H*', CDS->randomBytes(16)));
12317             }
12318              
12319             sub new {
12320 0     0     my $o = shift;
12321 0           my $id = shift;
12322              
12323 0           return bless {
12324             id => $id,
12325             objects => {},
12326             accounts => {},
12327             };
12328             }
12329              
12330 0     0     sub id { shift->{id} }
12331              
12332             sub accountForWriting {
12333 0     0     my $o = shift;
12334 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
12335              
12336 0           my $account = $o->{accounts}->{$hash->bytes};
12337 0 0         return $account if $account;
12338 0           return $o->{accounts}->{$hash->bytes} = {messages => {}, private => {}, public => {}};
12339             }
12340              
12341             # *** Store interface
12342              
12343             sub get {
12344 0     0     my $o = shift;
12345 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
12346 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
12347              
12348 0   0       my $entry = $o->{objects}->{$hash->bytes} // return;
12349 0           return $entry->{object};
12350             }
12351              
12352             sub book {
12353 0     0     my $o = shift;
12354 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
12355 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
12356              
12357 0   0       my $entry = $o->{objects}->{$hash->bytes} // return;
12358 0           $entry->{booked} = CDS->now;
12359 0           return 1;
12360             }
12361              
12362             sub put {
12363 0     0     my $o = shift;
12364 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
12365 0 0 0       my $object = shift; die 'wrong type '.ref($object).' for $object' if defined $object && ref $object ne 'CDS::Object';
  0            
12366 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
12367              
12368 0           $o->{objects}->{$hash->bytes} = {object => $object, booked => CDS->now};
12369 0           return;
12370             }
12371              
12372             sub list {
12373 0     0     my $o = shift;
12374 0 0 0       my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0            
12375 0           my $boxLabel = shift;
12376 0           my $timeout = shift;
12377 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
12378              
12379 0   0       my $account = $o->{accounts}->{$accountHash->bytes} // return [];
12380 0   0       my $box = $account->{$boxLabel} // return undef, 'Invalid box label.';
12381 0           return values %$box;
12382             }
12383              
12384             sub add {
12385 0     0     my $o = shift;
12386 0 0 0       my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0            
12387 0           my $boxLabel = shift;
12388 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
12389 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
12390              
12391 0   0       my $box = $o->accountForWriting($accountHash)->{$boxLabel} // return;
12392 0           $box->{$hash->bytes} = $hash;
12393             }
12394              
12395             sub remove {
12396 0     0     my $o = shift;
12397 0 0 0       my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0            
12398 0           my $boxLabel = shift;
12399 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
12400 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
12401              
12402 0   0       my $box = $o->accountForWriting($accountHash)->{$boxLabel} // return;
12403 0           delete $box->{$hash->bytes};
12404             }
12405              
12406             sub modify {
12407 0     0     my $o = shift;
12408 0           my $modifications = shift;
12409 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
12410              
12411 0           return $modifications->executeIndividually($o, $keyPair);
12412             }
12413              
12414             # Garbage collection
12415              
12416             sub collectGarbage {
12417 0     0     my $o = shift;
12418 0           my $graceTime = shift;
12419              
12420             # Mark all objects as not used
12421 0           for my $entry (values %{$o->{objects}}) {
  0            
12422 0           $entry->{inUse} = 0;
12423             }
12424              
12425             # Mark all objects newer than the grace time
12426 0           for my $entry (values %{$o->{objects}}) {
  0            
12427 0 0         $o->markEntry($entry) if $entry->{booked} > $graceTime;
12428             }
12429              
12430             # Mark all objects referenced from a box
12431 0           for my $account (values %{$o->{accounts}}) {
  0            
12432 0           for my $hash (values %{$account->{messages}}) { $o->markHash($hash); }
  0            
  0            
12433 0           for my $hash (values %{$account->{private}}) { $o->markHash($hash); }
  0            
  0            
12434 0           for my $hash (values %{$account->{public}}) { $o->markHash($hash); }
  0            
  0            
12435             }
12436              
12437             # Remove empty accounts
12438 0           while (my ($key, $account) = each %{$o->{accounts}}) {
  0            
12439 0 0         next if scalar keys %{$account->{messages}};
  0            
12440 0 0         next if scalar keys %{$account->{private}};
  0            
12441 0 0         next if scalar keys %{$account->{public}};
  0            
12442 0           delete $o->{accounts}->{$key};
12443             }
12444              
12445             # Remove obsolete objects
12446 0           while (my ($key, $entry) = each %{$o->{objects}}) {
  0            
12447 0 0         next if $entry->{inUse};
12448 0           delete $o->{objects}->{$key};
12449             }
12450             }
12451              
12452             sub markHash {
12453 0     0     my $o = shift;
12454 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
12455             # private
12456 0   0       my $child = $o->{objects}->{$hash->bytes} // return;
12457 0           $o->mark($child);
12458             }
12459              
12460             sub markEntry {
12461 0     0     my $o = shift;
12462 0           my $entry = shift;
12463             # private
12464 0 0         return if $entry->{inUse};
12465 0           $entry->{inUse} = 1;
12466              
12467             # Mark all children
12468 0           for my $hash ($entry->{object}->hashes) {
12469 0           $o->markHash($hash);
12470             }
12471             }
12472              
12473             package CDS::KeyPair;
12474              
12475             sub transfer {
12476 0     0     my $o = shift;
12477 0           my $hashes = shift;
12478 0           my $sourceStore = shift;
12479 0           my $destinationStore = shift;
12480              
12481 0           for my $hash (@$hashes) {
12482 0           my ($missing, $store, $storeError) = $o->recursiveTransfer($hash, $sourceStore, $destinationStore, {});
12483 0 0         return $missing if $missing;
12484 0 0         return undef, $store, $storeError if defined $storeError;
12485             }
12486              
12487 0           return;
12488             }
12489              
12490             sub recursiveTransfer {
12491 0     0     my $o = shift;
12492 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
12493 0           my $sourceStore = shift;
12494 0           my $destinationStore = shift;
12495 0           my $done = shift;
12496             # private
12497 0 0         return if $done->{$hash->bytes};
12498 0           $done->{$hash->bytes} = 1;
12499              
12500             # Book
12501 0           my ($booked, $bookError) = $destinationStore->book($hash, $o);
12502 0 0         return undef, $destinationStore, $bookError if defined $bookError;
12503 0 0         return if $booked;
12504              
12505             # Get
12506 0           my ($object, $getError) = $sourceStore->get($hash, $o);
12507 0 0         return undef, $sourceStore, $getError if defined $getError;
12508 0 0         return CDS::MissingObject->new($hash, $sourceStore) if ! defined $object;
12509              
12510             # Process children
12511 0           for my $child ($object->hashes) {
12512 0           my ($missing, $store, $error) = $o->recursiveTransfer($child, $sourceStore, $destinationStore, $done);
12513 0 0         return undef, $store, $error if defined $error;
12514 0 0         if (defined $missing) {
12515 0           push @{$missing->{path}}, $child;
  0            
12516 0           return $missing;
12517             }
12518             }
12519              
12520             # Put
12521 0           my $putError = $destinationStore->put($hash, $object, $o);
12522 0 0         return undef, $destinationStore, $putError if defined $putError;
12523 0           return;
12524             }
12525              
12526             sub createPublicEnvelope {
12527 0     0     my $o = shift;
12528 0 0 0       my $contentHash = shift; die 'wrong type '.ref($contentHash).' for $contentHash' if defined $contentHash && ref $contentHash ne 'CDS::Hash';
  0            
12529              
12530 0           my $envelope = CDS::Record->new;
12531 0           $envelope->add('content')->addHash($contentHash);
12532 0           $envelope->add('signature')->add($o->signHash($contentHash));
12533 0           return $envelope;
12534             }
12535              
12536             sub createPrivateEnvelope {
12537 0     0     my $o = shift;
12538 0           my $contentHashAndKey = shift;
12539 0           my $recipientPublicKeys = shift;
12540              
12541 0           my $envelope = CDS::Record->new;
12542 0           $envelope->add('content')->addHash($contentHashAndKey->hash);
12543 0           $o->addRecipientsToEnvelope($envelope, $contentHashAndKey->key, $recipientPublicKeys);
12544 0           $envelope->add('signature')->add($o->signHash($contentHashAndKey->hash));
12545 0           return $envelope;
12546             }
12547              
12548             sub createMessageEnvelope {
12549 0     0     my $o = shift;
12550 0           my $storeUrl = shift;
12551 0 0 0       my $messageRecord = shift; die 'wrong type '.ref($messageRecord).' for $messageRecord' if defined $messageRecord && ref $messageRecord ne 'CDS::Record';
  0            
12552 0           my $recipientPublicKeys = shift;
12553 0           my $expires = shift;
12554              
12555 0           my $contentRecord = CDS::Record->new;
12556 0           $contentRecord->add('store')->addText($storeUrl);
12557 0           $contentRecord->add('sender')->addHash($o->publicKey->hash);
12558 0           $contentRecord->addRecord($messageRecord->children);
12559 0           my $contentObject = $contentRecord->toObject;
12560 0           my $contentKey = CDS->randomKey;
12561 0           my $encryptedContent = CDS::C::aesCrypt($contentObject->bytes, $contentKey, CDS->zeroCTR);
12562             #my $hashToSign = $contentObject->calculateHash; # prior to 2020-05-05
12563 0           my $hashToSign = CDS::Hash->calculateFor($encryptedContent);
12564              
12565 0           my $envelope = CDS::Record->new;
12566 0           $envelope->add('content')->add($encryptedContent);
12567 0           $o->addRecipientsToEnvelope($envelope, $contentKey, $recipientPublicKeys);
12568 0           $envelope->add('updated by')->add(substr($o->publicKey->hash->bytes, 0, 24));
12569 0 0         $envelope->add('expires')->addInteger($expires) if defined $expires;
12570 0           $envelope->add('signature')->add($o->signHash($hashToSign));
12571 0           return $envelope;
12572             }
12573              
12574             sub addRecipientsToEnvelope {
12575 0     0     my $o = shift;
12576 0 0 0       my $envelope = shift; die 'wrong type '.ref($envelope).' for $envelope' if defined $envelope && ref $envelope ne 'CDS::Record';
  0            
12577 0           my $key = shift;
12578 0           my $recipientPublicKeys = shift;
12579             # private
12580 0           my $encryptedKeyRecord = $envelope->add('encrypted for');
12581 0           my $myHashBytes24 = substr($o->{publicKey}->hash->bytes, 0, 24);
12582 0           $encryptedKeyRecord->add($myHashBytes24)->add($o->{publicKey}->encrypt($key));
12583 0           for my $publicKey (@$recipientPublicKeys) {
12584 0 0         next if $publicKey->hash->equals($o->{publicKey}->hash);
12585 0           my $hashBytes24 = substr($publicKey->hash->bytes, 0, 24);
12586 0           $encryptedKeyRecord->add($hashBytes24)->add($publicKey->encrypt($key));
12587             }
12588             }
12589              
12590             sub generate {
12591 0     0     my $class = shift;
12592              
12593             # Generate a new private key
12594 0           my $rsaPrivateKey = CDS::C::privateKeyGenerate();
12595              
12596             # Serialize the public key
12597 0           my $rsaPublicKey = CDS::C::publicKeyFromPrivateKey($rsaPrivateKey);
12598 0           my $record = CDS::Record->new;
12599 0           $record->add('e')->add(CDS::C::publicKeyE($rsaPublicKey));
12600 0           $record->add('n')->add(CDS::C::publicKeyN($rsaPublicKey));
12601 0           my $publicKey = CDS::PublicKey->fromObject($record->toObject);
12602              
12603             # Return a new CDS::KeyPair instance
12604 0           return CDS::KeyPair->new($publicKey, $rsaPrivateKey);
12605             }
12606              
12607             sub fromFile {
12608 0     0     my $class = shift;
12609 0           my $file = shift;
12610              
12611 0   0       my $bytes = CDS->readBytesFromFile($file) // return;
12612 0           my $record = CDS::Record->fromObject(CDS::Object->fromBytes($bytes));
12613 0           return $class->fromRecord($record);
12614             }
12615              
12616             sub fromHex {
12617 0     0     my $class = shift;
12618 0           my $hex = shift;
12619              
12620 0           return $class->fromRecord(CDS::Record->fromObject(CDS::Object->fromBytes(pack 'H*', $hex)));
12621             }
12622              
12623             sub fromRecord {
12624 0     0     my $class = shift;
12625 0 0 0       my $record = shift // return; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0   0        
12626              
12627 0   0       my $publicKey = CDS::PublicKey->fromObject(CDS::Object->fromBytes($record->child('public key object')->bytesValue)) // return;
12628 0           my $rsaKey = $record->child('rsa key');
12629 0           my $e = $rsaKey->child('e')->bytesValue;
12630 0           my $p = $rsaKey->child('p')->bytesValue;
12631 0           my $q = $rsaKey->child('q')->bytesValue;
12632 0   0       return $class->new($publicKey, CDS::C::privateKeyNew($e, $p, $q) // return);
12633             }
12634              
12635             sub new {
12636 0     0     my $class = shift;
12637 0 0 0       my $publicKey = shift; die 'wrong type '.ref($publicKey).' for $publicKey' if defined $publicKey && ref $publicKey ne 'CDS::PublicKey';
  0            
12638 0           my $rsaPrivateKey = shift;
12639              
12640 0           return bless {
12641             publicKey => $publicKey, # The public key
12642             rsaPrivateKey => $rsaPrivateKey, # The private key
12643             };
12644             }
12645              
12646 0     0     sub publicKey { shift->{publicKey} }
12647 0     0     sub rsaPrivateKey { shift->{rsaPrivateKey} }
12648              
12649             ### Serialization ###
12650              
12651             sub toRecord {
12652 0     0     my $o = shift;
12653              
12654 0           my $record = CDS::Record->new;
12655 0           $record->add('public key object')->add($o->{publicKey}->object->bytes);
12656 0           my $rsaKeyRecord = $record->add('rsa key');
12657 0           $rsaKeyRecord->add('e')->add(CDS::C::privateKeyE($o->{rsaPrivateKey}));
12658 0           $rsaKeyRecord->add('p')->add(CDS::C::privateKeyP($o->{rsaPrivateKey}));
12659 0           $rsaKeyRecord->add('q')->add(CDS::C::privateKeyQ($o->{rsaPrivateKey}));
12660 0           return $record;
12661             }
12662              
12663             sub toHex {
12664 0     0     my $o = shift;
12665              
12666 0           my $object = $o->toRecord->toObject;
12667 0           return unpack('H*', $object->header).unpack('H*', $object->data);
12668             }
12669              
12670             sub writeToFile {
12671 0     0     my $o = shift;
12672 0           my $file = shift;
12673              
12674 0           my $object = $o->toRecord->toObject;
12675 0           return CDS->writeBytesToFile($file, $object->bytes);
12676             }
12677              
12678             ### Private key interface ###
12679              
12680             sub decrypt {
12681 0     0     my $o = shift;
12682 0           my $bytes = shift;
12683             # decrypt(bytes) -> bytes
12684 0           return CDS::C::privateKeyDecrypt($o->{rsaPrivateKey}, $bytes);
12685             }
12686              
12687             sub sign {
12688 0     0     my $o = shift;
12689 0           my $digest = shift;
12690             # sign(bytes) -> bytes
12691 0           return CDS::C::privateKeySign($o->{rsaPrivateKey}, $digest);
12692             }
12693              
12694             sub signHash {
12695 0     0     my $o = shift;
12696 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
12697             # signHash(hash) -> bytes
12698 0           return CDS::C::privateKeySign($o->{rsaPrivateKey}, $hash->bytes);
12699             }
12700              
12701             ### Retrieval ###
12702              
12703             # Retrieves an object from one of the stores, and decrypts it.
12704             sub getAndDecrypt {
12705 0     0     my $o = shift;
12706 0 0 0       my $hashAndKey = shift; die 'wrong type '.ref($hashAndKey).' for $hashAndKey' if defined $hashAndKey && ref $hashAndKey ne 'CDS::HashAndKey';
  0            
12707 0           my $store = shift;
12708              
12709 0           my ($object, $error) = $store->get($hashAndKey->hash, $o);
12710 0 0         return undef, undef, $error if defined $error;
12711 0 0         return undef, 'Not found.', undef if ! $object;
12712 0           return $object->crypt($hashAndKey->key);
12713             }
12714              
12715             # Retrieves an object from one of the stores, and parses it as record.
12716             sub getRecord {
12717 0     0     my $o = shift;
12718 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
12719 0           my $store = shift;
12720              
12721 0           my ($object, $error) = $store->get($hash, $o);
12722 0 0         return undef, undef, undef, $error if defined $error;
12723 0 0         return undef, undef, 'Not found.', undef if ! $object;
12724 0   0       my $record = CDS::Record->fromObject($object) // return undef, undef, 'Not a record.', undef;
12725 0           return $record, $object;
12726             }
12727              
12728             # Retrieves an object from one of the stores, decrypts it, and parses it as record.
12729             sub getAndDecryptRecord {
12730 0     0     my $o = shift;
12731 0 0 0       my $hashAndKey = shift; die 'wrong type '.ref($hashAndKey).' for $hashAndKey' if defined $hashAndKey && ref $hashAndKey ne 'CDS::HashAndKey';
  0            
12732 0           my $store = shift;
12733              
12734 0           my ($object, $error) = $store->get($hashAndKey->hash, $o);
12735 0 0         return undef, undef, undef, $error if defined $error;
12736 0 0         return undef, undef, 'Not found.', undef if ! $object;
12737 0           my $decrypted = $object->crypt($hashAndKey->key);
12738 0   0       my $record = CDS::Record->fromObject($decrypted) // return undef, undef, 'Not a record.', undef;
12739 0           return $record, $object;
12740             }
12741              
12742             # Retrieves an public key object from one of the stores, and parses its public key.
12743             sub getPublicKey {
12744 0     0     my $o = shift;
12745 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
12746 0           my $store = shift;
12747              
12748 0           my ($object, $error) = $store->get($hash, $o);
12749 0 0         return undef, undef, $error if defined $error;
12750 0 0         return undef, 'Not found.', undef if ! $object;
12751 0   0       return CDS::PublicKey->fromObject($object) // return undef, 'Not a public key.', undef;
12752             }
12753              
12754             ### Equality ###
12755              
12756             sub equals {
12757 0     0     my $this = shift;
12758 0           my $that = shift;
12759              
12760 0 0 0       return 1 if ! defined $this && ! defined $that;
12761 0 0 0       return if ! defined $this || ! defined $that;
12762 0           return $this->publicKey->hash->equals($that->publicKey->hash);
12763             }
12764              
12765             ### Open envelopes ###
12766              
12767             sub decryptKeyOnEnvelope {
12768 0     0     my $o = shift;
12769 0 0 0       my $envelope = shift; die 'wrong type '.ref($envelope).' for $envelope' if defined $envelope && ref $envelope ne 'CDS::Record';
  0            
12770              
12771             # Read the AES key
12772 0           my $hashBytes24 = substr($o->{publicKey}->hash->bytes, 0, 24);
12773 0           my $encryptedAesKey = $envelope->child('encrypted for')->child($hashBytes24)->bytesValue;
12774 0 0         $encryptedAesKey = $envelope->child('encrypted for')->child($o->{publicKey}->hash->bytes)->bytesValue if ! length $encryptedAesKey; # todo: remove this
12775 0 0         return if ! length $encryptedAesKey;
12776              
12777             # Decrypt the AES key
12778 0           my $aesKeyBytes = $o->decrypt($encryptedAesKey);
12779 0 0 0       return if ! $aesKeyBytes || length $aesKeyBytes != 32;
12780              
12781 0           return $aesKeyBytes;
12782             }
12783              
12784             # The result of parsing a KEYPAIR token (see Token.pm).
12785             package CDS::KeyPairToken;
12786              
12787             sub new {
12788 0     0     my $class = shift;
12789 0           my $file = shift;
12790 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
12791              
12792 0           return bless {
12793             file => $file,
12794             keyPair => $keyPair,
12795             };
12796             }
12797              
12798 0     0     sub file { shift->{file} }
12799 0     0     sub keyPair { shift->{keyPair} }
12800              
12801             package CDS::LoadActorGroup;
12802              
12803             sub load {
12804 0     0     my $class = shift;
12805 0 0 0       my $builder = shift; die 'wrong type '.ref($builder).' for $builder' if defined $builder && ref $builder ne 'CDS::ActorGroupBuilder';
  0            
12806 0           my $store = shift;
12807 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
12808 0           my $delegate = shift;
12809              
12810 0           my $o = bless {
12811             store => $store,
12812             keyPair => $keyPair,
12813             knownPublicKeys => $builder->knownPublicKeys,
12814             };
12815              
12816 0           my $members = [];
12817 0           for my $member ($builder->members) {
12818 0           my $isActive = $member->status eq 'active';
12819 0           my $isIdle = $member->status eq 'idle';
12820 0 0 0       next if ! $isActive && ! $isIdle;
12821              
12822 0           my ($publicKey, $storeError) = $o->getPublicKey($member->hash);
12823 0 0         return undef, $storeError if defined $storeError;
12824 0 0         next if ! $publicKey;
12825              
12826 0   0       my $accountStore = $delegate->onLoadActorGroupVerifyStore($member->storeUrl) // next;
12827 0           my $actorOnStore = CDS::ActorOnStore->new($publicKey, $accountStore);
12828 0           push @$members, CDS::ActorGroup::Member->new($actorOnStore, $member->storeUrl, $member->revision, $isActive);
12829             }
12830              
12831 0           my $entrustedActors = [];
12832 0           for my $actor ($builder->entrustedActors) {
12833 0           my ($publicKey, $storeError) = $o->getPublicKey($actor->hash);
12834 0 0         return undef, $storeError if defined $storeError;
12835 0 0         next if ! $publicKey;
12836              
12837 0   0       my $accountStore = $delegate->onLoadActorGroupVerifyStore($actor->storeUrl) // next;
12838 0           my $actorOnStore = CDS::ActorOnStore->new($publicKey, $accountStore);
12839 0           push @$entrustedActors, CDS::ActorGroup::EntrustedActor->new($actorOnStore, $actor->storeUrl);
12840             }
12841              
12842 0           return CDS::ActorGroup->new($members, $builder->entrustedActorsRevision, $entrustedActors);
12843             }
12844              
12845             sub getPublicKey {
12846 0     0     my $o = shift;
12847 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
12848              
12849 0           my $knownPublicKey = $o->{knownPublicKeys}->{$hash->bytes};
12850 0 0         return $knownPublicKey if $knownPublicKey;
12851              
12852 0           my ($publicKey, $invalidReason, $storeError) = $o->{keyPair}->getPublicKey($hash, $o->{store});
12853 0 0         return undef, $storeError if defined $storeError;
12854 0 0         return if defined $invalidReason;
12855              
12856 0           $o->{knownPublicKeys}->{$hash->bytes} = $publicKey;
12857 0           return $publicKey;
12858             };
12859              
12860             # A store that prints all accesses to a filehandle (STDERR by default).
12861             package CDS::LogStore;
12862              
12863 1     1   6664 use parent -norequire, 'CDS::Store';
  1         10  
  1         5  
12864              
12865             sub new {
12866 0     0     my $class = shift;
12867 0           my $store = shift;
12868 0   0       my $fileHandle = shift // *STDERR;
12869 0   0       my $prefix = shift // '';
12870              
12871 0           return bless {
12872             id => "Log Store\n".$store->id,
12873             store => $store,
12874             fileHandle => $fileHandle,
12875             prefix => '',
12876             };
12877             }
12878              
12879 0     0     sub id { shift->{id} }
12880 0     0     sub store { shift->{store} }
12881 0     0     sub fileHandle { shift->{fileHandle} }
12882 0     0     sub prefix { shift->{prefix} }
12883              
12884             sub get {
12885 0     0     my $o = shift;
12886 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
12887 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
12888              
12889 0           my $start = CDS::C::performanceStart();
12890 0           my ($object, $error) = $o->{store}->get($hash, $keyPair);
12891 0           my $elapsed = CDS::C::performanceElapsed($start);
12892 0 0         $o->log('get', $hash->shortHex, defined $object ? &formatByteLength($object->byteLength).' bytes' : defined $error ? 'failed: '.$error : 'not found', $elapsed);
    0          
12893 0           return $object, $error;
12894             }
12895              
12896             sub put {
12897 0     0     my $o = shift;
12898 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
12899 0 0 0       my $object = shift; die 'wrong type '.ref($object).' for $object' if defined $object && ref $object ne 'CDS::Object';
  0            
12900 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
12901              
12902 0           my $start = CDS::C::performanceStart();
12903 0           my $error = $o->{store}->put($hash, $object, $keyPair);
12904 0           my $elapsed = CDS::C::performanceElapsed($start);
12905 0 0         $o->log('put', $hash->shortHex . ' ' . &formatByteLength($object->byteLength) . ' bytes', defined $error ? 'failed: '.$error : 'OK', $elapsed);
12906 0           return $error;
12907             }
12908              
12909             sub book {
12910 0     0     my $o = shift;
12911 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
12912 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
12913              
12914 0           my $start = CDS::C::performanceStart();
12915 0           my ($booked, $error) = $o->{store}->book($hash, $keyPair);
12916 0           my $elapsed = CDS::C::performanceElapsed($start);
12917 0 0         $o->log('book', $hash->shortHex, defined $booked ? 'OK' : defined $error ? 'failed: '.$error : 'not found', $elapsed);
    0          
12918 0           return $booked, $error;
12919             }
12920              
12921             sub list {
12922 0     0     my $o = shift;
12923 0 0 0       my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0            
12924 0           my $boxLabel = shift;
12925 0           my $timeout = shift;
12926 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
12927              
12928 0           my $start = CDS::C::performanceStart();
12929 0           my ($hashes, $error) = $o->{store}->list($accountHash, $boxLabel, $timeout, $keyPair);
12930 0           my $elapsed = CDS::C::performanceElapsed($start);
12931 0 0         $o->log('list', $accountHash->shortHex . ' ' . $boxLabel . ($timeout ? ' ' . $timeout . ' s' : ''), defined $hashes ? scalar(@$hashes).' entries' : 'failed: '.$error, $elapsed);
    0          
12932 0           return $hashes, $error;
12933             }
12934              
12935             sub add {
12936 0     0     my $o = shift;
12937 0 0 0       my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0            
12938 0           my $boxLabel = shift;
12939 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
12940 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
12941              
12942 0           my $start = CDS::C::performanceStart();
12943 0           my $error = $o->{store}->add($accountHash, $boxLabel, $hash, $keyPair);
12944 0           my $elapsed = CDS::C::performanceElapsed($start);
12945 0 0         $o->log('add', $accountHash->shortHex . ' ' . $boxLabel . ' ' . $hash->shortHex, defined $error ? 'failed: '.$error : 'OK', $elapsed);
12946 0           return $error;
12947             }
12948              
12949             sub remove {
12950 0     0     my $o = shift;
12951 0 0 0       my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0            
12952 0           my $boxLabel = shift;
12953 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
12954 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
12955              
12956 0           my $start = CDS::C::performanceStart();
12957 0           my $error = $o->{store}->remove($accountHash, $boxLabel, $hash, $keyPair);
12958 0           my $elapsed = CDS::C::performanceElapsed($start);
12959 0 0         $o->log('remove', $accountHash->shortHex . ' ' . $boxLabel . ' ' . $hash->shortHex, defined $error ? 'failed: '.$error : 'OK', $elapsed);
12960 0           return $error;
12961             }
12962              
12963             sub modify {
12964 0     0     my $o = shift;
12965 0           my $modifications = shift;
12966 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
12967              
12968 0           my $start = CDS::C::performanceStart();
12969 0           my $error = $o->{store}->modify($modifications, $keyPair);
12970 0           my $elapsed = CDS::C::performanceElapsed($start);
12971 0 0         $o->log('modify', scalar(keys %{$modifications->objects}) . ' objects ' . scalar @{$modifications->additions} . ' additions ' . scalar @{$modifications->removals} . ' removals', defined $error ? 'failed: '.$error : 'OK', $elapsed);
  0            
  0            
  0            
12972 0           return $error;
12973             }
12974              
12975             sub log {
12976 0     0     my $o = shift;
12977 0           my $cmd = shift;
12978 0           my $input = shift;
12979 0           my $output = shift;
12980 0           my $elapsed = shift;
12981              
12982 0   0       my $fh = $o->{fileHandle} // return;
12983 0           print $fh $o->{prefix}, &left(8, $cmd), &left(40, $input), ' => ', &left(40, $output), &formatDuration($elapsed), ' us', "\n";
12984             }
12985              
12986             sub left {
12987 0     0     my $width = shift;
12988 0           my $text = shift;
12989             # private
12990 0 0         return $text . (' ' x ($width - length $text)) if length $text < $width;
12991 0           return $text;
12992             }
12993              
12994             sub formatByteLength {
12995 0     0     my $byteLength = shift;
12996             # private
12997 0           my $s = ''.$byteLength;
12998 0 0         $s = ' ' x (9 - length $s) . $s if length $s < 9;
12999 0           my $len = length $s;
13000 0           return substr($s, 0, $len - 6).' '.substr($s, $len - 6, 3).' '.substr($s, $len - 3, 3);
13001             }
13002              
13003             sub formatDuration {
13004 0     0     my $elapsed = shift;
13005             # private
13006 0           my $s = ''.$elapsed;
13007 0 0         $s = ' ' x (9 - length $s) . $s if length $s < 9;
13008 0           my $len = length $s;
13009 0           return substr($s, 0, $len - 6).' '.substr($s, $len - 6, 3).' '.substr($s, $len - 3, 3);
13010             }
13011              
13012             # Reads the message box of an actor.
13013             package CDS::MessageBoxReader;
13014              
13015             sub new {
13016 0     0     my $class = shift;
13017 0           my $pool = shift;
13018 0 0 0       my $actorOnStore = shift; die 'wrong type '.ref($actorOnStore).' for $actorOnStore' if defined $actorOnStore && ref $actorOnStore ne 'CDS::ActorOnStore';
  0            
13019 0           my $streamTimeout = shift;
13020              
13021 0   0       return bless {
13022             pool => $pool,
13023             actorOnStore => $actorOnStore,
13024             streamCache => CDS::StreamCache->new($pool, $actorOnStore, $streamTimeout // CDS->MINUTE),
13025             entries => {},
13026             };
13027             }
13028              
13029 0     0     sub pool { shift->{pool} }
13030 0     0     sub actorOnStore { shift->{actorOnStore} }
13031              
13032             sub read {
13033 0     0     my $o = shift;
13034 0   0       my $timeout = shift // 0;
13035              
13036 0           my $store = $o->{actorOnStore}->store;
13037 0           my ($hashes, $listError) = $store->list($o->{actorOnStore}->publicKey->hash, 'messages', $timeout, $o->{pool}->{keyPair});
13038 0 0         return if defined $listError;
13039              
13040 0           for my $hash (@$hashes) {
13041 0           my $entry = $o->{entries}->{$hash->bytes};
13042 0 0         $o->{entries}->{$hash->bytes} = $entry = CDS::MessageBoxReader::Entry->new($hash) if ! $entry;
13043 0 0         next if $entry->{processed};
13044              
13045             # Check the sender store, if necessary
13046 0 0         if ($entry->{waitingForStore}) {
13047 0           my ($dummy, $checkError) = $entry->{waitingForStore}->get(CDS->emptyBytesHash, $o->{pool}->{keyPair});
13048 0 0         next if defined $checkError;
13049             }
13050              
13051             # Get the envelope
13052 0           my ($object, $getError) = $o->{actorOnStore}->store->get($entry->{hash}, $o->{pool}->{keyPair});
13053 0 0         return if defined $getError;
13054              
13055             # Mark the entry as processed
13056 0           $entry->{processed} = 1;
13057              
13058 0 0         if (! defined $object) {
13059 0           $o->invalid($entry, 'Envelope object not found.');
13060 0           next;
13061             }
13062              
13063             # Parse the record
13064 0           my $envelope = CDS::Record->fromObject($object);
13065 0 0         if (! $envelope) {
13066 0           $o->invalid($entry, 'Envelope is not a record.');
13067 0           next;
13068             }
13069              
13070 0 0 0       my $message =
13071             $envelope->contains('head') && $envelope->contains('mac') ?
13072             $o->readStreamMessage($entry, $envelope) :
13073             $o->readNormalMessage($entry, $envelope);
13074 0 0         next if ! $message;
13075              
13076 0           $o->{pool}->{delegate}->onMessageBoxEntry($message);
13077             }
13078              
13079 0           $o->{streamCache}->removeObsolete;
13080 0           return 1;
13081             }
13082              
13083             sub readNormalMessage {
13084 0     0     my $o = shift;
13085 0           my $entry = shift;
13086 0 0 0       my $envelope = shift; die 'wrong type '.ref($envelope).' for $envelope' if defined $envelope && ref $envelope ne 'CDS::Record';
  0            
13087             # private
13088             # Read the embedded content object
13089 0           my $encryptedBytes = $envelope->child('content')->bytesValue;
13090 0 0         return $o->invalid($entry, 'Missing content object.') if ! length $encryptedBytes;
13091              
13092             # Decrypt the key
13093 0           my $aesKey = $o->{pool}->{keyPair}->decryptKeyOnEnvelope($envelope);
13094 0 0         return $o->invalid($entry, 'Not encrypted for us.') if ! $aesKey;
13095              
13096             # Decrypt the content
13097 0           my $contentObject = CDS::Object->fromBytes(CDS::C::aesCrypt($encryptedBytes, $aesKey, CDS->zeroCTR));
13098 0 0         return $o->invalid($entry, 'Invalid content object.') if ! $contentObject;
13099              
13100 0           my $content = CDS::Record->fromObject($contentObject);
13101 0 0         return $o->invalid($entry, 'Content object is not a record.') if ! $content;
13102              
13103             # Verify the sender hash
13104 0           my $senderHash = $content->child('sender')->hashValue;
13105 0 0         return $o->invalid($entry, 'Missing sender hash.') if ! $senderHash;
13106              
13107             # Verify the sender store
13108 0           my $storeRecord = $content->child('store');
13109 0 0         return $o->invalid($entry, 'Missing sender store.') if ! scalar $storeRecord->children;
13110              
13111 0           my $senderStoreUrl = $storeRecord->textValue;
13112 0           my $senderStore = $o->{pool}->{delegate}->onMessageBoxVerifyStore($senderStoreUrl, $entry->{hash}, $envelope, $senderHash);
13113 0 0         return $o->invalid($entry, 'Invalid sender store.') if ! $senderStore;
13114              
13115             # Retrieve the sender's public key
13116 0           my ($senderPublicKey, $invalidReason, $publicKeyStoreError) = $o->getPublicKey($senderHash, $senderStore);
13117 0 0         return if defined $publicKeyStoreError;
13118 0 0         return $o->invalid($entry, 'Failed to retrieve the sender\'s public key: '.$invalidReason) if defined $invalidReason;
13119              
13120             # Verify the signature
13121 0           my $signedHash = CDS::Hash->calculateFor($encryptedBytes);
13122 0 0         if (! CDS->verifyEnvelopeSignature($envelope, $senderPublicKey, $signedHash)) {
13123             # For backwards compatibility with versions before 2020-05-05
13124 0 0         return $o->invalid($entry, 'Invalid signature.') if ! CDS->verifyEnvelopeSignature($envelope, $senderPublicKey, $contentObject->calculateHash);
13125             }
13126              
13127             # The envelope is valid
13128 0           my $sender = CDS::ActorOnStore->new($senderPublicKey, $senderStore);
13129 0           my $source = CDS::Source->new($o->{pool}->{keyPair}, $o->{actorOnStore}, 'messages', $entry->{hash});
13130 0           return CDS::ReceivedMessage->new($o, $entry, $source, $envelope, $senderStoreUrl, $sender, $content);
13131             }
13132              
13133             sub readStreamMessage {
13134 0     0     my $o = shift;
13135 0           my $entry = shift;
13136 0 0 0       my $envelope = shift; die 'wrong type '.ref($envelope).' for $envelope' if defined $envelope && ref $envelope ne 'CDS::Record';
  0            
13137             # private
13138             # Get the head
13139 0           my $head = $envelope->child('head')->hashValue;
13140 0 0         return $o->invalid($entry, 'Invalid head message hash.') if ! $head;
13141              
13142             # Get the head envelope
13143 0           my $streamHead = $o->{streamCache}->readStreamHead($head);
13144 0 0         return if ! $streamHead;
13145 0 0         return $o->invalid($entry, 'Invalid stream head: '.$streamHead->error) if $streamHead->error;
13146              
13147             # Read the embedded content object
13148 0           my $encryptedBytes = $envelope->child('content')->bytesValue;
13149 0 0         return $o->invalid($entry, 'Missing content object.') if ! length $encryptedBytes;
13150              
13151             # Get the CTR
13152 0           my $ctr = $envelope->child('ctr')->bytesValue;
13153 0 0         return $o->invalid($entry, 'Invalid CTR.') if length $ctr != 16;
13154              
13155             # Get the MAC
13156 0           my $mac = $envelope->child('mac')->bytesValue;
13157 0 0         return $o->invalid($entry, 'Invalid MAC.') if ! $mac;
13158              
13159             # Verify the MAC
13160 0           my $signedHash = CDS::Hash->calculateFor($encryptedBytes);
13161 0           my $expectedMac = CDS::C::aesCrypt($signedHash->bytes, $streamHead->aesKey, $ctr);
13162 0 0         return $o->invalid($entry, 'Invalid MAC.') if $mac ne $expectedMac;
13163              
13164             # Decrypt the content
13165 0           my $contentObject = CDS::Object->fromBytes(CDS::C::aesCrypt($encryptedBytes, $streamHead->aesKey, CDS::C::counterPlusInt($ctr, 2)));
13166 0 0         return $o->invalid($entry, 'Invalid content object.') if ! $contentObject;
13167              
13168 0           my $content = CDS::Record->fromObject($contentObject);
13169 0 0         return $o->invalid($entry, 'Content object is not a record.') if ! $content;
13170              
13171             # The envelope is valid
13172 0           my $source = CDS::Source->new($o->{pool}->{keyPair}, $o->{actorOnStore}, 'messages', $entry->{hash});
13173 0           return CDS::ReceivedMessage->new($o, $entry, $source, $envelope, $streamHead->senderStoreUrl, $streamHead->sender, $content, $streamHead);
13174             }
13175              
13176             sub invalid {
13177 0     0     my $o = shift;
13178 0           my $entry = shift;
13179 0           my $reason = shift;
13180             # private
13181 0           my $source = CDS::Source->new($o->{pool}->{keyPair}, $o->{actorOnStore}, 'messages', $entry->{hash});
13182 0           $o->{pool}->{delegate}->onMessageBoxInvalidEntry($source, $reason);
13183             }
13184              
13185             sub getPublicKey {
13186 0     0     my $o = shift;
13187 0 0 0       my $senderHash = shift; die 'wrong type '.ref($senderHash).' for $senderHash' if defined $senderHash && ref $senderHash ne 'CDS::Hash';
  0            
13188 0           my $senderStore = shift;
13189 0           my $senderStoreUrl = shift;
13190             # private
13191             # Use the account key if sender and recipient are the same
13192 0 0         return $o->{actorOnStore}->publicKey if $senderHash->equals($o->{actorOnStore}->publicKey->hash);
13193              
13194             # Reuse a cached public key
13195 0           my $cachedPublicKey = $o->{pool}->{publicKeyCache}->get($senderHash);
13196 0 0         return $cachedPublicKey if $cachedPublicKey;
13197              
13198             # Retrieve the sender's public key from the sender's store
13199 0           my ($publicKey, $invalidReason, $storeError) = $o->{pool}->{keyPair}->getPublicKey($senderHash, $senderStore);
13200 0 0         return undef, undef, $storeError if defined $storeError;
13201 0 0         return undef, $invalidReason if defined $invalidReason;
13202 0           $o->{pool}->{publicKeyCache}->add($publicKey);
13203 0           return $publicKey;
13204             }
13205              
13206             package CDS::MessageBoxReader::Entry;
13207              
13208             sub new {
13209 0     0     my $class = shift;
13210 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
13211              
13212 0           return bless {
13213             hash => $hash,
13214             processed => 0,
13215             };
13216             }
13217              
13218             package CDS::MessageBoxReaderPool;
13219              
13220             sub new {
13221 0     0     my $class = shift;
13222 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
13223 0           my $publicKeyCache = shift;
13224 0           my $delegate = shift;
13225              
13226 0           return bless {
13227             keyPair => $keyPair,
13228             publicKeyCache => $publicKeyCache,
13229             delegate => $delegate,
13230             };
13231             }
13232              
13233 0     0     sub keyPair { shift->{keyPair} }
13234 0     0     sub publicKeyCache { shift->{publicKeyCache} }
13235              
13236             # Delegate
13237             # onMessageBoxVerifyStore($senderStoreUrl, $hash, $envelope, $senderHash)
13238             # onMessageBoxEntry($receivedMessage)
13239             # onMessageBoxStream($receivedMessage)
13240             # onMessageBoxInvalidEntry($source, $reason)
13241              
13242             package CDS::MessageChannel;
13243              
13244             sub new {
13245 0     0     my $class = shift;
13246 0           my $actor = shift;
13247 0           my $label = shift;
13248 0           my $validity = shift;
13249              
13250 0           my $o = bless {
13251             actor => $actor,
13252             label => $label,
13253             validity => $validity,
13254             };
13255              
13256 0           $o->{unsaved} = CDS::Unsaved->new($actor->sentList->unsaved);
13257 0           $o->{transfers} = [];
13258 0           $o->{recipients} = [];
13259 0           $o->{entrustedKeys} = [];
13260 0           $o->{obsoleteHashes} = {};
13261 0           $o->{currentSubmissionId} = 0;
13262 0           return $o;
13263             }
13264              
13265 0     0     sub actor { shift->{actor} }
13266 0     0     sub label { shift->{label} }
13267 0     0     sub validity { shift->{validity} }
13268 0     0     sub unsaved { shift->{unsaved} }
13269             sub item {
13270 0     0     my $o = shift;
13271 0           $o->{actor}->sentList->getOrCreate($o->{label}) }
13272             sub recipients {
13273 0     0     my $o = shift;
13274 0           @{$o->{recipients}} }
  0            
13275             sub entrustedKeys {
13276 0     0     my $o = shift;
13277 0           @{$o->{entrustedKeys}} }
  0            
13278              
13279             sub addObject {
13280 0     0     my $o = shift;
13281 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
13282 0 0 0       my $object = shift; die 'wrong type '.ref($object).' for $object' if defined $object && ref $object ne 'CDS::Object';
  0            
13283              
13284 0           $o->{unsaved}->state->addObject($hash, $object);
13285             }
13286              
13287             sub addTransfer {
13288 0     0     my $o = shift;
13289 0           my $hashes = shift;
13290 0           my $sourceStore = shift;
13291 0           my $context = shift;
13292              
13293 0 0         return if ! scalar @$hashes;
13294 0           push @{$o->{transfers}}, {hashes => $hashes, sourceStore => $sourceStore, context => $context};
  0            
13295             }
13296              
13297             sub setRecipientActorGroup {
13298 0     0     my $o = shift;
13299 0 0 0       my $actorGroup = shift; die 'wrong type '.ref($actorGroup).' for $actorGroup' if defined $actorGroup && ref $actorGroup ne 'CDS::ActorGroup';
  0            
13300              
13301 0           $o->{recipients} = [map { $_->actorOnStore } $actorGroup->members];
  0            
13302 0           $o->{entrustedKeys} = [map { $_->actorOnStore->publicKey } $actorGroup->entrustedActors];
  0            
13303             }
13304              
13305             sub setRecipients {
13306 0     0     my $o = shift;
13307 0           my $recipients = shift;
13308 0           my $entrustedKeys = shift;
13309              
13310 0           $o->{recipients} = $recipients;
13311 0           $o->{entrustedKeys} = $entrustedKeys;
13312             }
13313              
13314             sub submit {
13315 0     0     my $o = shift;
13316 0           my $message = shift;
13317 0           my $done = shift;
13318              
13319             # Check if the sent list has been loaded
13320 0 0         return if ! $o->{actor}->sentListReady;
13321              
13322             # Transfer
13323 0           my $transfers = $o->{transfers};
13324 0           $o->{transfers} = [];
13325 0           for my $transfer (@$transfers) {
13326 0           my ($missingObject, $store, $error) = $o->{actor}->keyPair->transfer($transfer->{hashes}, $transfer->{sourceStore}, $o->{actor}->messagingPrivateRoot->unsaved);
13327 0 0         return if defined $error;
13328              
13329 0 0         if ($missingObject) {
13330 0           $missingObject->{context} = $transfer->{context};
13331 0           return undef, $missingObject;
13332             }
13333             }
13334              
13335             # Send the message
13336 0           return CDS::MessageChannel::Submission->new($o, $message, $done);
13337             }
13338              
13339             sub clear {
13340 0     0     my $o = shift;
13341              
13342 0           $o->item->clear(CDS->now + $o->{validity});
13343             }
13344              
13345             package CDS::MessageChannel::Submission;
13346              
13347             sub new {
13348 0     0     my $class = shift;
13349 0           my $channel = shift;
13350 0           my $message = shift;
13351 0           my $done = shift;
13352              
13353 0           $channel->{currentSubmissionId} += 1;
13354              
13355             my $o = bless {
13356             channel => $channel,
13357             message => $message,
13358             done => $done,
13359             submissionId => $channel->{currentSubmissionId},
13360 0           recipients => [$channel->recipients],
13361             entrustedKeys => [$channel->entrustedKeys],
13362             expires => CDS->now + $channel->validity,
13363             };
13364              
13365             # Add the current envelope hash to the obsolete hashes
13366 0           my $item = $channel->item;
13367 0 0         $channel->{obsoleteHashes}->{$item->envelopeHash->bytes} = $item->envelopeHash if $item->envelopeHash;
13368 0           $o->{obsoleteHashesSnapshot} = [values %{$channel->{obsoleteHashes}}];
  0            
13369              
13370             # Create an envelope
13371 0           my $publicKeys = [];
13372 0           push @$publicKeys, $channel->{actor}->keyPair->publicKey;
13373 0           push @$publicKeys, map { $_->publicKey } @{$o->{recipients}};
  0            
  0            
13374 0           push @$publicKeys, @{$o->{entrustedKeys}};
  0            
13375 0           $o->{envelopeObject} = $channel->{actor}->keyPair->createMessageEnvelope($channel->{actor}->messagingStoreUrl, $message, $publicKeys, $o->{expires})->toObject;
13376 0           $o->{envelopeHash} = $o->{envelopeObject}->calculateHash;
13377              
13378             # Set the new item and wait until it gets saved
13379 0           $channel->{unsaved}->startSaving;
13380 0           $channel->{unsaved}->savingState->addDataSavedHandler($o);
13381 0           $channel->{actor}->sentList->unsaved->state->merge($channel->{unsaved}->savingState);
13382 0           $item->set($o->{expires}, $o->{envelopeHash}, $message);
13383 0           $channel->{unsaved}->savingDone;
13384              
13385 0           return $o;
13386             }
13387              
13388 0     0     sub channel { shift->{channel} }
13389 0     0     sub message { shift->{message} }
13390             sub recipients {
13391 0     0     my $o = shift;
13392 0           @{$o->{recipients}} }
  0            
13393             sub entrustedKeys {
13394 0     0     my $o = shift;
13395 0           @{$o->{entrustedKeys}} }
  0            
13396 0     0     sub expires { shift->{expires} }
13397 0     0     sub envelopeObject { shift->{envelopeObject} }
13398 0     0     sub envelopeHash { shift->{envelopeHash} }
13399              
13400             sub onDataSaved {
13401 0     0     my $o = shift;
13402              
13403             # If we are not the head any more, give up
13404 0 0         return $o->{done}->onMessageChannelSubmissionCancelled if $o->{submissionId} != $o->{channel}->{currentSubmissionId};
13405 0           $o->{channel}->{obsoleteHashes}->{$o->{envelopeHash}->bytes} = $o->{envelopeHash};
13406              
13407             # Process all recipients
13408 0           my $succeeded = 0;
13409 0           my $failed = 0;
13410 0           for my $recipient (@{$o->{recipients}}) {
  0            
13411 0           my $modifications = CDS::StoreModifications->new;
13412              
13413             # Prepare the list of removals
13414 0           my $removals = [];
13415 0           for my $hash (@{$o->{obsoleteHashesSnapshot}}) {
  0            
13416 0           $modifications->remove($recipient->publicKey->hash, 'messages', $hash);
13417             }
13418              
13419             # Add the message entry
13420 0           $modifications->add($recipient->publicKey->hash, 'messages', $o->{envelopeHash}, $o->{envelopeObject});
13421 0           my $error = $recipient->store->modify($modifications, $o->{channel}->{actor}->keyPair);
13422              
13423 0 0         if (defined $error) {
13424 0           $failed += 1;
13425 0           $o->{done}->onMessageChannelSubmissionRecipientFailed($recipient, $error);
13426             } else {
13427 0           $succeeded += 1;
13428 0           $o->{done}->onMessageChannelSubmissionRecipientDone($recipient);
13429             }
13430             }
13431              
13432 0 0 0       if ($failed == 0 || scalar keys %{$o->{obsoleteHashes}} > 64) {
  0            
13433 0           for my $hash (@{$o->{obsoleteHashesSnapshot}}) {
  0            
13434 0           delete $o->{channel}->{obsoleteHashes}->{$hash->bytes};
13435             }
13436             }
13437              
13438 0           $o->{done}->onMessageChannelSubmissionDone($succeeded, $failed);
13439             }
13440              
13441             package CDS::MissingObject;
13442              
13443             sub new {
13444 0     0     my $class = shift;
13445 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
13446 0           my $store = shift;
13447              
13448 0           return bless {hash => $hash, store => $store, path => [], context => undef};
13449             }
13450              
13451 0     0     sub hash { shift->{hash} }
13452 0     0     sub store { shift->{store} }
13453             sub path {
13454 0     0     my $o = shift;
13455 0           @{$o->{path}} }
  0            
13456 0     0     sub context { shift->{context} }
13457              
13458             package CDS::NewAnnounce;
13459              
13460             sub new {
13461 0     0     my $class = shift;
13462 0           my $messagingStore = shift;
13463              
13464 0           my $o = bless {
13465             messagingStore => $messagingStore,
13466             unsaved => CDS::Unsaved->new($messagingStore->store),
13467             transfers => [],
13468             card => CDS::Record->new,
13469             };
13470              
13471 0           my $publicKey = $messagingStore->actor->keyPair->publicKey;
13472 0           $o->{card}->add('public key')->addHash($publicKey->hash);
13473 0           $o->addObject($publicKey->hash, $publicKey->object);
13474 0           return $o;
13475             }
13476              
13477 0     0     sub messagingStore { shift->{messagingStore} }
13478 0     0     sub card { shift->{card} }
13479              
13480             sub addObject {
13481 0     0     my $o = shift;
13482 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
13483 0 0 0       my $object = shift; die 'wrong type '.ref($object).' for $object' if defined $object && ref $object ne 'CDS::Object';
  0            
13484              
13485 0           $o->{unsaved}->state->addObject($hash, $object);
13486             }
13487              
13488             sub addTransfer {
13489 0     0     my $o = shift;
13490 0           my $hashes = shift;
13491 0           my $sourceStore = shift;
13492 0           my $context = shift;
13493              
13494 0 0         return if ! scalar @$hashes;
13495 0           push @{$o->{transfers}}, {hashes => $hashes, sourceStore => $sourceStore, context => $context};
  0            
13496             }
13497              
13498             sub addActorGroup {
13499 0     0     my $o = shift;
13500 0           my $actorGroupBuilder = shift;
13501              
13502 0           $actorGroupBuilder->addToRecord($o->{card}, 0);
13503             }
13504              
13505             sub submit {
13506 0     0     my $o = shift;
13507              
13508 0           my $keyPair = $o->{messagingStore}->actor->keyPair;
13509              
13510             # Create the public card
13511 0           my $cardObject = $o->{card}->toObject;
13512 0           my $cardHash = $cardObject->calculateHash;
13513 0           $o->addObject($cardHash, $cardObject);
13514              
13515             # Prepare the public envelope
13516 0           my $me = $keyPair->publicKey->hash;
13517 0           my $envelopeObject = $keyPair->createPublicEnvelope($cardHash)->toObject;
13518 0           my $envelopeHash = $envelopeObject->calculateHash;
13519 0           $o->addTransfer([$cardHash], $o->{unsaved}, 'Announcing');
13520              
13521             # Transfer all trees
13522 0           for my $transfer (@{$o->{transfers}}) {
  0            
13523 0           my ($missingObject, $store, $error) = $keyPair->transfer($transfer->{hashes}, $transfer->{sourceStore}, $o->{messagingStore}->store);
13524 0 0         return if defined $error;
13525              
13526 0 0         if ($missingObject) {
13527 0           $missingObject->{context} = $transfer->{context};
13528 0           return undef, $missingObject;
13529             }
13530             }
13531              
13532             # Prepare a modification
13533 0           my $modifications = CDS::StoreModifications->new;
13534 0           $modifications->add($me, 'public', $envelopeHash, $envelopeObject);
13535              
13536             # List the current cards to remove them
13537             # Ignore errors, in the worst case, we are going to have multiple entries in the public box
13538 0           my ($hashes, $error) = $o->{messagingStore}->store->list($me, 'public', 0, $keyPair);
13539 0 0         if ($hashes) {
13540 0           for my $hash (@$hashes) {
13541 0           $modifications->remove($me, 'public', $hash);
13542             }
13543             }
13544              
13545             # Modify the public box
13546 0           my $modifyError = $o->{messagingStore}->store->modify($modifications, $keyPair);
13547 0 0         return if defined $modifyError;
13548 0           return $envelopeHash, $cardHash;
13549             }
13550              
13551             package CDS::NewMessagingStore;
13552              
13553             sub new {
13554 0     0     my $class = shift;
13555 0           my $actor = shift;
13556 0           my $store = shift;
13557              
13558 0           return bless {
13559             actor => $actor,
13560             store => $store,
13561             };
13562             }
13563              
13564 0     0     sub actor { shift->{actor} }
13565 0     0     sub store { shift->{store} }
13566              
13567             # A Condensation object.
13568             # A valid object starts with a 4-byte length (big-endian), followed by 32 * length bytes of hashes, followed by 0 or more bytes of data.
13569             package CDS::Object;
13570              
13571 0     0     sub emptyHeader { "\0\0\0\0" }
13572              
13573             sub create {
13574 0     0     my $class = shift;
13575 0           my $header = shift;
13576 0           my $data = shift;
13577              
13578 0 0         return if length $header < 4;
13579 0           my $hashesCount = unpack('L>', substr($header, 0, 4));
13580 0 0         return if length $header != 4 + $hashesCount * 32;
13581 0           return bless {
13582             bytes => $header.$data,
13583             hashesCount => $hashesCount,
13584             header => $header,
13585             data => $data
13586             };
13587             }
13588              
13589             sub fromBytes {
13590 0     0     my $class = shift;
13591 0   0       my $bytes = shift // return;
13592              
13593 0 0         return if length $bytes < 4;
13594              
13595 0           my $hashesCount = unpack 'L>', substr($bytes, 0, 4);
13596 0           my $dataStart = $hashesCount * 32 + 4;
13597 0 0         return if $dataStart > length $bytes;
13598              
13599 0           return bless {
13600             bytes => $bytes,
13601             hashesCount => $hashesCount,
13602             header => substr($bytes, 0, $dataStart),
13603             data => substr($bytes, $dataStart)
13604             };
13605             }
13606              
13607             sub fromFile {
13608 0     0     my $class = shift;
13609 0           my $file = shift;
13610              
13611 0           return $class->fromBytes(CDS->readBytesFromFile($file));
13612             }
13613              
13614 0     0     sub bytes { shift->{bytes} }
13615 0     0     sub header { shift->{header} }
13616 0     0     sub data { shift->{data} }
13617 0     0     sub hashesCount { shift->{hashesCount} }
13618             sub byteLength {
13619 0     0     my $o = shift;
13620 0           length($o->{header}) + length($o->{data}) }
13621              
13622             sub calculateHash {
13623 0     0     my $o = shift;
13624              
13625 0           return CDS::Hash->calculateFor($o->{bytes});
13626             }
13627              
13628             sub hashes {
13629 0     0     my $o = shift;
13630              
13631 0           return map { CDS::Hash->fromBytes(substr($o->{header}, $_ * 32 + 4, 32)) } 0 .. $o->{hashesCount} - 1;
  0            
13632             }
13633              
13634             sub hashAtIndex {
13635 0     0     my $o = shift;
13636 0   0       my $index = shift // return;
13637              
13638 0 0 0       return if $index < 0 || $index >= $o->{hashesCount};
13639 0           return CDS::Hash->fromBytes(substr($o->{header}, $index * 32 + 4, 32));
13640             }
13641              
13642             sub crypt {
13643 0     0     my $o = shift;
13644 0           my $key = shift;
13645              
13646 0           return CDS::Object->create($o->{header}, CDS::C::aesCrypt($o->{data}, $key, CDS->zeroCTR));
13647             }
13648              
13649             sub writeToFile {
13650 0     0     my $o = shift;
13651 0           my $file = shift;
13652              
13653 0           return CDS->writeBytesToFile($file, $o->{bytes});
13654             }
13655              
13656             # A store using a cache store to deliver frequently accessed objects faster, and a backend store.
13657             package CDS::ObjectCache;
13658              
13659 1     1   5693 use parent -norequire, 'CDS::Store';
  1         2  
  1         4  
13660              
13661             sub new {
13662 0     0     my $class = shift;
13663 0           my $backend = shift;
13664 0           my $cache = shift;
13665              
13666 0           return bless {
13667             id => "Object Cache\n".$backend->id."\n".$cache->id,
13668             backend => $backend,
13669             cache => $cache,
13670             };
13671             }
13672              
13673 0     0     sub id { shift->{id} }
13674 0     0     sub backend { shift->{backend} }
13675 0     0     sub cache { shift->{cache} }
13676              
13677             sub get {
13678 0     0     my $o = shift;
13679 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
13680 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
13681              
13682 0           my $objectFromCache = $o->{cache}->get($hash);
13683 0 0         return $objectFromCache if $objectFromCache;
13684              
13685 0           my ($object, $error) = $o->{backend}->get($hash, $keyPair);
13686 0 0         return undef, $error if ! defined $object;
13687 0           $o->{cache}->put($hash, $object, undef);
13688 0           return $object;
13689             }
13690              
13691             sub put {
13692 0     0     my $o = shift;
13693              
13694             # The important thing is that the backend succeeds. The cache is a nice-to-have.
13695 0           $o->{cache}->put(@_);
13696 0           return $o->{backend}->put(@_);
13697             }
13698              
13699             sub book {
13700 0     0     my $o = shift;
13701              
13702             # The important thing is that the backend succeeds. The cache is a nice-to-have.
13703 0           $o->{cache}->book(@_);
13704 0           return $o->{backend}->book(@_);
13705             }
13706              
13707             sub list {
13708 0     0     my $o = shift;
13709              
13710             # Just pass this through to the backend.
13711 0           return $o->{backend}->list(@_);
13712             }
13713              
13714             sub add {
13715 0     0     my $o = shift;
13716              
13717             # Just pass this through to the backend.
13718 0           return $o->{backend}->add(@_);
13719             }
13720              
13721             sub remove {
13722 0     0     my $o = shift;
13723              
13724             # Just pass this through to the backend.
13725 0           return $o->{backend}->remove(@_);
13726             }
13727              
13728             sub modify {
13729 0     0     my $o = shift;
13730              
13731             # Just pass this through to the backend.
13732 0           return $o->{backend}->modify(@_);
13733             }
13734              
13735             # The result of parsing an OBJECTFILE token (see Token.pm).
13736             package CDS::ObjectFileToken;
13737              
13738             sub new {
13739 0     0     my $class = shift;
13740 0           my $file = shift;
13741 0 0 0       my $object = shift; die 'wrong type '.ref($object).' for $object' if defined $object && ref $object ne 'CDS::Object';
  0            
13742              
13743 0           return bless {
13744             file => $file,
13745             object => $object,
13746             };
13747             }
13748              
13749 0     0     sub file { shift->{file} }
13750 0     0     sub object { shift->{object} }
13751              
13752             # The result of parsing an OBJECT token.
13753             package CDS::ObjectToken;
13754              
13755             sub new {
13756 0     0     my $class = shift;
13757 0           my $cliStore = shift;
13758 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
13759              
13760 0           return bless {
13761             cliStore => $cliStore,
13762             hash => $hash,
13763             };
13764             }
13765              
13766 0     0     sub cliStore { shift->{cliStore} }
13767 0     0     sub hash { shift->{hash} }
13768             sub url {
13769 0     0     my $o = shift;
13770 0           $o->{cliStore}->url.'/objects/'.$o->{hash}->hex }
13771              
13772             package CDS::Parser;
13773              
13774             sub new {
13775 0     0     my $class = shift;
13776 0           my $actor = shift;
13777 0           my $command = shift;
13778              
13779 0           my $start = CDS::Parser::Node->new(0);
13780 0           return bless {
13781             actor => $actor,
13782             ui => $actor->ui,
13783             start => $start,
13784             states => [CDS::Parser::State->new($start)],
13785             command => $command,
13786             };
13787             }
13788              
13789 0     0     sub actor { shift->{actor} }
13790 0     0     sub start { shift->{start} }
13791              
13792             sub execute {
13793 0     0     my $o = shift;
13794              
13795 0           my $processed = [$o->{command}];
13796 0           for my $arg (@_) {
13797 0 0         return $o->howToContinue($processed) if $arg eq '?';
13798 0 0         return $o->explain if $arg eq '??';
13799 0           my $token = CDS::Parser::Token->new($o->{actor}, $arg);
13800 0           $o->advance($token);
13801 0 0         return $o->invalid($processed, $token) if ! scalar @{$o->{states}};
  0            
13802 0           push @$processed, $arg;
13803             }
13804              
13805 0           my @results = grep { $_->runHandler } @{$o->{states}};
  0            
  0            
13806 0 0         return $o->howToContinue($processed) if ! scalar @results;
13807              
13808 0           my $maxWeight = 0;
13809 0           for my $result (@results) {
13810 0 0         $maxWeight = $result->cumulativeWeight if $maxWeight < $result->cumulativeWeight;
13811             }
13812              
13813 0           @results = grep { $_->cumulativeWeight == $maxWeight } @results;
  0            
13814 0 0         return $o->ambiguous if scalar @results > 1;
13815              
13816 0           my $result = shift @results;
13817 0           my $handler = $result->runHandler;
13818 0           my $instance = &{$handler->{constructor}}(undef, $o->{actor});
  0            
13819 0           &{$handler->{function}}($instance, $result);
  0            
13820             }
13821              
13822             sub advance {
13823 0     0     my $o = shift;
13824 0           my $token = shift;
13825              
13826 0           $o->{previousStates} = $o->{states};
13827 0           $o->{states} = [];
13828 0           for my $state (@{$o->{previousStates}}) {
  0            
13829 0           push @{$o->{states}}, $state->advance($token);
  0            
13830             }
13831             }
13832              
13833             sub showCompletions {
13834 0     0     my $o = shift;
13835 0           my $cmd = shift;
13836              
13837             # Parse the command line
13838 0           my $state = '';
13839 0           my $arg = '';
13840 0           my @args;
13841 0           for my $c (split //, $cmd) {
13842 0 0         if ($state eq '') {
    0          
    0          
    0          
    0          
13843 0 0         if ($c eq ' ') {
    0          
    0          
    0          
13844 0 0         push @args, $arg if length $arg;
13845 0           $arg = '';
13846             } elsif ($c eq '\'') {
13847 0 0         push @args, $arg if length $arg;
13848 0           $arg = '';
13849 0           $state = '\'';
13850             } elsif ($c eq '"') {
13851 0 0         push @args, $arg if length $arg;
13852 0           $arg = '';
13853 0           $state = '"';
13854             } elsif ($c eq '\\') {
13855 0           $state = '\\';
13856             } else {
13857 0           $arg .= $c;
13858             }
13859             } elsif ($state eq '\\') {
13860 0           $arg .= $c;
13861 0           $state = '';
13862             } elsif ($state eq '\'') {
13863 0 0         if ($c eq '\'') {
13864 0 0         push @args, $arg if length $arg;
13865 0           $arg = '';
13866 0           $state = '';
13867             } else {
13868 0           $arg .= $c;
13869             }
13870             } elsif ($state eq '"') {
13871 0 0         if ($c eq '"') {
    0          
13872 0 0         push @args, $arg if length $arg;
13873 0           $arg = '';
13874 0           $state = '';
13875             } elsif ($c eq '\\') {
13876 0           $state = '"\\';
13877             } else {
13878 0           $arg .= $c;
13879             }
13880             } elsif ($state eq '\\"') {
13881 0           $arg .= $c;
13882 0           $state = '"';
13883             }
13884             }
13885              
13886             # Use the last token to complete
13887 0           my $lastToken = CDS::Parser::Token->new($o->{actor}, $arg);
13888              
13889             # Look for possible states
13890 0           shift @args;
13891 0           for my $arg (@args) {
13892 0 0         return if $arg eq '?';
13893 0           $o->advance(CDS::Parser::Token->new($o->{actor}, $arg));
13894             }
13895              
13896             # Complete the last token
13897 0           my %possibilities;
13898 0           for my $state (@{$o->{states}}) {
  0            
13899 0           for my $possibility ($state->complete($lastToken)) {
13900 0           $possibilities{$possibility} = 1;
13901             }
13902             }
13903              
13904             # Print all possibilities
13905 0           for my $possibility (keys %possibilities) {
13906 0           print $possibility, "\n";
13907             }
13908             }
13909              
13910             sub ambiguous {
13911 0     0     my $o = shift;
13912              
13913 0           $o->{ui}->space;
13914 0           $o->{ui}->pRed('Your query is ambiguous. This is an error in the command grammar.');
13915 0           $o->explain;
13916             }
13917              
13918             sub explain {
13919 0     0     my $o = shift;
13920              
13921 0 0         for my $interpretation (sort { $b->cumulativeWeight <=> $a->cumulativeWeight || $b->isExecutable <=> $a->isExecutable } @{$o->{states}}) {
  0            
  0            
13922 0           $o->{ui}->space;
13923 0 0         $o->{ui}->title('Interpretation with weight ', $interpretation->cumulativeWeight, $interpretation->isExecutable ? $o->{ui}->green(' (executable)') : $o->{ui}->orange(' (incomplete)'));
13924 0           $o->showTuples($interpretation->path);
13925             }
13926              
13927 0           $o->{ui}->space;
13928             }
13929              
13930             sub showTuples {
13931 0     0     my $o = shift;
13932              
13933 0           for my $state (@_) {
13934 0           my $label = $state->label;
13935 0           my $value = $state->value;
13936              
13937 0           my $valueRef = ref $value;
13938 0 0 0       my $valueText =
    0          
    0          
    0          
13939             $valueRef eq '' ? $value // '' :
13940             $valueRef eq 'CDS::Hash' ? $value->hex :
13941             $valueRef eq 'CDS::ErrorHandlingStore' ? $value->url :
13942             $valueRef eq 'CDS::AccountToken' ? $value->actorHash->hex . ' on ' . $value->cliStore->url :
13943             $valueRef;
13944 0 0         $o->{ui}->line($o->{ui}->left(12, $label), $state->collectHandler ? $valueText : $o->{ui}->gray($valueText));
13945             }
13946             }
13947              
13948             sub cmd {
13949 0     0     my $o = shift;
13950 0           my $processed = shift;
13951              
13952 0           my $cmd = join(' ', map { $_ =~ s/(\\|'|")/\\$1/g ; $_ } @$processed);
  0            
  0            
13953 0 0         $cmd = '…'.substr($cmd, length($cmd) - 20, 20) if length $cmd > 30;
13954 0           return $cmd;
13955             }
13956              
13957             sub howToContinue {
13958 0     0     my $o = shift;
13959 0           my $processed = shift;
13960              
13961 0           my $cmd = $o->cmd($processed);
13962             #$o->displayWarnings($o->{states});
13963 0           $o->{ui}->space;
13964 0           for my $possibility (CDS::Parser::Continuations->collect($o->{states})) {
13965 0           $o->{ui}->line($o->{ui}->gray($cmd), $possibility);
13966             }
13967 0           $o->{ui}->space;
13968             }
13969              
13970             sub invalid {
13971 0     0     my $o = shift;
13972 0           my $processed = shift;
13973 0           my $invalid = shift;
13974              
13975 0           my $cmd = $o->cmd($processed);
13976 0           $o->displayWarnings($o->{previousStates});
13977 0           $o->{ui}->space;
13978              
13979 0           $o->{ui}->line($o->{ui}->gray($cmd), ' ', $o->{ui}->red($invalid->{text}));
13980 0 0         if (scalar @{$invalid->{warnings}}) {
  0            
13981 0           for my $warning (@{$invalid->{warnings}}) {
  0            
13982 0           $o->{ui}->warning($warning);
13983             }
13984             }
13985              
13986 0           $o->{ui}->space;
13987 0           $o->{ui}->title('Possible continuations');
13988 0           for my $possibility (CDS::Parser::Continuations->collect($o->{previousStates})) {
13989 0           $o->{ui}->line($o->{ui}->gray($cmd), $possibility);
13990             }
13991 0           $o->{ui}->space;
13992             }
13993              
13994             sub displayWarnings {
13995 0     0     my $o = shift;
13996 0           my $states = shift;
13997              
13998 0           for my $state (@$states) {
13999 0           my $current = $state;
14000 0           while ($current) {
14001 0           for my $warning (@{$current->{warnings}}) {
  0            
14002 0           $o->{ui}->warning($warning);
14003             }
14004 0           $current = $current->{previous};
14005             }
14006             }
14007             }
14008              
14009             # An arrow points from one node to another. The arrow is taken in State::advance if the next argument matches to the label.
14010             package CDS::Parser::Arrow;
14011              
14012             sub new {
14013 0     0     my $class = shift;
14014 0           my $node = shift;
14015 0           my $official = shift;
14016 0           my $weight = shift;
14017 0           my $label = shift;
14018 0           my $handler = shift;
14019              
14020 0           return bless {
14021             node => $node, # target node
14022             official => $official, # whether to show this arrow with '?'
14023             weight => $weight, # weight
14024             label => $label, # label
14025             handler => $handler, # handler to invoke if we take this arrow
14026             };
14027             }
14028              
14029             package CDS::Parser::Continuations;
14030              
14031             sub collect {
14032 0     0     my $class = shift;
14033 0           my $states = shift;
14034              
14035 0           my $o = bless {possibilities => {}};
14036              
14037 0           my $visitedNodes = {};
14038 0           for my $state (@$states) {
14039 0           $o->visit($visitedNodes, $state->node, '');
14040             }
14041              
14042 0           for my $possibility (keys %{$o->{possibilities}}) {
  0            
14043 0 0         delete $o->{possibilities}->{$possibility} if exists $o->{possibilities}->{$possibility.' …'};
14044             }
14045              
14046 0           return sort keys %{$o->{possibilities}};
  0            
14047             }
14048              
14049             sub visit {
14050 0     0     my $o = shift;
14051 0           my $visitedNodes = shift;
14052 0           my $node = shift;
14053 0           my $text = shift;
14054              
14055 0           $visitedNodes->{$node} = 1;
14056              
14057 0           my $arrows = [];
14058 0           $node->collectArrows($arrows);
14059              
14060 0           for my $arrow (@$arrows) {
14061 0 0         next if ! $arrow->{official};
14062              
14063 0           my $text = $text.' '.$arrow->{label};
14064 0 0         $o->{possibilities}->{$text} = 1 if $arrow->{node}->hasHandler;
14065 0 0 0       if ($arrow->{node}->endProposals || exists $visitedNodes->{$arrow->{node}}) {
14066 0 0         $o->{possibilities}->{$text . ($o->canContinue($arrow->{node}) ? ' …' : '')} = 1;
14067 0           next;
14068             }
14069              
14070 0           $o->visit($visitedNodes, $arrow->{node}, $text);
14071             }
14072              
14073 0           delete $visitedNodes->{$node};
14074             }
14075              
14076             sub canContinue {
14077 0     0     my $o = shift;
14078 0           my $node = shift;
14079              
14080 0           my $arrows = [];
14081 0           $node->collectArrows($arrows);
14082              
14083 0           for my $arrow (@$arrows) {
14084 0 0         next if ! $arrow->{official};
14085 0           return 1;
14086             }
14087              
14088 0           return;
14089             }
14090              
14091             # Nodes and arrows define the graph on which the parse state can move.
14092             package CDS::Parser::Node;
14093              
14094             sub new {
14095 0     0     my $class = shift;
14096 0           my $endProposals = shift;
14097 0           my $handler = shift;
14098              
14099 0           return bless {
14100             arrows => [], # outgoing arrows
14101             defaults => [], # default nodes, at which the current state could be as well
14102             endProposals => $endProposals, # if set, the proposal search algorithm stops at this node
14103             handler => $handler, # handler to be executed if parsing ends here
14104             };
14105             }
14106              
14107 0     0     sub endProposals { shift->{endProposals} }
14108              
14109             # Adds an arrow.
14110             sub addArrow {
14111 0     0     my $o = shift;
14112 0           my $to = shift;
14113 0           my $official = shift;
14114 0           my $weight = shift;
14115 0           my $label = shift;
14116 0           my $handler = shift;
14117              
14118 0           push @{$o->{arrows}}, CDS::Parser::Arrow->new($to, $official, $weight, $label, $handler);
  0            
14119             }
14120              
14121             # Adds a default node.
14122             sub addDefault {
14123 0     0     my $o = shift;
14124 0           my $node = shift;
14125              
14126 0           push @{$o->{defaults}}, $node;
  0            
14127             }
14128              
14129             sub collectArrows {
14130 0     0     my $o = shift;
14131 0           my $arrows = shift;
14132              
14133 0           push @$arrows, @{$o->{arrows}};
  0            
14134 0           for my $default (@{$o->{defaults}}) { $default->collectArrows($arrows); }
  0            
  0            
14135             }
14136              
14137             sub hasHandler {
14138 0     0     my $o = shift;
14139              
14140 0 0         return 1 if $o->{handler};
14141 0 0         for my $default (@{$o->{defaults}}) { return 1 if $default->hasHandler; }
  0            
  0            
14142 0           return;
14143             }
14144              
14145             sub getHandler {
14146 0     0     my $o = shift;
14147              
14148 0 0         return $o->{handler} if $o->{handler};
14149 0           for my $default (@{$o->{defaults}}) {
  0            
14150 0   0       my $handler = $default->getHandler // next;
14151 0           return $handler;
14152             }
14153 0           return;
14154             }
14155              
14156             # A parser state denotes a possible current state (after having parsed a certain number of arguments).
14157             # A parser keeps track of multiple states. When advancing, a state may disappear (if no possibility exists), or fan out (if multiple possibilities exist).
14158             # A state is immutable.
14159             package CDS::Parser::State;
14160              
14161             sub new {
14162 0     0     my $class = shift;
14163 0           my $node = shift;
14164 0           my $previous = shift;
14165 0           my $arrow = shift;
14166 0           my $value = shift;
14167 0           my $warnings = shift;
14168              
14169             return bless {
14170             node => $node, # current node
14171             previous => $previous, # previous state
14172             arrow => $arrow, # the arrow we took to get here
14173             value => $value, # the value we collected with the last arrow
14174             warnings => $warnings, # the warnings we collected with the last arrow
14175 0 0         cumulativeWeight => ($previous ? $previous->cumulativeWeight : 0) + ($arrow ? $arrow->{weight} : 0), # the weight we collected until here
    0          
14176             };
14177             }
14178              
14179 0     0     sub node { shift->{node} }
14180             sub runHandler {
14181 0     0     my $o = shift;
14182 0           $o->{node}->getHandler }
14183             sub isExecutable {
14184 0     0     my $o = shift;
14185 0 0         $o->{node}->getHandler ? 1 : 0 }
14186             sub collectHandler {
14187 0     0     my $o = shift;
14188 0 0         $o->{arrow} ? $o->{arrow}->{handler} : undef }
14189             sub label {
14190 0     0     my $o = shift;
14191 0 0         $o->{arrow} ? $o->{arrow}->{label} : 'cds' }
14192 0     0     sub value { shift->{value} }
14193 0     0     sub arrow { shift->{arrow} }
14194 0     0     sub cumulativeWeight { shift->{cumulativeWeight} }
14195              
14196             sub advance {
14197 0     0     my $o = shift;
14198 0           my $token = shift;
14199              
14200 0           my $arrows = [];
14201 0           $o->{node}->collectArrows($arrows);
14202              
14203             # Let the token know what possibilities we have
14204 0           for my $arrow (@$arrows) {
14205 0           $token->prepare($arrow->{label});
14206             }
14207              
14208             # Ask the token to interpret the text
14209 0           my @states;
14210 0           for my $arrow (@$arrows) {
14211 0   0       my $value = $token->as($arrow->{label}) // next;
14212 0           push @states, CDS::Parser::State->new($arrow->{node}, $o, $arrow, $value, $token->{warnings});
14213             }
14214              
14215 0           return @states;
14216             }
14217              
14218             sub complete {
14219 0     0     my $o = shift;
14220 0           my $token = shift;
14221              
14222 0           my $arrows = [];
14223 0           $o->{node}->collectArrows($arrows);
14224              
14225             # Let the token know what possibilities we have
14226 0           for my $arrow (@$arrows) {
14227 0 0         next if ! $arrow->{official};
14228 0           $token->prepare($arrow->{label});
14229             }
14230              
14231             # Ask the token to interpret the text
14232 0           for my $arrow (@$arrows) {
14233 0 0         next if ! $arrow->{official};
14234 0           $token->complete($arrow->{label});
14235             }
14236              
14237 0           return @{$token->{possibilities}};
  0            
14238             }
14239              
14240             sub arrows {
14241 0     0     my $o = shift;
14242              
14243 0           my $arrows = [];
14244 0           $o->{node}->collectArrows($arrows);
14245 0           return @$arrows;
14246             }
14247              
14248             sub path {
14249 0     0     my $o = shift;
14250              
14251 0           my @path;
14252 0           my $state = $o;
14253 0           while ($state) {
14254 0           unshift @path, $state;
14255 0           $state = $state->{previous};
14256             }
14257 0           return @path;
14258             }
14259              
14260             sub collect {
14261 0     0     my $o = shift;
14262 0           my $data = shift;
14263              
14264 0           for my $state ($o->path) {
14265 0   0       my $collectHandler = $state->collectHandler // next;
14266 0           &$collectHandler($data, $state->label, $state->value);
14267             }
14268             }
14269              
14270             package CDS::Parser::Token;
14271              
14272             sub new {
14273 0     0     my $class = shift;
14274 0           my $actor = shift;
14275 0           my $text = shift;
14276              
14277 0           return bless {
14278             actor => $actor,
14279             text => $text,
14280             keywords => {},
14281             cache => {},
14282             warnings => [],
14283             possibilities => [],
14284             };
14285             }
14286              
14287             sub prepare {
14288 0     0     my $o = shift;
14289 0           my $expect = shift;
14290              
14291 0 0         $o->{keywords}->{$expect} = 1 if $expect =~ /^[a-z0-9]*$/;
14292             }
14293              
14294             sub as {
14295 0     0     my $o = shift;
14296 0           my $expect = shift;
14297 0 0         exists $o->{cache}->{$expect} ? $o->{cache}->{$expect} : $o->{cache}->{$expect} = $o->produce($expect) }
14298              
14299             sub produce {
14300 0     0     my $o = shift;
14301 0           my $expect = shift;
14302              
14303 0 0         return $o->account if $expect eq 'ACCOUNT';
14304 0 0         return $o->hash if $expect eq 'ACTOR';
14305 0 0         return $o->actorGroup if $expect eq 'ACTORGROUP';
14306 0 0         return $o->aesKey if $expect eq 'AESKEY';
14307 0 0         return $o->box if $expect eq 'BOX';
14308 0 0         return $o->boxLabel if $expect eq 'BOXLABEL';
14309 0 0         return $o->file if $expect eq 'FILE';
14310 0 0         return $o->filename if $expect eq 'FILENAME';
14311 0 0         return $o->folder if $expect eq 'FOLDER';
14312 0 0         return $o->foldername if $expect eq 'FOLDERNAME';
14313 0 0         return $o->group if $expect eq 'GROUP';
14314 0 0         return $o->hash if $expect eq 'HASH';
14315 0 0         return $o->keyPair if $expect eq 'KEYPAIR';
14316 0 0         return $o->label if $expect eq 'LABEL';
14317 0 0         return $o->object if $expect eq 'OBJECT';
14318 0 0         return $o->objectFile if $expect eq 'OBJECTFILE';
14319 0 0         return $o->port if $expect eq 'PORT';
14320 0 0         return $o->store if $expect eq 'STORE';
14321 0 0         return $o->text if $expect eq 'TEXT';
14322 0 0         return $o->user if $expect eq 'USER';
14323 0 0         return $o->{text} eq $expect ? '' : undef;
14324             }
14325              
14326             sub complete {
14327 0     0     my $o = shift;
14328 0           my $expect = shift;
14329              
14330 0 0         return $o->completeAccount if $expect eq 'ACCOUNT';
14331 0 0         return $o->completeHash if $expect eq 'ACTOR';
14332 0 0         return $o->completeActorGroup if $expect eq 'ACTORGROUP';
14333 0 0         return if $expect eq 'AESKEY';
14334 0 0         return $o->completeBox if $expect eq 'BOX';
14335 0 0         return $o->completeBoxLabel if $expect eq 'BOXLABEL';
14336 0 0         return $o->completeFile if $expect eq 'FILE';
14337 0 0         return $o->completeFile if $expect eq 'FILENAME';
14338 0 0         return $o->completeFolder if $expect eq 'FOLDER';
14339 0 0         return $o->completeFolder if $expect eq 'FOLDERNAME';
14340 0 0         return $o->completeGroup if $expect eq 'GROUP';
14341 0 0         return $o->completeHash if $expect eq 'HASH';
14342 0 0         return $o->completeKeyPair if $expect eq 'KEYPAIR';
14343 0 0         return $o->completeLabel if $expect eq 'LABEL';
14344 0 0         return $o->completeObject if $expect eq 'OBJECT';
14345 0 0         return $o->completeObjectFile if $expect eq 'OBJECTFILE';
14346 0 0         return $o->completeStoreUrl if $expect eq 'STORE';
14347 0 0         return $o->completeUser if $expect eq 'USER';
14348 0 0         return if $expect eq 'TEXT';
14349 0           $o->addPossibility($expect);
14350             }
14351              
14352             sub addPossibility {
14353 0     0     my $o = shift;
14354 0           my $possibility = shift;
14355              
14356 0 0         push @{$o->{possibilities}}, $possibility.' ' if substr($possibility, 0, length $o->{text}) eq $o->{text};
  0            
14357             }
14358              
14359             sub addPartialPossibility {
14360 0     0     my $o = shift;
14361 0           my $possibility = shift;
14362              
14363 0 0         push @{$o->{possibilities}}, $possibility if substr($possibility, 0, length $o->{text}) eq $o->{text};
  0            
14364             }
14365              
14366             sub isKeyword {
14367 0     0     my $o = shift;
14368 0           exists $o->{keywords}->{$o->{text}} }
14369              
14370             sub account {
14371 0     0     my $o = shift;
14372              
14373             # From a remembered account
14374 0           my $record = $o->{actor}->remembered($o->{text});
14375 0           my $storeUrl = $record->child('store')->textValue;
14376 0           my $actorHash = CDS::Hash->fromBytes($record->child('actor')->bytesValue);
14377 0 0 0       if ($actorHash && length $storeUrl) {
14378 0   0       my $store = $o->{actor}->storeForUrl($storeUrl) // return $o->warning('Invalid store URL "', $storeUrl, '" in remembered account.');
14379 0           my $accountToken = CDS::AccountToken->new($store, $actorHash);
14380 0 0         return $o->warning('"', $o->{text}, '" is interpreted as a keyword. If you mean the account, write "', $accountToken->url, '".') if $o->isKeyword;
14381 0           return $accountToken;
14382             }
14383              
14384             # From a URL
14385 0 0         if ($o->{text} =~ /^\s*(.*?)\/accounts\/([0-9a-fA-F]{64,64})\/*\s*$/) {
14386 0           my $storeUrl = $1;
14387 0           my $actorHash = CDS::Hash->fromHex($2);
14388 0 0 0       $storeUrl = 'file://'.Cwd::abs_path($storeUrl) if $storeUrl !~ /^[a-zA-Z0-9_\+-]*:/ && -d $storeUrl;
14389 0   0       my $cliStore = $o->{actor}->storeForUrl($storeUrl) // return $o->warning('Invalid store URL "', $storeUrl, '".');
14390 0           return CDS::AccountToken->new($cliStore, $actorHash);
14391             }
14392              
14393 0           return;
14394             }
14395              
14396             sub completeAccount {
14397 0     0     my $o = shift;
14398              
14399 0           $o->completeUrl;
14400              
14401 0           my $records = $o->{actor}->rememberedRecords;
14402 0           for my $label (keys %$records) {
14403 0           my $record = $records->{$label};
14404 0           my $storeUrl = $record->child('store')->textValue;
14405 0 0         next if ! length $storeUrl;
14406 0   0       my $actorHash = CDS::Hash->fromBytes($record->child('actor')->bytesValue) // next;
14407              
14408 0           $o->addPossibility($label);
14409 0           $o->addPossibility($storeUrl.'/accounts/'.$actorHash->hex);
14410             }
14411              
14412 0           return;
14413             }
14414              
14415             sub aesKey {
14416 0     0     my $o = shift;
14417              
14418 0 0         $o->{text} =~ /^[0-9A-Fa-f]{64}$/ || return;
14419 0           return pack('H*', $o->{text});
14420             }
14421              
14422             sub box {
14423 0     0     my $o = shift;
14424              
14425             # From a URL
14426 0 0         if ($o->{text} =~ /^\s*(.*?)\/accounts\/([0-9a-fA-F]{64,64})\/(messages|private|public)\/*\s*$/) {
14427 0           my $storeUrl = $1;
14428 0           my $boxLabel = $3;
14429 0           my $actorHash = CDS::Hash->fromHex($2);
14430 0 0 0       $storeUrl = 'file://'.Cwd::abs_path($storeUrl) if $storeUrl !~ /^[a-zA-Z0-9_\+-]*:/ && -d $storeUrl;
14431 0   0       my $cliStore = $o->{actor}->storeForUrl($storeUrl) // return $o->warning('Invalid store URL "', $storeUrl, '".');
14432 0           my $accountToken = CDS::AccountToken->new($cliStore, $actorHash);
14433 0           return CDS::BoxToken->new($accountToken, $boxLabel);
14434             }
14435              
14436 0           return;
14437             }
14438              
14439             sub completeBox {
14440 0     0     my $o = shift;
14441              
14442 0           $o->completeUrl;
14443 0           return;
14444             }
14445              
14446             sub boxLabel {
14447 0     0     my $o = shift;
14448              
14449 0 0         return $o->{text} if $o->{text} eq 'messages';
14450 0 0         return $o->{text} if $o->{text} eq 'private';
14451 0 0         return $o->{text} if $o->{text} eq 'public';
14452 0           return;
14453             }
14454              
14455             sub completeBoxLabel {
14456 0     0     my $o = shift;
14457              
14458 0           $o->addPossibility('messages');
14459 0           $o->addPossibility('private');
14460 0           $o->addPossibility('public');
14461             }
14462              
14463             sub file {
14464 0     0     my $o = shift;
14465              
14466 0   0       my $file = Cwd::abs_path($o->{text}) // return;
14467 0 0         return if ! -f $file;
14468 0 0         return $o->warning('"', $o->{text}, '" is interpreted as keyword. If you mean the file, write "./', $o->{text}, '".') if $o->isKeyword;
14469 0           return $file;
14470             }
14471              
14472             sub completeFile {
14473 0     0     my $o = shift;
14474              
14475 0           my $folder = './';
14476 0           my $startFilename = $o->{text};
14477 0 0         $startFilename = $ENV{HOME}.'/'.$1 if $startFilename =~ /^~\/(.*)$/;
14478 0 0         if ($startFilename eq '~') {
    0          
14479 0           $folder = $ENV{HOME}.'/';
14480 0           $startFilename = '';
14481             } elsif ($startFilename =~ /^(.*\/)([^\/]*)$/) {
14482 0           $folder = $1;
14483 0           $startFilename = $2;
14484             }
14485              
14486 0           for my $filename (CDS->listFolder($folder)) {
14487 0 0         next if $filename eq '.';
14488 0 0         next if $filename eq '..';
14489 0 0         next if substr($filename, 0, length $startFilename) ne $startFilename;
14490 0           my $file = $folder.$filename;
14491 0 0         $file .= '/' if -d $file;
14492 0 0         $file .= ' ' if -f $file;
14493 0           push @{$o->{possibilities}}, $file;
  0            
14494             }
14495             }
14496              
14497             sub filename {
14498 0     0     my $o = shift;
14499              
14500 0 0         return $o->warning('"', $o->{text}, '" is interpreted as keyword. If you mean the file, write "./', $o->{text}, '".') if $o->isKeyword;
14501 0           return Cwd::abs_path($o->{text});
14502             }
14503              
14504             sub folder {
14505 0     0     my $o = shift;
14506              
14507 0   0       my $folder = Cwd::abs_path($o->{text}) // return;
14508 0 0         return if ! -d $folder;
14509 0 0         return $o->warning('"', $o->{text}, '" is interpreted as keyword. If you mean the folder, write "./', $o->{text}, '".') if $o->isKeyword;
14510 0           return $folder;
14511             }
14512              
14513             sub completeFolder {
14514 0     0     my $o = shift;
14515              
14516 0           my $folder = './';
14517 0           my $startFilename = $o->{text};
14518 0 0         if ($o->{text} =~ /^(.*\/)([^\/]*)$/) {
14519 0           $folder = $1;
14520 0           $startFilename = $2;
14521             }
14522              
14523 0           for my $filename (CDS->listFolder($folder)) {
14524 0 0         next if $filename eq '.';
14525 0 0         next if $filename eq '..';
14526 0 0         next if substr($filename, 0, length $startFilename) ne $startFilename;
14527 0           my $file = $folder.$filename;
14528 0 0         next if ! -d $file;
14529 0           push @{$o->{possibilities}}, $file.'/';
  0            
14530             }
14531             }
14532              
14533             sub foldername {
14534 0     0     my $o = shift;
14535              
14536 0 0         return $o->warning('"', $o->{text}, '" is interpreted as keyword. If you mean the folder, write "./', $o->{text}, '".') if $o->isKeyword;
14537 0           return Cwd::abs_path($o->{text});
14538             }
14539              
14540             sub group {
14541 0     0     my $o = shift;
14542              
14543 0 0         return int($1) if $o->{text} =~ /^\s*(\d{1,5})\s*$/;
14544 0           return getgrnam($o->{text});
14545             }
14546              
14547             sub completeGroup {
14548 0     0     my $o = shift;
14549              
14550 0           while (my $name = getgrent) {
14551 0           $o->addPossibility($name);
14552             }
14553             }
14554              
14555             sub hash {
14556 0     0     my $o = shift;
14557              
14558 0           my $hash = CDS::Hash->fromHex($o->{text});
14559 0 0         return $hash if $hash;
14560              
14561             # Check if it's a remembered actor hash
14562 0           my $record = $o->{actor}->remembered($o->{text});
14563 0   0       my $actorHash = CDS::Hash->fromBytes($record->child('actor')->bytesValue) // return;
14564 0 0         return $o->warning('"', $o->{text}, '" is interpreted as keyword. If you mean the actor, write "', $actorHash->hex, '".') if $o->isKeyword;
14565 0           return $actorHash;
14566             }
14567              
14568             sub completeHash {
14569 0     0     my $o = shift;
14570              
14571 0           my $records = $o->{actor}->rememberedRecords;
14572 0           for my $label (keys %$records) {
14573 0           my $record = $records->{$label};
14574 0   0       my $hash = CDS::Hash->fromBytes($record->child('actor')->bytesValue) // next;
14575 0           $o->addPossibility($label);
14576 0           $o->addPossibility($hash->hex);
14577             }
14578              
14579 0           for my $child ($o->{actor}->actorGroupSelector->children) {
14580 0   0       my $hash = $child->record->child('hash')->hashValue // next;
14581 0           $o->addPossibility($hash->hex);
14582             }
14583             }
14584              
14585             sub keyPair {
14586 0     0     my $o = shift;
14587              
14588             # Remembered key pair
14589 0           my $record = $o->{actor}->remembered($o->{text});
14590 0           my $file = $record->child('key pair')->textValue;
14591              
14592             # Key pair from file
14593 0 0         if (! length $file) {
14594 0   0       $file = Cwd::abs_path($o->{text}) // return;
14595 0 0 0       return $o->warning('"', $o->{text}, '" is interpreted as keyword. If you mean the file, write "./', $o->{text}, '".') if $o->isKeyword && -f $file;
14596             }
14597              
14598             # Load the key pair
14599 0 0         return if ! -f $file;
14600 0   0       my $bytes = CDS->readBytesFromFile($file) // return $o->warning('The key pair file "', $file, '" could not be read.');
14601 0   0       my $keyPair = CDS::KeyPair->fromRecord(CDS::Record->fromObject(CDS::Object->fromBytes($bytes))) // return $o->warning('The file "', $file, '" does not contain a key pair.');
14602 0           return CDS::KeyPairToken->new($file, $keyPair);
14603             }
14604              
14605             sub completeKeyPair {
14606 0     0     my $o = shift;
14607              
14608 0           $o->completeFile;
14609              
14610 0           my $records = $o->{actor}->rememberedRecords;
14611 0           for my $label (keys %$records) {
14612 0           my $record = $records->{$label};
14613 0 0         next if ! length $record->child('key pair')->textValue;
14614 0           $o->addPossibility($label);
14615             }
14616             }
14617              
14618             sub label {
14619 0     0     my $o = shift;
14620              
14621 0           my $records = $o->{actor}->remembered($o->{text});
14622 0 0         return $o->{text} if $records->children;
14623 0           return;
14624             }
14625              
14626             sub completeLabel {
14627 0     0     my $o = shift;
14628              
14629 0           my $records = $o->{actor}->rememberedRecords;
14630 0           for my $label (keys %$records) {
14631 0 0         next if substr($label, 0, length $o->{text}) ne $o->{text};
14632 0           $o->addPossibility($label);
14633             }
14634             }
14635              
14636             sub object {
14637 0     0     my $o = shift;
14638              
14639             # Folder stores use the first two hex digits as folder
14640 0 0         my $url = $o->{text} =~ /^\s*(.*?\/objects\/)([0-9a-fA-F]{2,2})\/([0-9a-fA-F]{62,62})\/*\s*$/ ? $1.$2.$3 : $o->{text};
14641              
14642             # From a URL
14643 0 0         if ($url =~ /^\s*(.*?)\/objects\/([0-9a-fA-F]{64,64})\/*\s*$/) {
14644 0           my $storeUrl = $1;
14645 0           my $hash = CDS::Hash->fromHex($2);
14646 0 0 0       $storeUrl = 'file://'.Cwd::abs_path($storeUrl) if $storeUrl !~ /^[a-zA-Z0-9_\+-]*:/ && -d $storeUrl;
14647 0   0       my $cliStore = $o->{actor}->storeForUrl($storeUrl) // return $o->warning('Invalid store URL "', $storeUrl, '".');
14648 0           return CDS::ObjectToken->new($cliStore, $hash);
14649             }
14650              
14651 0           return;
14652             }
14653              
14654             sub completeObject {
14655 0     0     my $o = shift;
14656              
14657 0           $o->completeUrl;
14658 0           return;
14659             }
14660              
14661             sub objectFile {
14662 0     0     my $o = shift;
14663              
14664             # Key pair from file
14665 0   0       my $file = Cwd::abs_path($o->{text}) // return;
14666 0 0 0       return $o->warning('"', $o->{text}, '" is interpreted as keyword. If you mean the file, write "./', $o->{text}, '".') if $o->isKeyword && -f $file;
14667              
14668             # Load the object
14669 0 0         return if ! -f $file;
14670 0   0       my $bytes = CDS->readBytesFromFile($file) // return $o->warning('The object file "', $file, '" could not be read.');
14671 0   0       my $object = CDS::Object->fromBytes($bytes) // return $o->warning('The file "', $file, '" does not contain a Condensation object.');
14672 0           return CDS::ObjectFileToken->new($file, $object);
14673             }
14674              
14675             sub completeObjectFile {
14676 0     0     my $o = shift;
14677              
14678 0           $o->completeFile;
14679 0           return;
14680             }
14681              
14682             sub actorGroup {
14683 0     0     my $o = shift;
14684              
14685             # We only accept named actor groups. Accepting a single account as actor group is ambiguous whenever ACCOUNT and ACTORGROUP are accepted. For commands that are requiring an ACTORGROUP, they can also accept an ACCOUNT and then convert it.
14686              
14687             # Check if it's an actor group label
14688 0           my $record = $o->{actor}->remembered($o->{text})->child('actor group');
14689 0 0         return if ! scalar $record->children;
14690 0 0         return $o->warning('"', $o->{text}, '" is interpreted as keyword. To refer to the actor group, rename it.') if $o->isKeyword;
14691              
14692 0           my $builder = CDS::ActorGroupBuilder->new;
14693 0           $builder->addKnownPublicKey($o->{actor}->keyPair->publicKey);
14694 0           $builder->parse($record, 1);
14695 0           my ($actorGroup, $storeError) = $builder->load($o->{actor}->groupDocument->unsaved, $o->{actor}->keyPair, $o);
14696 0 0         return $o->{actor}->storeError($o->{actor}->storageStore, $storeError) if defined $storeError;
14697 0           return CDS::ActorGroupToken->new($o->{text}, $actorGroup);
14698             }
14699              
14700             sub onLoadActorGroupVerifyStore {
14701 0     0     my $o = shift;
14702 0           my $storeUrl = shift;
14703 0           $o->{actor}->storeForUrl($storeUrl); }
14704              
14705             sub completeActorGroup {
14706 0     0     my $o = shift;
14707              
14708 0           my $records = $o->{actor}->rememberedRecords;
14709 0           for my $label (keys %$records) {
14710 0           my $record = $records->{$label};
14711 0 0         next if ! scalar $record->child('actor group')->children;
14712 0           $o->addPossibility($label);
14713             }
14714 0           return;
14715             }
14716              
14717             sub port {
14718 0     0     my $o = shift;
14719              
14720 0           my $port = int($o->{text});
14721 0 0 0       return if $port <= 0 || $port > 65536;
14722 0           return $port;
14723             }
14724              
14725             sub rememberedStoreUrl {
14726 0     0     my $o = shift;
14727              
14728 0           my $record = $o->{actor}->remembered($o->{text});
14729 0           my $storeUrl = $record->child('store')->textValue;
14730 0 0         return if ! length $storeUrl;
14731              
14732 0 0         return $o->warning('"', $o->{text}, '" is interpreted as keyword. If you mean the store, write "', $storeUrl, '".') if $o->isKeyword;
14733 0           return $storeUrl;
14734             }
14735              
14736             sub directStoreUrl {
14737 0     0     my $o = shift;
14738              
14739 0 0         return $o->warning('"', $o->{text}, '" is interpreted as keyword. If you mean the folder store, write "./', $o->{text}, '".') if $o->isKeyword;
14740 0 0         return if $o->{text} =~ /[0-9a-f]{32}/;
14741              
14742 0 0         return $o->{text} if $o->{text} =~ /^[a-zA-Z0-9_\+-]*:/;
14743 0 0 0       return 'file://'.Cwd::abs_path($o->{text}) if -d $o->{text} && -d $o->{text}.'/accounts' && -d $o->{text}.'/objects';
      0        
14744 0           return;
14745             }
14746              
14747             sub store {
14748 0     0     my $o = shift;
14749              
14750 0   0       my $url = $o->rememberedStoreUrl // $o->directStoreUrl // return;
      0        
14751 0   0       return $o->{actor}->storeForUrl($url) // return $o->warning('"', $o->{text}, '" looks like a store, but no implementation is available to handle this protocol.');
14752             }
14753              
14754             sub completeFolderStoreUrl {
14755 0     0     my $o = shift;
14756              
14757 0           my $folder = './';
14758 0           my $startFilename = $o->{text};
14759 0 0         if ($o->{text} =~ /^(.*\/)([^\/]*)$/) {
14760 0           $folder = $1;
14761 0           $startFilename = $2;
14762             }
14763              
14764 0           for my $filename (CDS->listFolder($folder)) {
14765 0 0         next if $filename eq '.';
14766 0 0         next if $filename eq '..';
14767 0 0         next if substr($filename, 0, length $startFilename) ne $startFilename;
14768 0           my $file = $folder.$filename;
14769 0 0         next if ! -d $file;
14770 0 0 0       push @{$o->{possibilities}}, $file . (-d $file.'/accounts' && -d $file.'/objects' ? ' ' : '/');
  0            
14771             }
14772             }
14773              
14774             sub completeStoreUrl {
14775 0     0     my $o = shift;
14776              
14777 0           $o->completeFolderStoreUrl;
14778 0           $o->completeUrl;
14779              
14780 0           my $records = $o->{actor}->rememberedRecords;
14781 0           for my $label (keys %$records) {
14782 0           my $record = $records->{$label};
14783 0 0         next if length $record->child('actor')->bytesValue;
14784 0           my $storeUrl = $record->child('store')->textValue;
14785 0 0         next if ! length $storeUrl;
14786 0           $o->addPossibility($label);
14787 0           $o->addPossibility($storeUrl);
14788             }
14789             }
14790              
14791             sub completeUrl {
14792 0     0     my $o = shift;
14793              
14794 0           $o->addPartialPossibility('http://');
14795 0           $o->addPartialPossibility('https://');
14796 0           $o->addPartialPossibility('ftp://');
14797 0           $o->addPartialPossibility('sftp://');
14798 0           $o->addPartialPossibility('file://');
14799             }
14800              
14801             sub text {
14802 0     0     my $o = shift;
14803              
14804 0           return $o->{text};
14805             }
14806              
14807             sub user {
14808 0     0     my $o = shift;
14809              
14810 0 0         return int($1) if $o->{text} =~ /^\s*(\d{1,5})\s*$/;
14811 0           return getpwnam($o->{text});
14812             }
14813              
14814             sub completeUser {
14815 0     0     my $o = shift;
14816              
14817 0           while (my $name = getpwent) {
14818 0           $o->addPossibility($name);
14819             }
14820             }
14821              
14822             sub warning {
14823 0     0     my $o = shift;
14824              
14825 0           push @{$o->{warnings}}, join('', @_);
  0            
14826 0           return;
14827             }
14828              
14829             # Reads the private box of an actor.
14830             package CDS::PrivateBoxReader;
14831              
14832             sub new {
14833 0     0     my $class = shift;
14834 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
14835 0           my $store = shift;
14836 0           my $delegate = shift;
14837              
14838 0           return bless {
14839             keyPair => $keyPair,
14840             actorOnStore => CDS::ActorOnStore->new($keyPair->publicKey, $store),
14841             delegate => $delegate,
14842             entries => {},
14843             };
14844             }
14845              
14846 0     0     sub keyPair { shift->{keyPair} }
14847 0     0     sub actorOnStore { shift->{actorOnStore} }
14848 0     0     sub delegate { shift->{delegate} }
14849              
14850             sub read {
14851 0     0     my $o = shift;
14852              
14853 0           my $store = $o->{actorOnStore}->store;
14854 0           my ($hashes, $listError) = $store->list($o->{actorOnStore}->publicKey->hash, 'private', 0, $o->{keyPair});
14855 0 0         return if defined $listError;
14856              
14857             # Keep track of the processed entries
14858 0           my $newEntries = {};
14859 0           for my $hash (@$hashes) {
14860 0   0       $newEntries->{$hash->bytes} = $o->{entries}->{$hash->bytes} // {hash => $hash, processed => 0};
14861             }
14862 0           $o->{entries} = $newEntries;
14863              
14864             # Process new entries
14865 0           for my $entry (values %$newEntries) {
14866 0 0         next if $entry->{processed};
14867              
14868             # Get the envelope
14869 0           my ($object, $getError) = $store->get($entry->{hash}, $o->{keyPair});
14870 0 0         return if defined $getError;
14871              
14872 0 0         if (! defined $object) {
14873 0           $o->invalid($entry, 'Envelope object not found.');
14874 0           next;
14875             }
14876              
14877             # Parse the record
14878 0           my $envelope = CDS::Record->fromObject($object);
14879 0 0         if (! $envelope) {
14880 0           $o->invalid($entry, 'Envelope is not a record.');
14881 0           next;
14882             }
14883              
14884             # Read the content hash
14885 0           my $contentHash = $envelope->child('content')->hashValue;
14886 0 0         if (! $contentHash) {
14887 0           $o->invalid($entry, 'Missing content hash.');
14888 0           next;
14889             }
14890              
14891             # Verify the signature
14892 0 0         if (! CDS->verifyEnvelopeSignature($envelope, $o->{keyPair}->publicKey, $contentHash)) {
14893 0           $o->invalid($entry, 'Invalid signature.');
14894 0           next;
14895             }
14896              
14897             # Decrypt the key
14898 0           my $aesKey = $o->{keyPair}->decryptKeyOnEnvelope($envelope);
14899 0 0         if (! $aesKey) {
14900 0           $o->invalid($entry, 'Not encrypted for us.');
14901 0           next;
14902             }
14903              
14904             # Retrieve the content
14905 0           my $contentHashAndKey = CDS::HashAndKey->new($contentHash, $aesKey);
14906 0           my ($contentRecord, $contentObject, $contentInvalidReason, $contentStoreError) = $o->{keyPair}->getAndDecryptRecord($contentHashAndKey, $store);
14907 0 0         return if defined $contentStoreError;
14908              
14909 0 0         if (defined $contentInvalidReason) {
14910 0           $o->invalid($entry, $contentInvalidReason);
14911 0           next;
14912             }
14913              
14914 0           $entry->{processed} = 1;
14915 0           my $source = CDS::Source->new($o->{keyPair}, $o->{actorOnStore}, 'private', $entry->{hash});
14916 0           $o->{delegate}->onPrivateBoxEntry($source, $envelope, $contentHashAndKey, $contentRecord);
14917             }
14918              
14919 0           return 1;
14920             }
14921              
14922             sub invalid {
14923 0     0     my $o = shift;
14924 0           my $entry = shift;
14925 0           my $reason = shift;
14926              
14927 0           $entry->{processed} = 1;
14928 0           my $source = CDS::Source->new($o->{actorOnStore}, 'private', $entry->{hash});
14929 0           $o->{delegate}->onPrivateBoxInvalidEntry($source, $reason);
14930             }
14931              
14932             # Delegate
14933             # onPrivateBoxEntry($source, $envelope, $contentHashAndKey, $contentRecord)
14934             # onPrivateBoxInvalidEntry($source, $reason)
14935              
14936             package CDS::PrivateRoot;
14937              
14938             sub new {
14939 0     0     my $class = shift;
14940 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
14941 0           my $store = shift;
14942 0           my $delegate = shift;
14943              
14944 0           my $o = bless {
14945             unsaved => CDS::Unsaved->new($store),
14946             delegate => $delegate,
14947             dataHandlers => {},
14948             hasChanges => 0,
14949             procured => 0,
14950             mergedEntries => [],
14951             };
14952              
14953 0           $o->{privateBoxReader} = CDS::PrivateBoxReader->new($keyPair, $store, $o);
14954 0           return $o;
14955             }
14956              
14957 0     0     sub delegate { shift->{delegate} }
14958 0     0     sub privateBoxReader { shift->{privateBoxReader} }
14959 0     0     sub unsaved { shift->{unsaved} }
14960 0     0     sub hasChanges { shift->{hasChanges} }
14961 0     0     sub procured { shift->{procured} }
14962              
14963             sub addDataHandler {
14964 0     0     my $o = shift;
14965 0           my $label = shift;
14966 0           my $dataHandler = shift;
14967              
14968 0           $o->{dataHandlers}->{$label} = $dataHandler;
14969             }
14970              
14971             sub removeDataHandler {
14972 0     0     my $o = shift;
14973 0           my $label = shift;
14974 0           my $dataHandler = shift;
14975              
14976 0           my $registered = $o->{dataHandlers}->{$label};
14977 0 0         return if $registered != $dataHandler;
14978 0           delete $o->{dataHandlers}->{$label};
14979             }
14980              
14981             # *** Procurement
14982              
14983             sub procure {
14984 0     0     my $o = shift;
14985 0           my $interval = shift;
14986              
14987 0           my $now = CDS->now;
14988 0 0         return $o->{procured} if $o->{procured} + $interval > $now;
14989 0   0       $o->{privateBoxReader}->read // return;
14990 0           $o->{procured} = $now;
14991 0           return $now;
14992             }
14993              
14994             # *** Merging
14995              
14996             sub onPrivateBoxEntry {
14997 0     0     my $o = shift;
14998 0 0 0       my $source = shift; die 'wrong type '.ref($source).' for $source' if defined $source && ref $source ne 'CDS::Source';
  0            
14999 0 0 0       my $envelope = shift; die 'wrong type '.ref($envelope).' for $envelope' if defined $envelope && ref $envelope ne 'CDS::Record';
  0            
15000 0           my $contentHashAndKey = shift;
15001 0           my $content = shift;
15002              
15003 0           for my $section ($content->children) {
15004 0   0       my $dataHandler = $o->{dataHandlers}->{$section->bytes} // next;
15005 0           $dataHandler->mergeData($section);
15006             }
15007              
15008 0           push @{$o->{mergedEntries}}, $source->hash;
  0            
15009             }
15010              
15011             sub onPrivateBoxInvalidEntry {
15012 0     0     my $o = shift;
15013 0 0 0       my $source = shift; die 'wrong type '.ref($source).' for $source' if defined $source && ref $source ne 'CDS::Source';
  0            
15014 0           my $reason = shift;
15015              
15016 0           $o->{delegate}->onPrivateRootReadingInvalidEntry($source, $reason);
15017 0           $source->discard;
15018             }
15019              
15020             # *** Saving
15021              
15022             sub dataChanged {
15023 0     0     my $o = shift;
15024              
15025 0           $o->{hasChanges} = 1;
15026             }
15027              
15028             sub save {
15029 0     0     my $o = shift;
15030 0           my $entrustedKeys = shift;
15031              
15032 0           $o->{unsaved}->startSaving;
15033 0 0         return $o->savingSucceeded if ! $o->{hasChanges};
15034 0           $o->{hasChanges} = 0;
15035              
15036             # Create the record
15037 0           my $record = CDS::Record->new;
15038 0           $record->add('created')->addInteger(CDS->now);
15039 0           $record->add('client')->add(CDS->version);
15040 0           for my $label (keys %{$o->{dataHandlers}}) {
  0            
15041 0           my $dataHandler = $o->{dataHandlers}->{$label};
15042 0           $dataHandler->addDataTo($record->add($label));
15043             }
15044              
15045             # Submit the object
15046 0           my $key = CDS->randomKey;
15047 0           my $object = $record->toObject->crypt($key);
15048 0           my $hash = $object->calculateHash;
15049 0           $o->{unsaved}->savingState->addObject($hash, $object);
15050 0           my $hashAndKey = CDS::HashAndKey->new($hash, $key);
15051              
15052             # Create the envelope
15053 0           my $keyPair = $o->{privateBoxReader}->keyPair;
15054 0           my $publicKeys = [$keyPair->publicKey, @$entrustedKeys];
15055 0           my $envelopeObject = $keyPair->createPrivateEnvelope($hashAndKey, $publicKeys)->toObject;
15056 0           my $envelopeHash = $envelopeObject->calculateHash;
15057 0           $o->{unsaved}->savingState->addObject($envelopeHash, $envelopeObject);
15058              
15059             # Transfer
15060 0           my ($missing, $store, $storeError) = $keyPair->transfer([$hash], $o->{unsaved}, $o->{privateBoxReader}->actorOnStore->store);
15061 0 0 0       return $o->savingFailed($missing) if defined $missing || defined $storeError;
15062              
15063             # Modify the private box
15064 0           my $modifications = CDS::StoreModifications->new;
15065 0           $modifications->add($keyPair->publicKey->hash, 'private', $envelopeHash, $envelopeObject);
15066 0           for my $hash (@{$o->{mergedEntries}}) {
  0            
15067 0           $modifications->remove($keyPair->publicKey->hash, 'private', $hash);
15068             }
15069              
15070 0           my $modifyError = $o->{privateBoxReader}->actorOnStore->store->modify($modifications, $keyPair);
15071 0 0         return $o->savingFailed if defined $modifyError;
15072              
15073             # Set the new merged hashes
15074 0           $o->{mergedEntries} = [$envelopeHash];
15075 0           return $o->savingSucceeded;
15076             }
15077              
15078             sub savingSucceeded {
15079 0     0     my $o = shift;
15080              
15081             # Discard all merged sources
15082 0           for my $source ($o->{unsaved}->savingState->mergedSources) {
15083 0           $source->discard;
15084             }
15085              
15086             # Call all data saved handlers
15087 0           for my $handler ($o->{unsaved}->savingState->dataSavedHandlers) {
15088 0           $handler->onDataSaved;
15089             }
15090              
15091 0           $o->{unsaved}->savingDone;
15092 0           return 1;
15093             }
15094              
15095             sub savingFailed {
15096 0     0     my $o = shift;
15097 0           my $missing = shift;
15098             # private
15099 0           $o->{unsaved}->savingFailed;
15100 0           $o->{hasChanges} = 1;
15101 0           return undef, $missing;
15102             }
15103              
15104             # A public key of somebody.
15105             package CDS::PublicKey;
15106              
15107             sub fromObject {
15108 0     0     my $class = shift;
15109 0 0 0       my $object = shift; die 'wrong type '.ref($object).' for $object' if defined $object && ref $object ne 'CDS::Object';
  0            
15110              
15111 0   0       my $record = CDS::Record->fromObject($object) // return;
15112 0   0       my $rsaPublicKey = CDS::C::publicKeyNew($record->child('e')->bytesValue, $record->child('n')->bytesValue) // return;
15113 0           return bless {
15114             hash => $object->calculateHash,
15115             rsaPublicKey => $rsaPublicKey,
15116             object => $object,
15117             lastAccess => 0, # used by PublicKeyCache
15118             };
15119             }
15120              
15121 0     0     sub object { shift->{object} }
15122             sub bytes {
15123 0     0     my $o = shift;
15124 0           $o->{object}->bytes }
15125              
15126             ### Public key interface ###
15127              
15128 0     0     sub hash { shift->{hash} }
15129             sub encrypt {
15130 0     0     my $o = shift;
15131 0           my $bytes = shift;
15132 0           CDS::C::publicKeyEncrypt($o->{rsaPublicKey}, $bytes) }
15133             sub verifyHash {
15134 0     0     my $o = shift;
15135 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
15136 0           my $signature = shift;
15137 0           CDS::C::publicKeyVerify($o->{rsaPublicKey}, $hash->bytes, $signature) }
15138              
15139             package CDS::PublicKeyCache;
15140              
15141             sub new {
15142 0     0     my $class = shift;
15143 0           my $maxSize = shift;
15144              
15145 0           return bless {
15146             cache => {},
15147             maxSize => $maxSize,
15148             };
15149             }
15150              
15151             sub add {
15152 0     0     my $o = shift;
15153 0 0 0       my $publicKey = shift; die 'wrong type '.ref($publicKey).' for $publicKey' if defined $publicKey && ref $publicKey ne 'CDS::PublicKey';
  0            
15154              
15155 0           $o->{cache}->{$publicKey->hash->bytes} = {publicKey => $publicKey, lastAccess => CDS->now};
15156 0           $o->deleteOldest;
15157 0           return;
15158             }
15159              
15160             sub get {
15161 0     0     my $o = shift;
15162 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
15163              
15164 0   0       my $entry = $o->{cache}->{$hash->bytes} // return;
15165 0           $entry->{lastAccess} = CDS->now;
15166 0           return $entry->{publicKey};
15167             }
15168              
15169             sub deleteOldest {
15170 0     0     my $o = shift;
15171             # private
15172 0 0         return if scalar values %{$o->{cache}} < $o->{maxSize};
  0            
15173              
15174 0           my @entries = sort { $a->{lastAccess} <=> $b->{lastAccess} } values %{$o->{cache}};
  0            
  0            
15175 0           my $toRemove = int(scalar(@entries) - $o->{maxSize} / 2);
15176 0           for my $entry (@entries) {
15177 0           $toRemove -= 1;
15178 0 0         last if $toRemove <= 0;
15179 0           delete $o->{cache}->{$entry->{publicKey}->hash->bytes};
15180             }
15181             }
15182              
15183             package CDS::PutTree;
15184              
15185             sub new {
15186 0     0     my $o = shift;
15187 0           my $store = shift;
15188 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
15189 0           my $commitPool = shift;
15190              
15191 0           return bless {
15192             store => $store,
15193             commitPool => $commitPool,
15194             keyPair => $keyPair,
15195             done => {},
15196             };
15197             }
15198              
15199             sub put {
15200 0     0     my $o = shift;
15201 0 0 0       my $hash = shift // return; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0   0        
15202              
15203 0 0         return if $o->{done}->{$hash->bytes};
15204              
15205             # Get the item
15206 0   0       my $hashAndObject = $o->{commitPool}->object($hash) // return;
15207              
15208             # Upload all children
15209 0           for my $hash ($hashAndObject->object->hashes) {
15210 0           my $error = $o->put($hash);
15211 0 0         return $error if defined $error;
15212             }
15213              
15214             # Upload this object
15215 0           my $error = $o->{store}->put($hashAndObject->hash, $hashAndObject->object, $o->{keyPair});
15216 0 0         return $error if defined $error;
15217 0           $o->{done}->{$hash->bytes} = 1;
15218 0           return;
15219             }
15220              
15221             package CDS::ReceivedMessage;
15222              
15223             sub new {
15224 0     0     my $class = shift;
15225 0           my $messageBoxReader = shift;
15226 0           my $entry = shift;
15227 0 0 0       my $source = shift; die 'wrong type '.ref($source).' for $source' if defined $source && ref $source ne 'CDS::Source';
  0            
15228 0 0 0       my $envelope = shift; die 'wrong type '.ref($envelope).' for $envelope' if defined $envelope && ref $envelope ne 'CDS::Record';
  0            
15229 0           my $senderStoreUrl = shift;
15230 0           my $sender = shift;
15231 0           my $content = shift;
15232 0           my $streamHead = shift;
15233              
15234 0           return bless {
15235             messageBoxReader => $messageBoxReader,
15236             entry => $entry,
15237             source => $source,
15238             envelope => $envelope,
15239             senderStoreUrl => $senderStoreUrl,
15240             sender => $sender,
15241             content => $content,
15242             streamHead => $streamHead,
15243             isDone => 0,
15244             };
15245             }
15246              
15247 0     0     sub source { shift->{source} }
15248 0     0     sub envelope { shift->{envelope} }
15249 0     0     sub senderStoreUrl { shift->{senderStoreUrl} }
15250 0     0     sub sender { shift->{sender} }
15251 0     0     sub content { shift->{content} }
15252              
15253             sub waitForSenderStore {
15254 0     0     my $o = shift;
15255              
15256 0           $o->{entry}->{waitingForStore} = $o->sender->store;
15257             }
15258              
15259             sub skip {
15260 0     0     my $o = shift;
15261              
15262 0           $o->{entry}->{processed} = 0;
15263             }
15264              
15265             # A record is a tree, whereby each nodes holds a byte sequence and an optional hash.
15266             # Child nodes are ordered, although the order does not always matter.
15267             package CDS::Record;
15268              
15269             sub fromObject {
15270 0     0     my $class = shift;
15271 0 0 0       my $object = shift // return; die 'wrong type '.ref($object).' for $object' if defined $object && ref $object ne 'CDS::Object';
  0   0        
15272              
15273 0           my $root = CDS::Record->new;
15274 0   0       $root->addFromObject($object) // return;
15275 0           return $root;
15276             }
15277              
15278             sub new {
15279 0     0     my $class = shift;
15280 0           my $bytes = shift;
15281 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
15282              
15283 0   0       bless {
15284             bytes => $bytes // '',
15285             hash => $hash,
15286             children => [],
15287             };
15288             }
15289              
15290             # *** Adding
15291              
15292             # Adds a record
15293             sub add {
15294 0     0     my $o = shift;
15295 0           my $bytes = shift;
15296 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
15297              
15298 0           my $record = CDS::Record->new($bytes, $hash);
15299 0           push @{$o->{children}}, $record;
  0            
15300 0           return $record;
15301             }
15302              
15303             sub addText {
15304 0     0     my $o = shift;
15305 0           my $value = shift;
15306 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
15307 0   0       $o->add(Encode::encode_utf8($value // ''), $hash) }
15308             sub addBoolean {
15309 0     0     my $o = shift;
15310 0           my $value = shift;
15311 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
15312 0           $o->add(CDS->bytesFromBoolean($value), $hash) }
15313             sub addInteger {
15314 0     0     my $o = shift;
15315 0           my $value = shift;
15316 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
15317 0   0       $o->add(CDS->bytesFromInteger($value // 0), $hash) }
15318             sub addUnsigned {
15319 0     0     my $o = shift;
15320 0           my $value = shift;
15321 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
15322 0   0       $o->add(CDS->bytesFromUnsigned($value // 0), $hash) }
15323             sub addFloat32 {
15324 0     0     my $o = shift;
15325 0           my $value = shift;
15326 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
15327 0   0       $o->add(CDS->bytesFromFloat32($value // 0), $hash) }
15328             sub addFloat64 {
15329 0     0     my $o = shift;
15330 0           my $value = shift;
15331 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
15332 0   0       $o->add(CDS->bytesFromFloat64($value // 0), $hash) }
15333             sub addHash {
15334 0     0     my $o = shift;
15335 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
15336 0           $o->add('', $hash) }
15337             sub addHashAndKey {
15338 0     0     my $o = shift;
15339 0 0 0       my $hashAndKey = shift; die 'wrong type '.ref($hashAndKey).' for $hashAndKey' if defined $hashAndKey && ref $hashAndKey ne 'CDS::HashAndKey';
  0            
15340 0 0         $hashAndKey ? $o->add($hashAndKey->key, $hashAndKey->hash) : $o->add('') }
15341             sub addRecord {
15342 0     0     my $o = shift;
15343 0           push @{$o->{children}}, @_; return; }
  0            
  0            
15344              
15345             sub addFromObject {
15346 0     0     my $o = shift;
15347 0 0 0       my $object = shift // return; die 'wrong type '.ref($object).' for $object' if defined $object && ref $object ne 'CDS::Object';
  0   0        
15348              
15349 0 0         return 1 if ! length $object->data;
15350 0           return CDS::RecordReader->new($object)->readChildren($o);
15351             }
15352              
15353             # *** Set value
15354              
15355             sub set {
15356 0     0     my $o = shift;
15357 0           my $bytes = shift;
15358 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
15359              
15360 0           $o->{bytes} = $bytes;
15361 0           $o->{hash} = $hash;
15362 0           return;
15363             }
15364              
15365             # *** Querying
15366              
15367             # Returns true if the record contains a child with the indicated bytes.
15368             sub contains {
15369 0     0     my $o = shift;
15370 0           my $bytes = shift;
15371              
15372 0           for my $child (@{$o->{children}}) {
  0            
15373 0 0         return 1 if $child->{bytes} eq $bytes;
15374             }
15375 0           return;
15376             }
15377              
15378             # Returns the child record for the given bytes. If no record with these bytes exists, a record with these bytes is returned (but not added).
15379             sub child {
15380 0     0     my $o = shift;
15381 0           my $bytes = shift;
15382              
15383 0           for my $child (@{$o->{children}}) {
  0            
15384 0 0         return $child if $child->{bytes} eq $bytes;
15385             }
15386 0           return $o->new($bytes);
15387             }
15388              
15389             # Returns the first child, or an empty record.
15390             sub firstChild {
15391 0     0     my $o = shift;
15392 0   0       $o->{children}->[0] // $o->new }
15393              
15394             # Returns the nth child, or an empty record.
15395             sub nthChild {
15396 0     0     my $o = shift;
15397 0           my $i = shift;
15398 0   0       $o->{children}->[$i] // $o->new }
15399              
15400             sub containsText {
15401 0     0     my $o = shift;
15402 0           my $text = shift;
15403 0   0       $o->contains(Encode::encode_utf8($text // '')) }
15404             sub childWithText {
15405 0     0     my $o = shift;
15406 0           my $text = shift;
15407 0   0       $o->child(Encode::encode_utf8($text // '')) }
15408              
15409             # *** Get value
15410              
15411 0     0     sub bytes { shift->{bytes} }
15412 0     0     sub hash { shift->{hash} }
15413             sub children {
15414 0     0     my $o = shift;
15415 0           @{$o->{children}} }
  0            
15416              
15417             sub asText {
15418 0     0     my $o = shift;
15419 0   0       Encode::decode_utf8($o->{bytes}) // '' }
15420             sub asBoolean {
15421 0     0     my $o = shift;
15422 0           CDS->booleanFromBytes($o->{bytes}) }
15423             sub asInteger {
15424 0     0     my $o = shift;
15425 0   0       CDS->integerFromBytes($o->{bytes}) // 0 }
15426             sub asUnsigned {
15427 0     0     my $o = shift;
15428 0   0       CDS->unsignedFromBytes($o->{bytes}) // 0 }
15429             sub asFloat {
15430 0     0     my $o = shift;
15431 0   0       CDS->floatFromBytes($o->{bytes}) // 0 }
15432              
15433             sub asHashAndKey {
15434 0     0     my $o = shift;
15435              
15436 0 0         return if ! $o->{hash};
15437 0 0         return if length $o->{bytes} != 32;
15438 0           return CDS::HashAndKey->new($o->{hash}, $o->{bytes});
15439             }
15440              
15441             sub bytesValue {
15442 0     0     my $o = shift;
15443 0           $o->firstChild->bytes }
15444             sub hashValue {
15445 0     0     my $o = shift;
15446 0           $o->firstChild->hash }
15447             sub textValue {
15448 0     0     my $o = shift;
15449 0           $o->firstChild->asText }
15450             sub booleanValue {
15451 0     0     my $o = shift;
15452 0           $o->firstChild->asBoolean }
15453             sub integerValue {
15454 0     0     my $o = shift;
15455 0           $o->firstChild->asInteger }
15456             sub unsignedValue {
15457 0     0     my $o = shift;
15458 0           $o->firstChild->asUnsigned }
15459             sub floatValue {
15460 0     0     my $o = shift;
15461 0           $o->firstChild->asFloat }
15462             sub hashAndKeyValue {
15463 0     0     my $o = shift;
15464 0           $o->firstChild->asHashAndKey }
15465              
15466             # *** Dependent hashes
15467              
15468             sub dependentHashes {
15469 0     0     my $o = shift;
15470              
15471 0           my $hashes = {};
15472 0           $o->traverseHashes($hashes);
15473 0           return values %$hashes;
15474             }
15475              
15476             sub traverseHashes {
15477 0     0     my $o = shift;
15478 0           my $hashes = shift;
15479             # private
15480 0 0         $hashes->{$o->{hash}->bytes} = $o->{hash} if $o->{hash};
15481 0           for my $child (@{$o->{children}}) {
  0            
15482 0           $child->traverseHashes($hashes);
15483             }
15484             }
15485              
15486             # *** Size
15487              
15488             sub countEntries {
15489 0     0     my $o = shift;
15490              
15491 0           my $count = 1;
15492 0           for my $child (@{$o->{children}}) { $count += $child->countEntries; }
  0            
  0            
15493 0           return $count;
15494             }
15495              
15496             sub calculateSize {
15497 0     0     my $o = shift;
15498              
15499 0           return 4 + $o->calculateSizeContribution;
15500             }
15501              
15502             sub calculateSizeContribution {
15503 0     0     my $o = shift;
15504             # private
15505 0           my $byteLength = length $o->{bytes};
15506 0 0         my $size = $byteLength < 30 ? 1 : $byteLength < 286 ? 2 : 9;
    0          
15507 0           $size += $byteLength;
15508 0 0         $size += 32 + 4 if $o->{hash};
15509 0           for my $child (@{$o->{children}}) {
  0            
15510 0           $size += $child->calculateSizeContribution;
15511             }
15512 0           return $size;
15513             }
15514              
15515             # *** Serialization
15516              
15517             # Serializes this record into a Condensation object.
15518             sub toObject {
15519 0     0     my $o = shift;
15520              
15521 0           my $writer = CDS::RecordWriter->new;
15522 0           $writer->writeChildren($o);
15523 0           return CDS::Object->create($writer->header, $writer->data);
15524             }
15525              
15526             package CDS::RecordReader;
15527              
15528             sub new {
15529 0     0     my $class = shift;
15530 0 0 0       my $object = shift; die 'wrong type '.ref($object).' for $object' if defined $object && ref $object ne 'CDS::Object';
  0            
15531              
15532 0           return bless {
15533             object => $object,
15534             data => $object->data,
15535             pos => 0,
15536             hasError => 0
15537             };
15538             }
15539              
15540 0     0     sub hasError { shift->{hasError} }
15541              
15542             sub readChildren {
15543 0     0     my $o = shift;
15544 0 0 0       my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0            
15545              
15546 0           while (1) {
15547             # Flags
15548 0   0       my $flags = $o->readUnsigned8 // return;
15549              
15550             # Data
15551 0           my $length = $flags & 0x1f;
15552 0 0 0       my $byteLength = $length == 30 ? 30 + ($o->readUnsigned8 // return) : $length == 31 ? ($o->readUnsigned64 // return) : $length;
    0 0        
15553 0           my $bytes = $o->readBytes($byteLength);
15554 0 0 0       my $hash = $flags & 0x20 ? $o->{object}->hashAtIndex($o->readUnsigned32 // return) : undef;
15555 0 0         return if $o->{hasError};
15556              
15557             # Children
15558 0           my $child = $record->add($bytes, $hash);
15559 0 0 0       return if $flags & 0x40 && ! $o->readChildren($child);
15560 0 0         return 1 if ! ($flags & 0x80);
15561             }
15562             }
15563              
15564             sub use {
15565 0     0     my $o = shift;
15566 0           my $length = shift;
15567              
15568 0           my $start = $o->{pos};
15569 0           $o->{pos} += $length;
15570 0 0         return substr($o->{data}, $start, $length) if $o->{pos} <= length $o->{data};
15571 0           $o->{hasError} = 1;
15572 0           return;
15573             }
15574              
15575             sub readUnsigned8 {
15576 0     0     my $o = shift;
15577 0   0       unpack('C', $o->use(1) // return) }
15578             sub readUnsigned32 {
15579 0     0     my $o = shift;
15580 0   0       unpack('L>', $o->use(4) // return) }
15581             sub readUnsigned64 {
15582 0     0     my $o = shift;
15583 0   0       unpack('Q>', $o->use(8) // return) }
15584             sub readBytes {
15585 0     0     my $o = shift;
15586 0           my $length = shift;
15587 0           $o->use($length) }
15588             sub trailer {
15589 0     0     my $o = shift;
15590 0           substr($o->{data}, $o->{pos}) }
15591              
15592             package CDS::RecordWriter;
15593              
15594             sub new {
15595 0     0     my $class = shift;
15596              
15597 0           return bless {
15598             hashesCount => 0,
15599             hashes => '',
15600             data => ''
15601             };
15602             }
15603              
15604             sub header {
15605 0     0     my $o = shift;
15606 0           pack('L>', $o->{hashesCount}).$o->{hashes} }
15607 0     0     sub data { shift->{data} }
15608              
15609             sub writeChildren {
15610 0     0     my $o = shift;
15611 0 0 0       my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0            
15612              
15613 0           my @children = @{$record->{children}};
  0            
15614 0 0         return if ! scalar @children;
15615 0           my $lastChild = pop @children;
15616 0           for my $child (@children) { $o->writeNode($child, 1); }
  0            
15617 0           $o->writeNode($lastChild, 0);
15618             }
15619              
15620             sub writeNode {
15621 0     0     my $o = shift;
15622 0 0 0       my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0            
15623 0           my $hasMoreSiblings = shift;
15624              
15625             # Flags
15626 0           my $byteLength = length $record->{bytes};
15627 0 0         my $flags = $byteLength < 30 ? $byteLength : $byteLength < 286 ? 30 : 31;
    0          
15628 0 0         $flags |= 0x20 if defined $record->{hash};
15629 0           my $countChildren = scalar @{$record->{children}};
  0            
15630 0 0         $flags |= 0x40 if $countChildren;
15631 0 0         $flags |= 0x80 if $hasMoreSiblings;
15632 0           $o->writeUnsigned8($flags);
15633              
15634             # Data
15635 0 0         $o->writeUnsigned8($byteLength - 30) if ($flags & 0x1f) == 30;
15636 0 0         $o->writeUnsigned64($byteLength) if ($flags & 0x1f) == 31;
15637 0           $o->writeBytes($record->{bytes});
15638 0 0         $o->writeUnsigned32($o->addHash($record->{hash})) if $flags & 0x20;
15639              
15640             # Children
15641 0           $o->writeChildren($record);
15642             }
15643              
15644             sub writeUnsigned8 {
15645 0     0     my $o = shift;
15646 0           my $value = shift;
15647 0           $o->{data} .= pack('C', $value) }
15648             sub writeUnsigned32 {
15649 0     0     my $o = shift;
15650 0           my $value = shift;
15651 0           $o->{data} .= pack('L>', $value) }
15652             sub writeUnsigned64 {
15653 0     0     my $o = shift;
15654 0           my $value = shift;
15655 0           $o->{data} .= pack('Q>', $value) }
15656              
15657             sub writeBytes {
15658 0     0     my $o = shift;
15659 0           my $bytes = shift;
15660              
15661 0 0         warn $bytes.' is a utf8 string, not a byte string.' if utf8::is_utf8($bytes);
15662 0           $o->{data} .= $bytes;
15663             }
15664              
15665             sub addHash {
15666 0     0     my $o = shift;
15667 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
15668              
15669 0           my $index = $o->{hashesCount};
15670 0           $o->{hashes} .= $hash->bytes;
15671 0           $o->{hashesCount} += 1;
15672 0           return $index;
15673             }
15674              
15675             package CDS::RootDocument;
15676              
15677 1     1   13517 use parent -norequire, 'CDS::Document';
  1         3  
  1         8  
15678              
15679             sub new {
15680 0     0     my $class = shift;
15681 0           my $privateRoot = shift;
15682 0           my $label = shift;
15683              
15684 0           my $o = $class->SUPER::new($privateRoot->privateBoxReader->keyPair, $privateRoot->unsaved);
15685 0           $o->{privateRoot} = $privateRoot;
15686 0           $o->{label} = $label;
15687 0           $privateRoot->addDataHandler($label, $o);
15688              
15689             # State
15690 0           $o->{dataSharingMessage} = undef;
15691 0           return $o;
15692             }
15693              
15694 0     0     sub privateRoot { shift->{privateRoot} }
15695 0     0     sub label { shift->{label} }
15696              
15697             sub savingDone {
15698 0     0     my $o = shift;
15699 0           my $revision = shift;
15700 0           my $newPart = shift;
15701 0           my $obsoleteParts = shift;
15702              
15703 0           $o->{privateRoot}->unsaved->state->merge($o->{unsaved}->savingState);
15704 0           $o->{unsaved}->savingDone;
15705 0 0 0       $o->{privateRoot}->dataChanged if $newPart || scalar @$obsoleteParts;
15706             }
15707              
15708             sub addDataTo {
15709 0     0     my $o = shift;
15710 0 0 0       my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0            
15711              
15712 0           for my $part (sort { $a->{hashAndKey}->hash->bytes cmp $b->{hashAndKey}->hash->bytes } values %{$o->{parts}}) {
  0            
  0            
15713 0           $record->addHashAndKey($part->{hashAndKey});
15714             }
15715             }
15716             sub mergeData {
15717 0     0     my $o = shift;
15718 0 0 0       my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0            
15719              
15720 0           my @hashesAndKeys;
15721 0           for my $child ($record->children) {
15722 0   0       push @hashesAndKeys, $child->asHashAndKey // next;
15723             }
15724              
15725 0           $o->merge(@hashesAndKeys);
15726             }
15727              
15728             sub mergeExternalData {
15729 0     0     my $o = shift;
15730 0           my $store = shift;
15731 0 0 0       my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0            
15732 0 0 0       my $source = shift; die 'wrong type '.ref($source).' for $source' if defined $source && ref $source ne 'CDS::Source';
  0            
15733              
15734 0           my @hashes;
15735             my @hashesAndKeys;
15736 0           for my $child ($record->children) {
15737 0   0       my $hashAndKey = $child->asHashAndKey // next;
15738 0 0         next if $o->{parts}->{$hashAndKey->hash->bytes};
15739 0           push @hashes, $hashAndKey->hash;
15740 0           push @hashesAndKeys, $hashAndKey;
15741             }
15742              
15743 0           my ($missing, $transferStore, $storeError) = $o->{keyPair}->transfer([@hashes], $store, $o->{privateRoot}->unsaved);
15744 0 0         return if defined $storeError;
15745 0 0         return if $missing;
15746              
15747 0 0         if ($source) {
15748 0           $source->keep;
15749 0           $o->{privateRoot}->unsaved->state->addMergedSource($source);
15750             }
15751              
15752 0           $o->merge(@hashesAndKeys);
15753 0           return 1;
15754             }
15755              
15756             package CDS::Selector;
15757              
15758             sub root {
15759 0     0     my $class = shift;
15760 0           my $document = shift;
15761              
15762 0           return bless {document => $document, id => 'ROOT', label => ''};
15763             }
15764              
15765 0     0     sub document { shift->{document} }
15766 0     0     sub parent { shift->{parent} }
15767 0     0     sub label { shift->{label} }
15768              
15769             sub child {
15770 0     0     my $o = shift;
15771 0           my $label = shift;
15772              
15773             return bless {
15774             document => $o->{document},
15775 0           id => $o->{id}.'/'.unpack('H*', $label),
15776             parent => $o,
15777             label => $label,
15778             };
15779             }
15780              
15781             sub childWithText {
15782 0     0     my $o = shift;
15783 0           my $label = shift;
15784              
15785 0   0       return $o->child(Encode::encode_utf8($label // ''));
15786             }
15787              
15788             sub children {
15789 0     0     my $o = shift;
15790              
15791 0   0       my $item = $o->{document}->get($o) // return;
15792 0           return map { $_->{selector} } @{$item->{children}};
  0            
  0            
15793             }
15794              
15795             # Value
15796              
15797             sub revision {
15798 0     0     my $o = shift;
15799              
15800 0   0       my $item = $o->{document}->get($o) // return 0;
15801 0           return $item->{revision};
15802             }
15803              
15804             sub isSet {
15805 0     0     my $o = shift;
15806              
15807 0   0       my $item = $o->{document}->get($o) // return;
15808 0           return scalar $item->{record}->children > 0;
15809             }
15810              
15811             sub record {
15812 0     0     my $o = shift;
15813              
15814 0   0       my $item = $o->{document}->get($o) // return CDS::Record->new;
15815 0           return $item->{record};
15816             }
15817              
15818             sub set {
15819 0     0     my $o = shift;
15820 0 0 0       my $record = shift // return; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0   0        
15821              
15822 0           my $now = CDS->now;
15823 0           my $item = $o->{document}->getOrCreate($o);
15824 0 0         $item->mergeValue($o->{document}->{changes}, $item->{revision} >= $now ? $item->{revision} + 1 : $now, $record);
15825             }
15826              
15827             sub merge {
15828 0     0     my $o = shift;
15829 0           my $revision = shift;
15830 0 0 0       my $record = shift // return; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0   0        
15831              
15832 0           my $item = $o->{document}->getOrCreate($o);
15833 0           return $item->mergeValue($o->{document}->{changes}, $revision, $record);
15834             }
15835              
15836             sub clear {
15837 0     0     my $o = shift;
15838 0           $o->set(CDS::Record->new) }
15839              
15840             sub clearInThePast {
15841 0     0     my $o = shift;
15842              
15843 0 0         $o->merge($o->revision + 1, CDS::Record->new) if $o->isSet;
15844             }
15845              
15846             sub forget {
15847 0     0     my $o = shift;
15848              
15849 0   0       my $item = $o->{document}->get($o) // return;
15850 0           $item->forget;
15851             }
15852              
15853             sub forgetBranch {
15854 0     0     my $o = shift;
15855              
15856 0           for my $child ($o->children) { $child->forgetBranch; }
  0            
15857 0           $o->forget;
15858             }
15859              
15860             # Convenience methods (simple interface)
15861              
15862             sub firstValue {
15863 0     0     my $o = shift;
15864              
15865 0   0       my $item = $o->{document}->get($o) // return CDS::Record->new;
15866 0           return $item->{record}->firstChild;
15867             }
15868              
15869             sub bytesValue {
15870 0     0     my $o = shift;
15871 0           $o->firstValue->bytes }
15872             sub hashValue {
15873 0     0     my $o = shift;
15874 0           $o->firstValue->hash }
15875             sub textValue {
15876 0     0     my $o = shift;
15877 0           $o->firstValue->asText }
15878             sub booleanValue {
15879 0     0     my $o = shift;
15880 0           $o->firstValue->asBoolean }
15881             sub integerValue {
15882 0     0     my $o = shift;
15883 0           $o->firstValue->asInteger }
15884             sub unsignedValue {
15885 0     0     my $o = shift;
15886 0           $o->firstValue->asUnsigned }
15887             sub floatValue {
15888 0     0     my $o = shift;
15889 0           $o->firstValue->asFloat }
15890             sub hashAndKeyValue {
15891 0     0     my $o = shift;
15892 0           $o->firstValue->asHashAndKey }
15893              
15894             # Sets a new value unless the node has that value already.
15895             sub setBytes {
15896 0     0     my $o = shift;
15897 0           my $bytes = shift;
15898 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
15899              
15900 0           my $record = CDS::Record->new;
15901 0           $record->add($bytes, $hash);
15902 0           $o->set($record);
15903             }
15904              
15905             sub setHash {
15906 0     0     my $o = shift;
15907 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
15908 0           $o->setBytes('', $hash); };
15909             sub setText {
15910 0     0     my $o = shift;
15911 0           my $value = shift;
15912 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
15913 0           $o->setBytes(Encode::encode_utf8($value), $hash); };
15914             sub setBoolean {
15915 0     0     my $o = shift;
15916 0           my $value = shift;
15917 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
15918 0           $o->setBytes(CDS->bytesFromBoolean($value), $hash); };
15919             sub setInteger {
15920 0     0     my $o = shift;
15921 0           my $value = shift;
15922 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
15923 0           $o->setBytes(CDS->bytesFromInteger($value), $hash); };
15924             sub setUnsigned {
15925 0     0     my $o = shift;
15926 0           my $value = shift;
15927 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
15928 0           $o->setBytes(CDS->bytesFromUnsigned($value), $hash); };
15929             sub setFloat32 {
15930 0     0     my $o = shift;
15931 0           my $value = shift;
15932 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
15933 0           $o->setBytes(CDS->bytesFromFloat32($value), $hash); };
15934             sub setFloat64 {
15935 0     0     my $o = shift;
15936 0           my $value = shift;
15937 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
15938 0           $o->setBytes(CDS->bytesFromFloat64($value), $hash); };
15939             sub setHashAndKey {
15940 0     0     my $o = shift;
15941 0 0 0       my $hashAndKey = shift; die 'wrong type '.ref($hashAndKey).' for $hashAndKey' if defined $hashAndKey && ref $hashAndKey ne 'CDS::HashAndKey';
  0            
15942 0           $o->setBytes($hashAndKey->key, $hashAndKey->hash); };
15943              
15944             # Adding objects and merged sources
15945              
15946             sub addObject {
15947 0     0     my $o = shift;
15948 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
15949 0 0 0       my $object = shift; die 'wrong type '.ref($object).' for $object' if defined $object && ref $object ne 'CDS::Object';
  0            
15950              
15951 0           $o->{document}->{unsaved}->state->addObject($hash, $object);
15952             }
15953              
15954             sub addMergedSource {
15955 0     0     my $o = shift;
15956 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
15957              
15958 0           $o->{document}->{unsaved}->state->addMergedSource($hash);
15959             }
15960              
15961             package CDS::SentItem;
15962              
15963 1     1   2144 use parent -norequire, 'CDS::UnionList::Item';
  1         2  
  1         4  
15964              
15965             sub new {
15966 0     0     my $class = shift;
15967 0           my $unionList = shift;
15968 0           my $id = shift;
15969              
15970 0           my $o = $class->SUPER::new($unionList, $id);
15971 0           $o->{validUntil} = 0;
15972 0           $o->{message} = CDS::Record->new;
15973 0           return $o;
15974             }
15975              
15976 0     0     sub validUntil { shift->{validUntil} }
15977             sub envelopeHash {
15978 0     0     my $o = shift;
15979 0           CDS::Hash->fromBytes($o->{message}->bytes) }
15980             sub envelopeHashBytes {
15981 0     0     my $o = shift;
15982 0           $o->{message}->bytes }
15983 0     0     sub message { shift->{message} }
15984              
15985             sub addToRecord {
15986 0     0     my $o = shift;
15987 0 0 0       my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0            
15988              
15989 0           $record->add($o->{id})->addInteger($o->{validUntil})->addRecord($o->{message});
15990             }
15991              
15992             sub set {
15993 0     0     my $o = shift;
15994 0           my $validUntil = shift;
15995 0 0 0       my $envelopeHash = shift; die 'wrong type '.ref($envelopeHash).' for $envelopeHash' if defined $envelopeHash && ref $envelopeHash ne 'CDS::Hash';
  0            
15996 0 0 0       my $messageRecord = shift; die 'wrong type '.ref($messageRecord).' for $messageRecord' if defined $messageRecord && ref $messageRecord ne 'CDS::Record';
  0            
15997              
15998 0           my $message = CDS::Record->new($envelopeHash->bytes);
15999 0           $message->addRecord($messageRecord->children);
16000 0           $o->merge($o->{unionList}->{changes}, CDS->max($validUntil, $o->{validUntil} + 1), $message);
16001             }
16002              
16003             sub clear {
16004 0     0     my $o = shift;
16005 0           my $validUntil = shift;
16006              
16007 0           $o->merge($o->{unionList}->{changes}, CDS->max($validUntil, $o->{validUntil} + 1), CDS::Record->new);
16008             }
16009              
16010             sub merge {
16011 0     0     my $o = shift;
16012 0           my $part = shift;
16013 0           my $validUntil = shift;
16014 0           my $message = shift;
16015              
16016 0 0         return if $o->{validUntil} > $validUntil;
16017 0 0 0       return if $o->{validUntil} == $validUntil && $part->{size} < $o->{part}->{size};
16018 0           $o->{validUntil} = $validUntil;
16019 0           $o->{message} = $message;
16020 0           $o->setPart($part);
16021             }
16022              
16023             package CDS::SentList;
16024              
16025 1     1   507 use parent -norequire, 'CDS::UnionList';
  1         2  
  1         4  
16026              
16027             sub new {
16028 0     0     my $class = shift;
16029 0           my $privateRoot = shift;
16030              
16031 0           return $class->SUPER::new($privateRoot, 'sent list');
16032             }
16033              
16034             sub createItem {
16035 0     0     my $o = shift;
16036 0           my $id = shift;
16037              
16038 0           return CDS::SentItem->new($o, $id);
16039             }
16040              
16041             sub mergeRecord {
16042 0     0     my $o = shift;
16043 0           my $part = shift;
16044 0 0 0       my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0            
16045              
16046 0           my $item = $o->getOrCreate($record->bytes);
16047 0           for my $child ($record->children) {
16048 0           my $validUntil = $child->asInteger;
16049 0           my $message = $child->firstChild;
16050 0           $item->merge($part, $validUntil, $message);
16051             }
16052             }
16053              
16054             sub forgetObsoleteItems {
16055 0     0     my $o = shift;
16056              
16057 0           my $now = CDS->now;
16058 0           my $toDelete = [];
16059 0           for my $item (values %{$o->{items}}) {
  0            
16060 0 0         next if $item->{validUntil} >= $now;
16061 0           $o->forgetItem($item);
16062             }
16063             }
16064              
16065             package CDS::Source;
16066              
16067             sub new {
16068 0     0     my $class = shift;
16069 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
16070 0 0 0       my $actorOnStore = shift; die 'wrong type '.ref($actorOnStore).' for $actorOnStore' if defined $actorOnStore && ref $actorOnStore ne 'CDS::ActorOnStore';
  0            
16071 0           my $boxLabel = shift;
16072 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
16073              
16074 0           return bless {
16075             keyPair => $keyPair,
16076             actorOnStore => $actorOnStore,
16077             boxLabel => $boxLabel,
16078             hash => $hash,
16079             referenceCount => 1,
16080             };
16081             }
16082              
16083 0     0     sub keyPair { shift->{keyPair} }
16084 0     0     sub actorOnStore { shift->{actorOnStore} }
16085 0     0     sub boxLabel { shift->{boxLabel} }
16086 0     0     sub hash { shift->{hash} }
16087 0     0     sub referenceCount { shift->{referenceCount} }
16088              
16089             sub keep {
16090 0     0     my $o = shift;
16091              
16092 0 0         if ($o->{referenceCount} < 1) {
16093 0           warn 'The source '.$o->{actorOnStore}->publicKey->hash->hex.'/'.$o->{boxLabel}.'/'.$o->{hash}->hex.' has already been discarded, and cannot be kept any more.';
16094 0           return;
16095             }
16096              
16097 0           $o->{referenceCount} += 1;
16098             }
16099              
16100             sub discard {
16101 0     0     my $o = shift;
16102              
16103 0 0         if ($o->{referenceCount} < 1) {
16104 0           warn 'The source '.$o->{actorOnStore}->publicKey->hash->hex.'/'.$o->{boxLabel}.'/'.$o->{hash}->hex.' has already been discarded, and cannot be discarded again.';
16105 0           return;
16106             }
16107              
16108 0           $o->{referenceCount} -= 1;
16109 0 0         return if $o->{referenceCount} > 0;
16110              
16111 0           $o->{actorOnStore}->store->remove($o->{actorOnStore}->publicKey->hash, $o->{boxLabel}, $o->{hash}, $o->{keyPair});
16112             }
16113              
16114             # A store mapping objects and accounts to a group of stores.
16115             package CDS::SplitStore;
16116              
16117 1     1   670 use parent -norequire, 'CDS::Store';
  1         2  
  1         4  
16118              
16119             sub new {
16120 0     0     my $class = shift;
16121 0           my $key = shift;
16122              
16123 0           return bless {
16124             id => 'Split Store\n'.unpack('H*', CDS::C::aesCrypt(CDS->zeroCTR, $key, CDS->zeroCTR)),
16125             key => $key,
16126             accountStores => [],
16127             objectStores => [],
16128             };
16129             }
16130              
16131 0     0     sub id { shift->{id} }
16132              
16133             ### Store configuration
16134              
16135             sub assignAccounts {
16136 0     0     my $o = shift;
16137 0           my $fromIndex = shift;
16138 0           my $toIndex = shift;
16139 0           my $store = shift;
16140              
16141 0           for my $i ($fromIndex .. $toIndex) {
16142 0           $o->{accountStores}->[$i] = $store;
16143             }
16144             }
16145              
16146             sub assignObjects {
16147 0     0     my $o = shift;
16148 0           my $fromIndex = shift;
16149 0           my $toIndex = shift;
16150 0           my $store = shift;
16151              
16152 0           for my $i ($fromIndex .. $toIndex) {
16153 0           $o->{objectStores}->[$i] = $store;
16154             }
16155             }
16156              
16157             sub objectStore {
16158 0     0     my $o = shift;
16159 0           my $index = shift;
16160 0           $o->{objectStores}->[$index] }
16161             sub accountStore {
16162 0     0     my $o = shift;
16163 0           my $index = shift;
16164 0           $o->{accountStores}->[$index] }
16165              
16166             ### Hash encryption
16167              
16168             our $zeroCounter = "\0" x 16;
16169              
16170             sub storeIndex {
16171 0     0     my $o = shift;
16172 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
16173              
16174             # To avoid attacks on a single store, the hash is encrypted with a key known to the operator only
16175 0           my $encryptedBytes = CDS::C::aesCrypt(substr($hash->bytes, 0, 16), $o->{key}, $zeroCounter);
16176              
16177             # Use the first byte as store index
16178 0           return ord(substr($encryptedBytes, 0, 1));
16179             }
16180              
16181             ### Store interface
16182              
16183             sub get {
16184 0     0     my $o = shift;
16185 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
16186 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
16187              
16188 0   0       my $store = $o->objectStore($o->storeIndex($hash)) // return undef, 'No store assigned.';
16189 0           return $store->get($hash, $keyPair);
16190             }
16191              
16192             sub put {
16193 0     0     my $o = shift;
16194 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
16195 0 0 0       my $object = shift; die 'wrong type '.ref($object).' for $object' if defined $object && ref $object ne 'CDS::Object';
  0            
16196 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
16197              
16198 0   0       my $store = $o->objectStore($o->storeIndex($hash)) // return undef, 'No store assigned.';
16199 0           return $store->put($hash, $object, $keyPair);
16200             }
16201              
16202             sub book {
16203 0     0     my $o = shift;
16204 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
16205 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
16206              
16207 0   0       my $store = $o->objectStore($o->storeIndex($hash)) // return undef, 'No store assigned.';
16208 0           return $store->book($hash, $keyPair);
16209             }
16210              
16211             sub list {
16212 0     0     my $o = shift;
16213 0 0 0       my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0            
16214 0           my $boxLabel = shift;
16215 0           my $timeout = shift;
16216 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
16217              
16218 0   0       my $store = $o->accountStore($o->storeIndex($accountHash)) // return undef, 'No store assigned.';
16219 0           return $store->list($accountHash, $boxLabel, $timeout, $keyPair);
16220             }
16221              
16222             sub add {
16223 0     0     my $o = shift;
16224 0 0 0       my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0            
16225 0           my $boxLabel = shift;
16226 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
16227 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
16228              
16229 0   0       my $store = $o->accountStore($o->storeIndex($accountHash)) // return 'No store assigned.';
16230 0           return $store->add($accountHash, $boxLabel, $hash, $keyPair);
16231             }
16232              
16233             sub remove {
16234 0     0     my $o = shift;
16235 0 0 0       my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0            
16236 0           my $boxLabel = shift;
16237 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
16238 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
16239              
16240 0   0       my $store = $o->accountStore($o->storeIndex($accountHash)) // return 'No store assigned.';
16241 0           return $store->remove($accountHash, $boxLabel, $hash, $keyPair);
16242             }
16243              
16244             sub modify {
16245 0     0     my $o = shift;
16246 0           my $modifications = shift;
16247 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
16248              
16249             # Put objects
16250 0           my %objectsByStoreId;
16251 0           for my $entry (values %{$modifications->objects}) {
  0            
16252 0           my $store = $o->objectStore($o->storeIndex($entry->{hash}));
16253 0           my $target = $objectsByStoreId{$store->id};
16254 0           $objectsByStoreId{$store->id} = $target = {store => $store, modifications => CDS::StoreModifications->new};
16255 0           $target->modifications->put($entry->{hash}, $entry->{object});
16256             }
16257              
16258 0           for my $item (values %objectsByStoreId) {
16259 0           my $error = $item->{store}->modify($item->{modifications}, $keyPair);
16260 0 0         return $error if $error;
16261             }
16262              
16263             # Add box entries
16264 0           my %additionsByStoreId;
16265 0           for my $operation (@{$modifications->additions}) {
  0            
16266 0           my $store = $o->accountStore($o->storeIndex($operation->{accountHash}));
16267 0           my $target = $additionsByStoreId{$store->id};
16268 0           $additionsByStoreId{$store->id} = $target = {store => $store, modifications => CDS::StoreModifications->new};
16269 0           $target->modifications->add($operation->{accountHash}, $operation->{boxLabel}, $operation->{hash});
16270             }
16271              
16272 0           for my $item (values %additionsByStoreId) {
16273 0           my $error = $item->{store}->modify($item->{modifications}, $keyPair);
16274 0 0         return $error if $error;
16275             }
16276              
16277             # Remove box entries (but ignore errors)
16278 0           my %removalsByStoreId;
16279 0           for my $operation (@$modifications->removals) {
16280 0           my $store = $o->accountStore($o->storeIndex($operation->{accountHash}));
16281 0           my $target = $removalsByStoreId{$store->id};
16282 0           $removalsByStoreId{$store->id} = $target = {store => $store, modifications => CDS::StoreModifications->new};
16283 0           $target->modifications->add($operation->{accountHash}, $operation->{boxLabel}, $operation->{hash});
16284             }
16285              
16286 0           for my $item (values %removalsByStoreId) {
16287 0           $item->{store}->modify($item->{modifications}, $keyPair);
16288             }
16289              
16290 0           return;
16291             }
16292              
16293             # General
16294             # sub id($o) # () => String
16295             package CDS::Store;
16296              
16297             # Object store functions
16298             # sub get($o, $hash, $keyPair) # Hash, KeyPair? => Object?, String?
16299             # sub put($o, $hash, $object, $keyPair) # Hash, Object, KeyPair? => String?
16300             # sub book($o, $hash, $keyPair) # Hash, KeyPair? => 1?, String?
16301              
16302             # Account store functions
16303             # sub list($o, $accountHash, $boxLabel, $timeout, $keyPair) # Hash, String, Duration, KeyPair? => @$Hash, String?
16304             # sub add($o, $accountHash, $boxLabel, $hash, $keyPair) # Hash, String, Hash, KeyPair? => String?
16305             # sub remove($o, $accountHash, $boxLabel, $hash, $keyPair) # Hash, String, Hash, KeyPair? => String?
16306             # sub modify($o, $storeModifications, $keyPair) # StoreModifications, KeyPair? => String?
16307              
16308             package CDS::StoreModifications;
16309              
16310             sub new {
16311 0     0     my $class = shift;
16312              
16313 0           return bless {
16314             objects => {},
16315             additions => [],
16316             removals => [],
16317             };
16318             }
16319              
16320 0     0     sub objects { shift->{objects} }
16321 0     0     sub additions { shift->{additions} }
16322 0     0     sub removals { shift->{removals} }
16323              
16324             sub isEmpty {
16325 0     0     my $o = shift;
16326              
16327 0 0         return if scalar keys %{$o->{objects}};
  0            
16328 0 0         return if scalar @{$o->{additions}};
  0            
16329 0 0         return if scalar @{$o->{removals}};
  0            
16330 0           return 1;
16331             }
16332              
16333             sub put {
16334 0     0     my $o = shift;
16335 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
16336 0 0 0       my $object = shift; die 'wrong type '.ref($object).' for $object' if defined $object && ref $object ne 'CDS::Object';
  0            
16337              
16338 0           $o->{objects}->{$hash->bytes} = {hash => $hash, object => $object};
16339             }
16340              
16341             sub add {
16342 0     0     my $o = shift;
16343 0 0 0       my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0            
16344 0           my $boxLabel = shift;
16345 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
16346 0 0 0       my $object = shift; die 'wrong type '.ref($object).' for $object' if defined $object && ref $object ne 'CDS::Object';
  0            
16347              
16348 0 0         $o->put($hash, $object) if $object;
16349 0           push @{$o->{additions}}, {accountHash => $accountHash, boxLabel => $boxLabel, hash => $hash};
  0            
16350             }
16351              
16352             sub remove {
16353 0     0     my $o = shift;
16354 0 0 0       my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0            
16355 0           my $boxLabel = shift;
16356 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
16357              
16358 0           push @{$o->{removals}}, {accountHash => $accountHash, boxLabel => $boxLabel, hash => $hash};
  0            
16359             }
16360              
16361             sub executeIndividually {
16362 0     0     my $o = shift;
16363 0           my $store = shift;
16364 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
16365              
16366             # Process objects
16367 0           for my $entry (values %{$o->{objects}}) {
  0            
16368 0           my $error = $store->put($entry->{hash}, $entry->{object}, $keyPair);
16369 0 0         return $error if $error;
16370             }
16371              
16372             # Process additions
16373 0           for my $entry (@{$o->{additions}}) {
  0            
16374 0           my $error = $store->add($entry->{accountHash}, $entry->{boxLabel}, $entry->{hash}, $keyPair);
16375 0 0         return $error if $error;
16376             }
16377              
16378             # Process removals (and ignore errors)
16379 0           for my $entry (@{$o->{removals}}) {
  0            
16380 0           $store->remove($entry->{accountHash}, $entry->{boxLabel}, $entry->{hash}, $keyPair);
16381             }
16382              
16383 0           return;
16384             }
16385              
16386             # Returns a text representation of box additions and removals.
16387             sub toRecord {
16388 0     0     my $o = shift;
16389              
16390 0           my $record = CDS::Record->new;
16391              
16392             # Objects
16393 0           my $objectsRecord = $record->add('put');
16394 0           for my $entry (values %{$o->{objects}}) {
  0            
16395 0           $objectsRecord->add($entry->{hash}->bytes)->add($entry->{object}->bytes);
16396             }
16397              
16398             # Box additions and removals
16399 0           &addEntriesToRecord($o->{additions}, $record->add('add'));
16400 0           &addEntriesToRecord($o->{removals}, $record->add('remove'));
16401              
16402 0           return $record;
16403             }
16404              
16405             sub addEntriesToRecord {
16406 0     0     my $unsortedEntries = shift;
16407 0 0 0       my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0            
16408             # private
16409 0 0         my @additions = sort { ($a->{accountHash}->bytes cmp $b->{accountHash}->bytes) || ($a->{boxLabel} cmp $b->{boxLabel}) } @$unsortedEntries;
  0            
16410 0           my $entry = shift @additions;
16411 0           while (defined $entry) {
16412 0           my $accountHash = $entry->{accountHash};
16413 0           my $accountRecord = $record->add($accountHash->bytes);
16414              
16415 0   0       while (defined $entry && $entry->{accountHash}->bytes eq $accountHash->bytes) {
16416 0           my $boxLabel = $entry->{boxLabel};
16417 0           my $boxRecord = $accountRecord->add($boxLabel);
16418              
16419 0   0       while (defined $entry && $entry->{boxLabel} eq $boxLabel) {
16420 0           $boxRecord->add($entry->{hash}->bytes);
16421 0           $entry = shift @additions;
16422             }
16423             }
16424             }
16425             }
16426              
16427             sub fromBytes {
16428 0     0     my $class = shift;
16429 0           my $bytes = shift;
16430              
16431 0   0       my $object = CDS::Object->fromBytes($bytes) // return;
16432 0   0       my $record = CDS::Record->fromObject($object) // return;
16433 0           return $class->fromRecord($record);
16434             }
16435              
16436             sub fromRecord {
16437 0     0     my $class = shift;
16438 0 0 0       my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0            
16439              
16440 0           my $modifications = $class->new;
16441              
16442             # Read objects (and "envelopes" entries used before 2022-01)
16443 0           for my $objectRecord ($record->child('put')->children, $record->child('envelopes')->children) {
16444 0   0       my $hash = CDS::Hash->fromBytes($objectRecord->bytes) // return;
16445 0   0       my $object = CDS::Object->fromBytes($objectRecord->firstChild->bytes) // return;
16446             #return if $o->{checkEnvelopeHash} && ! $object->calculateHash->equals($hash);
16447 0           $modifications->put($hash, $object);
16448             }
16449              
16450             # Read additions and removals
16451 0   0       &readEntriesFromRecord($modifications->{additions}, $record->child('add')) // return;
16452 0   0       &readEntriesFromRecord($modifications->{removals}, $record->child('remove')) // return;
16453              
16454 0           return $modifications;
16455             }
16456              
16457             sub readEntriesFromRecord {
16458 0     0     my $entries = shift;
16459 0 0 0       my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0            
16460             # private
16461 0           for my $accountHashRecord ($record->children) {
16462 0   0       my $accountHash = CDS::Hash->fromBytes($accountHashRecord->bytes) // return;
16463 0           for my $boxLabelRecord ($accountHashRecord->children) {
16464 0           my $boxLabel = $boxLabelRecord->bytes;
16465 0 0         return if ! CDS->isValidBoxLabel($boxLabel);
16466              
16467 0           for my $hashRecord ($boxLabelRecord->children) {
16468 0   0       my $hash = CDS::Hash->fromBytes($hashRecord->bytes) // return;
16469 0           push @$entries, {accountHash => $accountHash, boxLabel => $boxLabel, hash => $hash};
16470             }
16471             }
16472             }
16473              
16474 0           return 1;
16475             }
16476              
16477             package CDS::StreamCache;
16478              
16479             sub new {
16480 0     0     my $class = shift;
16481 0           my $pool = shift;
16482 0 0 0       my $actorOnStore = shift; die 'wrong type '.ref($actorOnStore).' for $actorOnStore' if defined $actorOnStore && ref $actorOnStore ne 'CDS::ActorOnStore';
  0            
16483 0           my $timeout = shift;
16484              
16485 0           return bless {
16486             pool => $pool,
16487             actorOnStore => $actorOnStore,
16488             timeout => $timeout,
16489             cache => {},
16490             };
16491             }
16492              
16493 0     0     sub messageBoxReader { shift->{messageBoxReader} }
16494              
16495             sub removeObsolete {
16496 0     0     my $o = shift;
16497              
16498 0           my $limit = CDS->now - $o->{timeout};
16499 0           for my $key (%{$o->{knownStreamHeads}}) {
  0            
16500 0   0       my $streamHead = $o->{knownStreamHeads}->{$key} // next;
16501 0 0         next if $streamHead->lastUsed < $limit;
16502 0           delete $o->{knownStreamHeads}->{$key};
16503             }
16504             }
16505              
16506             sub readStreamHead {
16507 0     0     my $o = shift;
16508 0           my $head = shift;
16509              
16510 0           my $streamHead = $o->{knownStreamHeads}->{$head->hex};
16511 0 0         if ($streamHead) {
16512 0           $streamHead->stillInUse;
16513 0           return $streamHead;
16514             }
16515              
16516             # Retrieve the head envelope
16517 0           my ($object, $getError) = $o->{actorOnStore}->store->get($head, $o->{pool}->{keyPair});
16518 0 0         return if defined $getError;
16519              
16520             # Parse the head envelope
16521 0           my $envelope = CDS::Record->fromObject($object);
16522 0 0         return $o->invalid($head, 'Not a record.') if ! $envelope;
16523              
16524             # Read the embedded content object
16525 0           my $encryptedBytes = $envelope->child('content')->bytesValue;
16526 0 0         return $o->invalid($head, 'Missing content object.') if ! length $encryptedBytes;
16527              
16528             # Decrypt the key
16529 0           my $aesKey = $o->{pool}->{keyPair}->decryptKeyOnEnvelope($envelope);
16530 0 0         return $o->invalid($head, 'Not encrypted for us.') if ! $aesKey;
16531              
16532             # Decrypt the content
16533 0           my $contentObject = CDS::Object->fromBytes(CDS::C::aesCrypt($encryptedBytes, $aesKey, CDS->zeroCTR));
16534 0 0         return $o->invalid($head, 'Invalid content object.') if ! $contentObject;
16535              
16536 0           my $content = CDS::Record->fromObject($contentObject);
16537 0 0         return $o->invalid($head, 'Content object is not a record.') if ! $content;
16538              
16539             # Verify the sender hash
16540 0           my $senderHash = $content->child('sender')->hashValue;
16541 0 0         return $o->invalid($head, 'Missing sender hash.') if ! $senderHash;
16542              
16543             # Verify the sender store
16544 0           my $storeRecord = $content->child('store');
16545 0 0         return $o->invalid($head, 'Missing sender store.') if ! scalar $storeRecord->children;
16546              
16547 0           my $senderStoreUrl = $storeRecord->textValue;
16548 0           my $senderStore = $o->{pool}->{delegate}->onMessageBoxVerifyStore($senderStoreUrl, $head, $envelope, $senderHash);
16549 0 0         return $o->invalid($head, 'Invalid sender store.') if ! $senderStore;
16550              
16551             # Retrieve the sender's public key
16552 0           my ($senderPublicKey, $invalidReason, $publicKeyStoreError) = $o->getPublicKey($senderHash, $senderStore);
16553 0 0         return if defined $publicKeyStoreError;
16554 0 0         return $o->invalid($head, 'Failed to retrieve the sender\'s public key: '.$invalidReason) if defined $invalidReason;
16555              
16556             # Verify the signature
16557 0           my $signedHash = CDS::Hash->calculateFor($encryptedBytes);
16558 0 0         return $o->invalid($head, 'Invalid signature.') if ! CDS->verifyEnvelopeSignature($envelope, $senderPublicKey, $signedHash);
16559              
16560             # The envelope is valid
16561 0           my $sender = CDS::ActorOnStore->new($senderPublicKey, $senderStore);
16562 0           my $newStreamHead = CDS::StreamHead->new($head, $envelope, $senderStoreUrl, $sender, $aesKey, $content);
16563 0           $o->{knownStreamHeads}->{$head->hex} = $newStreamHead;
16564 0           return $newStreamHead;
16565             }
16566              
16567             sub invalid {
16568 0     0     my $o = shift;
16569 0           my $head = shift;
16570 0           my $reason = shift;
16571             # private
16572 0           my $newStreamHead = CDS::StreamHead->new($head, undef, undef, undef, undef, undef, $reason);
16573 0           $o->{knownStreamHeads}->{$head->hex} = $newStreamHead;
16574 0           return $newStreamHead;
16575             }
16576              
16577             package CDS::StreamHead;
16578              
16579             sub new {
16580 0     0     my $class = shift;
16581 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
16582 0 0 0       my $envelope = shift; die 'wrong type '.ref($envelope).' for $envelope' if defined $envelope && ref $envelope ne 'CDS::Record';
  0            
16583 0           my $senderStoreUrl = shift;
16584 0           my $sender = shift;
16585 0           my $content = shift;
16586 0           my $error = shift;
16587              
16588 0           return bless {
16589             hash => $hash,
16590             envelope => $envelope,
16591             senderStoreUrl => $senderStoreUrl,
16592             sender => $sender,
16593             content => $content,
16594             error => $error,
16595             lastUsed => CDS->now,
16596             };
16597             }
16598              
16599 0     0     sub hash { shift->{hash} }
16600 0     0     sub envelope { shift->{envelope} }
16601 0     0     sub senderStoreUrl { shift->{senderStoreUrl} }
16602 0     0     sub sender { shift->{sender} }
16603 0     0     sub content { shift->{content} }
16604 0     0     sub error { shift->{error} }
16605             sub isValid {
16606 0     0     my $o = shift;
16607 0           ! defined $o->{error} }
16608 0     0     sub lastUsed { shift->{lastUsed} }
16609              
16610             sub stillInUse {
16611 0     0     my $o = shift;
16612              
16613 0           $o->{lastUsed} = CDS->now;
16614             }
16615              
16616             package CDS::SubDocument;
16617              
16618 1     1   3451 use parent -norequire, 'CDS::Document';
  1         2  
  1         4  
16619              
16620             sub new {
16621 0     0     my $class = shift;
16622 0 0 0       my $parentSelector = shift; die 'wrong type '.ref($parentSelector).' for $parentSelector' if defined $parentSelector && ref $parentSelector ne 'CDS::Selector';
  0            
16623              
16624 0           my $o = $class->SUPER::new($parentSelector->document->keyPair, $parentSelector->document->unsaved);
16625 0           $o->{parentSelector} = $parentSelector;
16626 0           return $o;
16627             }
16628              
16629 0     0     sub parentSelector { shift->{parentSelector} }
16630              
16631             sub partSelector {
16632 0     0     my $o = shift;
16633 0 0 0       my $hashAndKey = shift; die 'wrong type '.ref($hashAndKey).' for $hashAndKey' if defined $hashAndKey && ref $hashAndKey ne 'CDS::HashAndKey';
  0            
16634              
16635 0           $o->{parentSelector}->child(substr($hashAndKey->hash->bytes, 0, 16));
16636             }
16637              
16638             sub read {
16639 0     0     my $o = shift;
16640              
16641 0           $o->merge(map { $_->hashAndKeyValue } $o->{parentSelector}->children);
  0            
16642 0           return $o->SUPER::read;
16643             }
16644              
16645             sub savingDone {
16646 0     0     my $o = shift;
16647 0           my $revision = shift;
16648 0           my $newPart = shift;
16649 0           my $obsoleteParts = shift;
16650              
16651 0           $o->{parentSelector}->document->unsaved->state->merge($o->{unsaved}->savingState);
16652              
16653             # Remove obsolete parts
16654 0           for my $part (@$obsoleteParts) {
16655 0           $o->partSelector($part->{hashAndKey})->merge($revision, CDS::Record->new);
16656             }
16657              
16658             # Add the new part
16659 0 0         if ($newPart) {
16660 0           my $record = CDS::Record->new;
16661 0           $record->addHashAndKey($newPart->{hashAndKey});
16662 0           $o->partSelector($newPart->{hashAndKey})->merge($revision, $record);
16663             }
16664              
16665 0           $o->{unsaved}->savingDone;
16666             }
16667              
16668             # Useful functions to display textual information on the terminal
16669             package CDS::UI;
16670              
16671             sub new {
16672 0     0     my $class = shift;
16673 0   0       my $fileHandle = shift // *STDOUT;
16674 0           my $pure = shift;
16675              
16676 0           binmode $fileHandle, ":utf8";
16677 0           return bless {
16678             fileHandle => $fileHandle,
16679             pure => $pure,
16680             indentCount => 0,
16681             indent => '',
16682             valueIndent => 16,
16683             hasSpace => 0,
16684             hasError => 0,
16685             hasWarning => 0,
16686             };
16687             }
16688              
16689 0     0     sub fileHandle { shift->{fileHandle} }
16690              
16691             ### Indent
16692              
16693             sub pushIndent {
16694 0     0     my $o = shift;
16695              
16696 0           $o->{indentCount} += 1;
16697 0           $o->{indent} = ' ' x $o->{indentCount};
16698 0           return;
16699             }
16700              
16701             sub popIndent {
16702 0     0     my $o = shift;
16703              
16704 0           $o->{indentCount} -= 1;
16705 0           $o->{indent} = ' ' x $o->{indentCount};
16706 0           return;
16707             }
16708              
16709             sub valueIndent {
16710 0     0     my $o = shift;
16711 0           my $width = shift;
16712              
16713 0           $o->{valueIndent} = $width;
16714             }
16715              
16716             ### Low-level (non-semantic) output
16717              
16718             sub print {
16719 0     0     my $o = shift;
16720              
16721 0   0       my $fh = $o->{fileHandle} // return;
16722 0           print $fh @_;
16723             }
16724              
16725             sub raw {
16726 0     0     my $o = shift;
16727              
16728 0           $o->removeProgress;
16729 0   0       my $fh = $o->{fileHandle} // return;
16730 0           binmode $fh, ":bytes";
16731 0           print $fh @_;
16732 0           binmode $fh, ":utf8";
16733 0           $o->{hasSpace} = 0;
16734 0           return;
16735             }
16736              
16737             sub space {
16738 0     0     my $o = shift;
16739              
16740 0           $o->removeProgress;
16741 0 0         return if $o->{hasSpace};
16742 0           $o->{hasSpace} = 1;
16743 0           $o->print("\n");
16744 0           return;
16745             }
16746              
16747             # A line of text (without word-wrap).
16748             sub line {
16749 0     0     my $o = shift;
16750              
16751 0           $o->removeProgress;
16752 0           my $span = CDS::UI::Span->new(@_);
16753 0           $o->print($o->{indent});
16754 0           $span->printTo($o);
16755 0           $o->print(chr(0x1b), '[0m', "\n");
16756 0           $o->{hasSpace} = 0;
16757 0           return;
16758             }
16759              
16760             # A line of word-wrapped text.
16761             sub p {
16762 0     0     my $o = shift;
16763              
16764 0           $o->removeProgress;
16765 0           my $span = CDS::UI::Span->new(@_);
16766 0           $span->wordWrap({lineLength => 0, maxLength => 100 - length $o->{indent}, indent => $o->{indent}});
16767 0           $o->print($o->{indent});
16768 0           $span->printTo($o);
16769 0           $o->print(chr(0x1b), '[0m', "\n");
16770 0           $o->{hasSpace} = 0;
16771 0           return;
16772             }
16773              
16774             # Line showing the progress.
16775             sub progress {
16776 0     0     my $o = shift;
16777              
16778 0 0         return if $o->{pure};
16779 0           $| = 1;
16780 0           $o->{hasProgress} = 1;
16781 0           my $text = ' '.join('', @_);
16782 0 0         $text = substr($text, 0, 79).'…' if length $text > 80;
16783 0 0         $text .= ' ' x (80 - length $text) if length $text < 80;
16784 0           $o->print($text, "\r");
16785             }
16786              
16787             # Progress line removal.
16788             sub removeProgress {
16789 0     0     my $o = shift;
16790              
16791 0 0         return if $o->{pure};
16792 0 0         return if ! $o->{hasProgress};
16793 0           $o->print(' ' x 80, "\r");
16794 0           $o->{hasProgress} = 0;
16795 0           $| = 0;
16796             }
16797              
16798             ### Low-level (non-semantic) formatting
16799              
16800             sub span {
16801 0     0     my $o = shift;
16802 0           CDS::UI::Span->new(@_) }
16803              
16804             sub bold {
16805 0     0     my $o = shift;
16806              
16807 0           my $span = CDS::UI::Span->new(@_);
16808 0           $span->{bold} = 1;
16809 0           return $span;
16810             }
16811              
16812             sub underlined {
16813 0     0     my $o = shift;
16814              
16815 0           my $span = CDS::UI::Span->new(@_);
16816 0           $span->{underlined} = 1;
16817 0           return $span;
16818             }
16819              
16820             sub foreground {
16821 0     0     my $o = shift;
16822 0           my $foreground = shift;
16823              
16824 0           my $span = CDS::UI::Span->new(@_);
16825 0           $span->{foreground} = $foreground;
16826 0           return $span;
16827             }
16828              
16829             sub background {
16830 0     0     my $o = shift;
16831 0           my $background = shift;
16832              
16833 0           my $span = CDS::UI::Span->new(@_);
16834 0           $span->{background} = $background;
16835 0           return $span;
16836             }
16837              
16838             sub red {
16839 0     0     my $o = shift;
16840 0           $o->foreground(196, @_) } # for failure
16841             sub green {
16842 0     0     my $o = shift;
16843 0           $o->foreground(40, @_) } # for success
16844             sub orange {
16845 0     0     my $o = shift;
16846 0           $o->foreground(166, @_) } # for warnings
16847             sub blue {
16848 0     0     my $o = shift;
16849 0           $o->foreground(33, @_) } # to highlight something (selection)
16850             sub violet {
16851 0     0     my $o = shift;
16852 0           $o->foreground(93, @_) } # to highlight something (selection)
16853             sub gold {
16854 0     0     my $o = shift;
16855 0           $o->foreground(238, @_) } # for commands that can be executed
16856             sub gray {
16857 0     0     my $o = shift;
16858 0           $o->foreground(246, @_) } # for additional (less important) information
16859              
16860             sub darkBold {
16861 0     0     my $o = shift;
16862              
16863 0           my $span = CDS::UI::Span->new(@_);
16864 0           $span->{bold} = 1;
16865 0           $span->{foreground} = 240;
16866 0           return $span;
16867             }
16868              
16869             ### Semantic output
16870              
16871             sub title {
16872 0     0     my $o = shift;
16873 0           $o->line($o->bold(@_)) }
16874              
16875             sub left {
16876 0     0     my $o = shift;
16877 0           my $width = shift;
16878 0           my $text = shift;
16879              
16880 0 0         return substr($text, 0, $width - 1).'…' if length $text > $width;
16881 0           return $text . ' ' x ($width - length $text);
16882             }
16883              
16884             sub right {
16885 0     0     my $o = shift;
16886 0           my $width = shift;
16887 0           my $text = shift;
16888              
16889 0 0         return substr($text, 0, $width - 1).'…' if length $text > $width;
16890 0           return ' ' x ($width - length $text) . $text;
16891             }
16892              
16893             sub keyValue {
16894 0     0     my $o = shift;
16895 0           my $key = shift;
16896 0           my $firstLine = shift;
16897              
16898 0           my $indent = $o->{valueIndent} - length $o->{indent};
16899 0 0 0       $key = substr($key, 0, $indent - 2).'…' if defined $firstLine && length $key >= $indent;
16900 0           $key .= ' ' x ($indent - length $key);
16901 0           $o->line($o->gray($key), $firstLine);
16902 0           my $noKey = ' ' x $indent;
16903 0           for my $line (@_) { $o->line($noKey, $line); }
  0            
16904 0           return;
16905             }
16906              
16907             sub command {
16908 0     0     my $o = shift;
16909 0           $o->line($o->bold(@_)) }
16910              
16911             sub verbose {
16912 0     0     my $o = shift;
16913 0 0         $o->line($o->foreground(45, @_)) if $o->{verbose} }
16914              
16915             sub pGreen {
16916 0     0     my $o = shift;
16917              
16918 0           $o->p($o->green(@_));
16919 0           return;
16920             }
16921              
16922             sub pOrange {
16923 0     0     my $o = shift;
16924              
16925 0           $o->p($o->orange(@_));
16926 0           return;
16927             }
16928              
16929             sub pRed {
16930 0     0     my $o = shift;
16931              
16932 0           $o->p($o->red(@_));
16933 0           return;
16934             }
16935              
16936             ### Warnings and errors
16937              
16938 0     0     sub hasWarning { shift->{hasWarning} }
16939 0     0     sub hasError { shift->{hasError} }
16940              
16941             sub warning {
16942 0     0     my $o = shift;
16943              
16944 0           $o->{hasWarning} = 1;
16945 0           $o->p($o->orange(@_));
16946 0           return;
16947             }
16948              
16949             sub error {
16950 0     0     my $o = shift;
16951              
16952 0           $o->{hasError} = 1;
16953 0           my $span = CDS::UI::Span->new(@_);
16954 0           $span->{background} = 196;
16955 0           $span->{foreground} = 15;
16956 0           $span->{bold} = 1;
16957 0           $o->line($span);
16958 0           return;
16959             }
16960              
16961             ### Semantic formatting
16962              
16963             sub a {
16964 0     0     my $o = shift;
16965 0           $o->underlined(@_) }
16966              
16967             ### Human readable formats
16968              
16969             sub niceBytes {
16970 0     0     my $o = shift;
16971 0           my $bytes = shift;
16972 0           my $maxLength = shift;
16973              
16974 0           my $length = length $bytes;
16975 0 0 0       my $text = defined $maxLength && $length > $maxLength ? substr($bytes, 0, $maxLength - 1).'…' : $bytes;
16976 0           $text =~ s/[\x00-\x1f\x7f-\xff]/./g;
16977 0           return $text;
16978             }
16979              
16980             sub niceFileSize {
16981 0     0     my $o = shift;
16982 0           my $fileSize = shift;
16983              
16984 0 0         return $fileSize.' bytes' if $fileSize < 1000;
16985 0 0         return sprintf('%0.1f', $fileSize / 1000).' KB' if $fileSize < 10000;
16986 0 0         return sprintf('%0.0f', $fileSize / 1000).' KB' if $fileSize < 1000000;
16987 0 0         return sprintf('%0.1f', $fileSize / 1000000).' MB' if $fileSize < 10000000;
16988 0 0         return sprintf('%0.0f', $fileSize / 1000000).' MB' if $fileSize < 1000000000;
16989 0 0         return sprintf('%0.1f', $fileSize / 1000000000).' GB' if $fileSize < 10000000000;
16990 0           return sprintf('%0.0f', $fileSize / 1000000000).' GB';
16991             }
16992              
16993             sub niceDateTimeLocal {
16994 0     0     my $o = shift;
16995 0   0       my $time = shift // time() * 1000;
16996              
16997 0           my @t = localtime($time / 1000);
16998 0           return sprintf('%04d-%02d-%02d %02d:%02d:%02d', $t[5] + 1900, $t[4] + 1, $t[3], $t[2], $t[1], $t[0]);
16999             }
17000              
17001             sub niceDateTime {
17002 0     0     my $o = shift;
17003 0   0       my $time = shift // time() * 1000;
17004              
17005 0           my @t = gmtime($time / 1000);
17006 0           return sprintf('%04d-%02d-%02d %02d:%02d:%02d UTC', $t[5] + 1900, $t[4] + 1, $t[3], $t[2], $t[1], $t[0]);
17007             }
17008              
17009             sub niceDate {
17010 0     0     my $o = shift;
17011 0   0       my $time = shift // time() * 1000;
17012              
17013 0           my @t = gmtime($time / 1000);
17014 0           return sprintf('%04d-%02d-%02d', $t[5] + 1900, $t[4] + 1, $t[3]);
17015             }
17016              
17017             sub niceTime {
17018 0     0     my $o = shift;
17019 0   0       my $time = shift // time() * 1000;
17020              
17021 0           my @t = gmtime($time / 1000);
17022 0           return sprintf('%02d:%02d:%02d UTC', $t[2], $t[1], $t[0]);
17023             }
17024              
17025             ### Special output
17026              
17027             sub record {
17028 0     0     my $o = shift;
17029 0 0 0       my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0            
17030 0           my $storeUrl = shift;
17031 0           CDS::UI::Record->display($o, $record, $storeUrl) }
17032              
17033             sub recordChildren {
17034 0     0     my $o = shift;
17035 0 0 0       my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0            
17036 0           my $storeUrl = shift;
17037              
17038 0           for my $child ($record->children) {
17039 0           CDS::UI::Record->display($o, $child, $storeUrl);
17040             }
17041             }
17042              
17043             sub selector {
17044 0     0     my $o = shift;
17045 0 0 0       my $selector = shift; die 'wrong type '.ref($selector).' for $selector' if defined $selector && ref $selector ne 'CDS::Selector';
  0            
17046 0           my $rootLabel = shift;
17047              
17048 0           my $item = $selector->document->get($selector);
17049 0 0         my $revision = $item->{revision} ? $o->green(' ', $o->niceDateTime($item->{revision})) : '';
17050              
17051 0 0         if ($selector->{id} eq 'ROOT') {
17052 0   0       $o->line($o->bold($rootLabel // 'Data tree'), $revision);
17053 0           $o->recordChildren($selector->record);
17054 0           $o->selectorChildren($selector);
17055             } else {
17056 0           my $label = $selector->label;
17057 0 0         my $labelText = length $label > 64 ? substr($label, 0, 64).'…' : $label;
17058 0           $labelText =~ s/[\x00-\x1f\x7f-\xff]/·/g;
17059 0           $o->line($o->blue($labelText), $revision);
17060              
17061 0           $o->pushIndent;
17062 0           $o->recordChildren($selector->record);
17063 0           $o->selectorChildren($selector);
17064 0           $o->popIndent;
17065             }
17066             }
17067              
17068             sub selectorChildren {
17069 0     0     my $o = shift;
17070 0 0 0       my $selector = shift; die 'wrong type '.ref($selector).' for $selector' if defined $selector && ref $selector ne 'CDS::Selector';
  0            
17071              
17072 0           for my $child (sort { $a->{id} cmp $b->{id} } $selector->children) {
  0            
17073 0           $o->selector($child);
17074             }
17075             }
17076              
17077             sub hexDump {
17078 0     0     my $o = shift;
17079 0           my $bytes = shift;
17080 0           CDS::UI::HexDump->new($o, $bytes) }
17081              
17082             package CDS::UI::HexDump;
17083              
17084             sub new {
17085 0     0     my $class = shift;
17086 0           my $ui = shift;
17087 0           my $bytes = shift;
17088              
17089 0           return bless {ui => $ui, bytes => $bytes, styleChanges => [], };
17090             }
17091              
17092 0     0     sub reset { chr(0x1b).'[0m' }
17093             sub foreground {
17094 0     0     my $o = shift;
17095 0           my $color = shift;
17096 0           chr(0x1b).'[0;38;5;'.$color.'m' }
17097              
17098             sub changeStyle {
17099 0     0     my $o = shift;
17100              
17101 0           push @{$o->{styleChanges}}, @_;
  0            
17102             }
17103              
17104             sub styleHashList {
17105 0     0     my $o = shift;
17106 0           my $offset = shift;
17107              
17108 0           my $hashesCount = unpack('L>', substr($o->{bytes}, $offset, 4));
17109 0           my $dataStart = $offset + 4 + $hashesCount * 32;
17110 0 0         return $offset if $dataStart > length $o->{bytes};
17111              
17112             # Styles
17113 0           my $darkGreen = $o->foreground(28);
17114 0           my $green0 = $o->foreground(40);
17115 0           my $green1 = $o->foreground(34);
17116              
17117             # Color the hash count
17118 0           my $pos = $offset;
17119 0           $o->changeStyle({at => $pos, style => $darkGreen, breakBefore => 1});
17120 0           $pos += 4;
17121              
17122             # Color the hashes
17123 0           my $alternate = 0;
17124 0           while ($hashesCount) {
17125 0 0         $o->changeStyle({at => $pos, style => $alternate ? $green1 : $green0, breakBefore => 1});
17126 0           $pos += 32;
17127 0           $alternate = 1 - $alternate;
17128 0           $hashesCount -= 1;
17129             }
17130              
17131 0           return $dataStart;
17132             }
17133              
17134             sub styleRecord {
17135 0     0     my $o = shift;
17136 0           my $offset = shift;
17137              
17138             # Styles
17139 0           my $blue = $o->foreground(33);
17140 0           my $black = $o->reset;
17141 0           my $violet = $o->foreground(93);
17142 0           my @styleChanges;
17143              
17144             # Prepare
17145 0           my $pos = $offset;
17146 0           my $hasError = 0;
17147 0           my $level = 0;
17148              
17149 0     0     my $use = sub { my $length = shift;
17150 0           my $start = $pos;
17151 0           $pos += $length;
17152 0 0         return substr($o->{bytes}, $start, $length) if $pos <= length $o->{bytes};
17153 0           $hasError = 1;
17154 0           return;
17155 0           };
17156              
17157 0   0 0     my $readUnsigned8 = sub { unpack('C', &$use(1) // return) };
  0            
17158 0   0 0     my $readUnsigned32 = sub { unpack('L>', &$use(4) // return) };
  0            
17159 0   0 0     my $readUnsigned64 = sub { unpack('Q>', &$use(8) // return) };
  0            
17160              
17161             # Parse all record nodes
17162 0           while ($level >= 0) {
17163             # Flags
17164 0           push @styleChanges, {at => $pos, style => $blue, breakBefore => 1};
17165 0   0       my $flags = &$readUnsigned8 // last;
17166              
17167             # Data
17168 0           my $length = $flags & 0x1f;
17169 0 0 0       my $byteLength = $length == 30 ? 30 + (&$readUnsigned8 // last) : $length == 31 ? (&$readUnsigned64 // last) : $length;
    0 0        
17170              
17171 0 0         if ($byteLength) {
17172 0           push @styleChanges, {at => $pos, style => $black};
17173 0   0       &$use($byteLength) // last;
17174             }
17175              
17176 0 0         if ($flags & 0x20) {
17177 0           push @styleChanges, {at => $pos, style => $violet};
17178 0   0       &$readUnsigned32 // last;
17179             }
17180              
17181             # Children
17182 0 0         $level += 1 if $flags & 0x40;
17183 0 0         $level -= 1 if ! ($flags & 0x80);
17184             }
17185              
17186             # Don't apply any styles if there are errors
17187 0 0         $hasError = 1 if $pos != length $o->{bytes};
17188 0 0         return $offset if $hasError;
17189              
17190 0           $o->changeStyle(@styleChanges);
17191 0           return $pos;
17192             }
17193              
17194             sub display {
17195 0     0     my $o = shift;
17196              
17197 0           $o->{ui}->valueIndent(8);
17198              
17199 0           my $resetStyle = chr(0x1b).'[0m';
17200 0           my $length = length($o->{bytes});
17201 0           my $lineStart = 0;
17202 0           my $currentStyle = '';
17203              
17204 0           my @styleChanges = sort { $a->{at} <=> $b->{at} } @{$o->{styleChanges}};
  0            
  0            
17205 0           push @styleChanges, {at => $length};
17206 0           my $nextChange = shift(@styleChanges);
17207              
17208 0           $o->{ui}->line($o->{ui}->gray('···· 0 1 2 3 4 5 6 7 8 9 a b c d e f 0123456789abcdef'));
17209 0           while ($lineStart < $length) {
17210 0           my $hexLine = $currentStyle;
17211 0           my $textLine = $currentStyle;
17212              
17213 0           my $k = 0;
17214 0           while ($k < 16) {
17215 0           my $index = $lineStart + $k;
17216 0 0         last if $index >= $length;
17217              
17218 0           my $break = 0;
17219 0           while ($index >= $nextChange->{at}) {
17220 0           $currentStyle = $nextChange->{style};
17221 0   0       $break = $nextChange->{breakBefore} && $k > 0;
17222 0           $hexLine .= $currentStyle;
17223 0           $textLine .= $currentStyle;
17224 0           $nextChange = shift @styleChanges;
17225 0 0         last if $break;
17226             }
17227              
17228 0 0         last if $break;
17229              
17230 0           my $byte = substr($o->{bytes}, $lineStart + $k, 1);
17231 0           $hexLine .= ' '.unpack('H*', $byte);
17232              
17233 0           my $code = ord($byte);
17234 0 0 0       $textLine .= $code >= 32 && $code <= 126 ? $byte : '·';
17235              
17236 0           $k += 1;
17237             }
17238              
17239 0           $hexLine .= ' ' x (16 - $k);
17240 0           $textLine .= ' ' x (16 - $k);
17241 0           $o->{ui}->line($o->{ui}->gray(unpack('H4', pack('S>', $lineStart))), ' ', $hexLine, $resetStyle, ' ', $textLine, $resetStyle);
17242              
17243 0           $lineStart += $k;
17244             }
17245             }
17246              
17247             package CDS::UI::ProgressStore;
17248              
17249 1     1   4786 use parent -norequire, 'CDS::Store';
  1         2  
  1         11  
17250              
17251             sub new {
17252 0     0     my $class = shift;
17253 0           my $store = shift;
17254 0           my $url = shift;
17255 0           my $ui = shift;
17256              
17257 0           return bless {
17258             store => $store,
17259             url => $url,
17260             ui => $ui,
17261             }
17262             }
17263              
17264 0     0     sub store { shift->{store} }
17265 0     0     sub url { shift->{url} }
17266 0     0     sub ui { shift->{ui} }
17267              
17268             sub id {
17269 0     0     my $o = shift;
17270 0           'Progress'."\n ".$o->{store}->id }
17271              
17272             ### Object store functions
17273              
17274             sub get {
17275 0     0     my $o = shift;
17276 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
17277 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
17278              
17279 0           $o->{ui}->progress('GET ', $hash->shortHex, ' on ', $o->{url});
17280 0           return $o->{store}->get($hash, $keyPair);
17281             }
17282              
17283             sub book {
17284 0     0     my $o = shift;
17285 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
17286 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
17287              
17288 0           $o->{ui}->progress('BOOK ', $hash->shortHex, ' on ', $o->{url});
17289 0           return $o->{store}->book($hash, $keyPair);
17290             }
17291              
17292             sub put {
17293 0     0     my $o = shift;
17294 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
17295 0 0 0       my $object = shift; die 'wrong type '.ref($object).' for $object' if defined $object && ref $object ne 'CDS::Object';
  0            
17296 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
17297              
17298 0           $o->{ui}->progress('PUT ', $hash->shortHex, ' (', $o->{ui}->niceFileSize($object->byteLength), ') on ', $o->{url});
17299 0           return $o->{store}->put($hash, $object, $keyPair);
17300             }
17301              
17302             ### Account store functions
17303              
17304             sub list {
17305 0     0     my $o = shift;
17306 0 0 0       my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0            
17307 0           my $boxLabel = shift;
17308 0           my $timeout = shift;
17309 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
17310              
17311 0 0         $o->{ui}->progress($timeout == 0 ? 'LIST ' : 'WATCH ', $boxLabel, ' of ', $accountHash->shortHex, ' on ', $o->{url});
17312 0           return $o->{store}->list($accountHash, $boxLabel, $timeout, $keyPair);
17313             }
17314              
17315             sub add {
17316 0     0     my $o = shift;
17317 0 0 0       my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0            
17318 0           my $boxLabel = shift;
17319 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
17320 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
17321              
17322 0           $o->{ui}->progress('ADD ', $accountHash->shortHex, ' ', $boxLabel, ' ', $hash->shortHex, ' on ', $o->{url});
17323 0           return $o->{store}->add($accountHash, $boxLabel, $hash, $keyPair);
17324             }
17325              
17326             sub remove {
17327 0     0     my $o = shift;
17328 0 0 0       my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0            
17329 0           my $boxLabel = shift;
17330 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
17331 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
17332              
17333 0           $o->{ui}->progress('REMOVE ', $accountHash->shortHex, ' ', $boxLabel, ' ', $hash->shortHex, ' on ', $o->{url});
17334 0           return $o->{store}->remove($accountHash, $boxLabel, $hash, $keyPair);
17335             }
17336              
17337             sub modify {
17338 0     0     my $o = shift;
17339 0           my $modifications = shift;
17340 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
17341              
17342 0           $o->{ui}->progress('MODIFY +', scalar @{$modifications->additions}, ' -', scalar @{$modifications->removals}, ' on ', $o->{url});
  0            
  0            
17343 0           return $o->{store}->modify($modifications, $keyPair);
17344             }
17345              
17346             # Displays a record, and tries to guess the byte interpretation
17347             package CDS::UI::Record;
17348              
17349             sub display {
17350 0     0     my $class = shift;
17351 0           my $ui = shift;
17352 0 0 0       my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0            
17353 0           my $storeUrl = shift;
17354              
17355 0 0         my $o = bless {
17356             ui => $ui,
17357             onStore => defined $storeUrl ? $ui->gray(' on ', $storeUrl) : '',
17358             };
17359              
17360 0           $o->record($record, '');
17361             }
17362              
17363             sub record {
17364 0     0     my $o = shift;
17365 0 0 0       my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0            
17366 0           my $context = shift;
17367              
17368 0           my $bytes = $record->bytes;
17369 0           my $hash = $record->hash;
17370 0           my @children = $record->children;
17371              
17372             # Try to interpret the key / value pair with a set of heuristic rules
17373             my @value =
17374             ! length $bytes && $hash ? ($o->{ui}->gold('cds show record '), $hash->hex, $o->{onStore}) :
17375             ! length $bytes ? $o->{ui}->gray('empty') :
17376 0 0 0       length $bytes == 32 && $hash ? ($o->{ui}->gold('cds show record '), $hash->hex, $o->{onStore}, $o->{ui}->gold(' decrypted with ', unpack('H*', $bytes))) :
    0 0        
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
17377             $context eq 'e' ? $o->hexValue($bytes) :
17378             $context eq 'n' ? $o->hexValue($bytes) :
17379             $context eq 'p' ? $o->hexValue($bytes) :
17380             $context eq 'q' ? $o->hexValue($bytes) :
17381             $context eq 'encrypted for' ? $o->hexValue($bytes) :
17382             $context eq 'updated by' ? $o->hexValue($bytes) :
17383             $context =~ /(^| )id( |$)/ ? $o->hexValue($bytes) :
17384             $context =~ /(^| )key( |$)/ ? $o->hexValue($bytes) :
17385             $context =~ /(^| )signature( |$)/ ? $o->hexValue($bytes) :
17386             $context =~ /(^| )revision( |$)/ ? $o->revisionValue($bytes) :
17387             $context =~ /(^| )date( |$)/ ? $o->dateValue($bytes) :
17388             $context =~ /(^| )expires( |$)/ ? $o->dateValue($bytes) :
17389             $o->guessValue($bytes);
17390              
17391 0 0 0       push @value, ' ', $o->{ui}->blue($hash->hex), $o->{onStore} if $hash && ($bytes && length $bytes != 32);
      0        
17392 0           $o->{ui}->line(@value);
17393              
17394             # Children
17395 0           $o->{ui}->pushIndent;
17396 0           for my $child (@children) { $o->record($child, $bytes); }
  0            
17397 0           $o->{ui}->popIndent;
17398             }
17399              
17400             sub hexValue {
17401 0     0     my $o = shift;
17402 0           my $bytes = shift;
17403              
17404 0           my $length = length $bytes;
17405 0 0         return '#'.unpack('H*', substr($bytes, 0, $length)) if $length <= 64;
17406 0           return '#'.unpack('H*', substr($bytes, 0, 64)), '…', $o->{ui}->gray(' (', $length, ' bytes)');
17407             }
17408              
17409             sub guessValue {
17410 0     0     my $o = shift;
17411 0           my $bytes = shift;
17412              
17413 0           my $length = length $bytes;
17414 0 0         my $text = $length > 64 ? substr($bytes, 0, 64).'…' : $bytes;
17415 0           $text =~ s/[\x00-\x1f\x7f-\xff]/·/g;
17416 0           my @value = ($text);
17417              
17418 0 0         if ($length <= 8) {
17419 0           my $integer = CDS->integerFromBytes($bytes);
17420 0 0         push @value, $o->{ui}->gray(' = ', $integer, $o->looksLikeTimestamp($integer) ? ' = '.$o->{ui}->niceDateTime($integer).' = '.$o->{ui}->niceDateTimeLocal($integer) : '');
17421             }
17422              
17423 0 0 0       push @value, $o->{ui}->gray(' = ', CDS->floatFromBytes($bytes)) if $length == 4 || $length == 8;
17424 0 0         push @value, $o->{ui}->gray(' = ', CDS::Hash->fromBytes($bytes)->hex) if $length == 32;
17425 0 0         push @value, $o->{ui}->gray(' (', length $bytes, ' bytes)') if length $bytes > 64;
17426 0           return @value;
17427             }
17428              
17429             sub dateValue {
17430 0     0     my $o = shift;
17431 0           my $bytes = shift;
17432              
17433 0           my $integer = CDS->integerFromBytes($bytes);
17434 0 0         return $integer if ! $o->looksLikeTimestamp($integer);
17435 0           return $o->{ui}->niceDateTime($integer), ' ', $o->{ui}->gray($o->{ui}->niceDateTimeLocal($integer));
17436             }
17437              
17438             sub revisionValue {
17439 0     0     my $o = shift;
17440 0           my $bytes = shift;
17441              
17442 0           my $integer = CDS->integerFromBytes($bytes);
17443 0 0         return $integer if ! $o->looksLikeTimestamp($integer);
17444 0           return $o->{ui}->niceDateTime($integer);
17445             }
17446              
17447             sub looksLikeTimestamp {
17448 0     0     my $o = shift;
17449 0           my $integer = shift;
17450              
17451 0   0       return $integer > 100000000000 && $integer < 10000000000000;
17452             }
17453              
17454             package CDS::UI::Span;
17455              
17456             sub new {
17457 0     0     my $class = shift;
17458              
17459 0           return bless {
17460             text => [@_],
17461             };
17462             }
17463              
17464             sub printTo {
17465 0     0     my $o = shift;
17466 0           my $ui = shift;
17467 0           my $parent = shift;
17468              
17469 0 0         if ($parent) {
17470 0   0       $o->{appliedForeground} = $o->{foreground} // $parent->{appliedForeground};
17471 0   0       $o->{appliedBackground} = $o->{background} // $parent->{appliedBackground};
17472 0   0       $o->{appliedBold} = $o->{bold} // $parent->{appliedBold} // 0;
      0        
17473 0   0       $o->{appliedUnderlined} = $o->{underlined} // $parent->{appliedUnderlined} // 0;
      0        
17474             } else {
17475 0           $o->{appliedForeground} = $o->{foreground};
17476 0           $o->{appliedBackground} = $o->{background};
17477 0   0       $o->{appliedBold} = $o->{bold} // 0;
17478 0   0       $o->{appliedUnderlined} = $o->{underlined} // 0;
17479             }
17480              
17481 0           my $style = chr(0x1b).'[0';
17482 0 0         $style .= ';1' if $o->{appliedBold};
17483 0 0         $style .= ';4' if $o->{appliedUnderlined};
17484 0 0         $style .= ';38;5;'.$o->{appliedForeground} if defined $o->{appliedForeground};
17485 0 0         $style .= ';48;5;'.$o->{appliedBackground} if defined $o->{appliedBackground};
17486 0           $style .= 'm';
17487              
17488 0           my $needStyle = 1;
17489 0           for my $child (@{$o->{text}}) {
  0            
17490 0           my $ref = ref $child;
17491 0 0         if ($ref eq 'CDS::UI::Span') {
    0          
    0          
17492 0           $child->printTo($ui, $o);
17493 0           $needStyle = 1;
17494 0           next;
17495             } elsif (length $ref) {
17496 0           warn 'Printing REF';
17497 0           $child = $ref;
17498             } elsif (! defined $child) {
17499 0           warn 'Printing UNDEF';
17500 0           $child = 'UNDEF';
17501             }
17502              
17503 0 0         if ($needStyle) {
17504 0           $ui->print($style);
17505 0           $needStyle = 0;
17506             }
17507              
17508 0           $ui->print($child);
17509             }
17510             }
17511              
17512             sub wordWrap {
17513 0     0     my $o = shift;
17514 0           my $state = shift;
17515              
17516 0           my $index = -1;
17517 0           for my $child (@{$o->{text}}) {
  0            
17518 0           $index += 1;
17519              
17520 0 0         next if ! defined $child;
17521              
17522 0           my $ref = ref $child;
17523 0 0         if ($ref eq 'CDS::UI::Span') {
    0          
    0          
17524 0           $child->wordWrap($state);
17525 0           next;
17526             } elsif (length $ref) {
17527 0           warn 'Printing REF';
17528 0           $child = $ref;
17529             } elsif (! defined $child) {
17530 0           warn 'Printing UNDEF';
17531 0           $child = 'UNDEF';
17532             }
17533              
17534 0           my $position = -1;
17535 0           for my $char (split //, $child) {
17536 0           $position += 1;
17537 0           $state->{lineLength} += 1;
17538 0 0 0       if ($char eq ' ' || $char eq "\t") {
    0 0        
17539 0           $state->{wrapSpan} = $o;
17540 0           $state->{wrapIndex} = $index;
17541 0           $state->{wrapPosition} = $position;
17542 0           $state->{wrapReturn} = $state->{lineLength};
17543             } elsif ($state->{wrapSpan} && $state->{lineLength} > $state->{maxLength}) {
17544 0           my $text = $state->{wrapSpan}->{text}->[$state->{wrapIndex}];
17545 0           $text = substr($text, 0, $state->{wrapPosition})."\n".$state->{indent}.substr($text, $state->{wrapPosition} + 1);
17546 0           $state->{wrapSpan}->{text}->[$state->{wrapIndex}] = $text;
17547 0           $state->{lineLength} -= $state->{wrapReturn};
17548 0 0 0       $position += length $state->{indent} if $state->{wrapSpan} == $o && $state->{wrapIndex} == $index;
17549 0           $state->{wrapSpan} = undef;
17550             }
17551             }
17552             }
17553             }
17554              
17555             package CDS::UnionList;
17556              
17557             sub new {
17558 0     0     my $class = shift;
17559 0           my $privateRoot = shift;
17560 0           my $label = shift;
17561              
17562 0           my $o = bless {
17563             privateRoot => $privateRoot,
17564             label => $label,
17565             unsaved => CDS::Unsaved->new($privateRoot->unsaved),
17566             items => {},
17567             parts => {},
17568             hasPartsToMerge => 0,
17569             }, $class;
17570              
17571 0           $o->{unused} = CDS::UnionList::Part->new;
17572 0           $o->{changes} = CDS::UnionList::Part->new;
17573 0           $privateRoot->addDataHandler($label, $o);
17574 0           return $o;
17575             }
17576              
17577 0     0     sub privateRoot { shift->{privateRoot} }
17578 0     0     sub unsaved { shift->{unsaved} }
17579             sub items {
17580 0     0     my $o = shift;
17581 0           values %{$o->{items}} }
  0            
17582             sub parts {
17583 0     0     my $o = shift;
17584 0           values %{$o->{parts}} }
  0            
17585              
17586             sub get {
17587 0     0     my $o = shift;
17588 0           my $id = shift;
17589 0           $o->{items}->{$id} }
17590              
17591             sub getOrCreate {
17592 0     0     my $o = shift;
17593 0           my $id = shift;
17594              
17595 0           my $item = $o->{items}->{$id};
17596 0 0         return $item if $item;
17597 0           my $newItem = $o->createItem($id);
17598 0           $o->{items}->{$id} = $newItem;
17599 0           return $newItem;
17600             }
17601              
17602             # abstract sub createItem($o, $id)
17603             # abstract sub forgetObsoleteItems($o)
17604              
17605             sub forget {
17606 0     0     my $o = shift;
17607 0           my $id = shift;
17608              
17609 0   0       my $item = $o->{items}->{$id} // return;
17610 0           $item->{part}->{count} -= 1;
17611 0           delete $o->{items}->{$id};
17612             }
17613              
17614             sub forgetItem {
17615 0     0     my $o = shift;
17616 0           my $item = shift;
17617              
17618 0           $item->{part}->{count} -= 1;
17619 0           delete $o->{items}->{$item->id};
17620             }
17621              
17622             # *** MergeableData interface
17623              
17624             sub addDataTo {
17625 0     0     my $o = shift;
17626 0 0 0       my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0            
17627              
17628 0           for my $part (sort { $a->{hashAndKey}->hash->bytes cmp $b->{hashAndKey}->hash->bytes } values %{$o->{parts}}) {
  0            
  0            
17629 0           $record->addHashAndKey($part->{hashAndKey});
17630             }
17631             }
17632              
17633             sub mergeData {
17634 0     0     my $o = shift;
17635 0 0 0       my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0            
17636              
17637 0           my @hashesAndKeys;
17638 0           for my $child ($record->children) {
17639 0   0       push @hashesAndKeys, $child->asHashAndKey // next;
17640             }
17641              
17642 0           $o->merge(@hashesAndKeys);
17643             }
17644              
17645             sub mergeExternalData {
17646 0     0     my $o = shift;
17647 0           my $store = shift;
17648 0 0 0       my $record = shift; die 'wrong type '.ref($record).' for $record' if defined $record && ref $record ne 'CDS::Record';
  0            
17649 0 0 0       my $source = shift; die 'wrong type '.ref($source).' for $source' if defined $source && ref $source ne 'CDS::Source';
  0            
17650              
17651 0           my @hashes;
17652             my @hashesAndKeys;
17653 0           for my $child ($record->children) {
17654 0   0       my $hashAndKey = $child->asHashAndKey // next;
17655 0 0         next if $o->{parts}->{$hashAndKey->hash->bytes};
17656 0           push @hashes, $hashAndKey->hash;
17657 0           push @hashesAndKeys, $hashAndKey;
17658             }
17659              
17660 0           my $keyPair = $o->{privateRoot}->privateBoxReader->keyPair;
17661 0           my ($missing, $transferStore, $storeError) = $keyPair->transfer([@hashes], $store, $o->{privateRoot}->unsaved);
17662 0 0         return if defined $storeError;
17663 0 0         return if $missing;
17664              
17665 0 0         if ($source) {
17666 0           $source->keep;
17667 0           $o->{privateRoot}->unsaved->state->addMergedSource($source);
17668             }
17669              
17670 0           $o->merge(@hashesAndKeys);
17671 0           return 1;
17672             }
17673              
17674             sub merge {
17675 0     0     my $o = shift;
17676              
17677 0           for my $hashAndKey (@_) {
17678 0 0         next if ! $hashAndKey;
17679 0 0         next if $o->{parts}->{$hashAndKey->hash->bytes};
17680 0           my $part = CDS::UnionList::Part->new;
17681 0           $part->{hashAndKey} = $hashAndKey;
17682 0           $o->{parts}->{$hashAndKey->hash->bytes} = $part;
17683 0           $o->{hasPartsToMerge} = 1;
17684             }
17685             }
17686              
17687             # *** Reading
17688              
17689             sub read {
17690 0     0     my $o = shift;
17691              
17692 0 0         return 1 if ! $o->{hasPartsToMerge};
17693              
17694             # Load the parts
17695 0           for my $part (values %{$o->{parts}}) {
  0            
17696 0 0         next if $part->{isMerged};
17697 0 0         next if $part->{loadedRecord};
17698              
17699 0           my ($record, $object, $invalidReason, $storeError) = $o->{privateRoot}->privateBoxReader->keyPair->getAndDecryptRecord($part->{hashAndKey}, $o->{privateRoot}->unsaved);
17700 0 0         return if defined $storeError;
17701              
17702 0 0         delete $o->{parts}->{$part->{hashAndKey}->hash->bytes} if defined $invalidReason;
17703 0           $part->{loadedRecord} = $record;
17704             }
17705              
17706             # Merge the loaded parts
17707 0           for my $part (values %{$o->{parts}}) {
  0            
17708 0 0         next if $part->{isMerged};
17709 0 0         next if ! $part->{loadedRecord};
17710              
17711             # Merge
17712 0           for my $child ($part->{loadedRecord}->children) {
17713 0           $o->mergeRecord($part, $child);
17714             }
17715              
17716 0           delete $part->{loadedRecord};
17717 0           $part->{isMerged} = 1;
17718             }
17719              
17720 0           $o->{hasPartsToMerge} = 0;
17721 0           return 1;
17722             }
17723              
17724             # abstract sub mergeRecord($o, $part, $record)
17725              
17726             # *** Saving
17727              
17728             sub hasChanges {
17729 0     0     my $o = shift;
17730 0           $o->{changes}->{count} > 0 }
17731              
17732             sub save {
17733 0     0     my $o = shift;
17734              
17735 0           $o->forgetObsoleteItems;
17736 0           $o->{unsaved}->startSaving;
17737              
17738 0 0         if ($o->{changes}->{count}) {
17739             # Take the changes
17740 0           my $newPart = $o->{changes};
17741 0           $o->{changes} = CDS::UnionList::Part->new;
17742              
17743             # Add all changes
17744 0           my $record = CDS::Record->new;
17745 0           for my $item (values %{$o->{items}}) {
  0            
17746 0 0         next if $item->{part} != $newPart;
17747 0           $item->addToRecord($record);
17748             }
17749              
17750             # Select all parts smaller than 2 * count elements
17751 0           my $count = $newPart->{count};
17752 0           while (1) {
17753 0           my $addedPart = 0;
17754 0           for my $part (values %{$o->{parts}}) {
  0            
17755 0 0 0       next if ! $part->{isMerged} || $part->{selected} || $part->{count} >= $count * 2;
      0        
17756 0           $count += $part->{count};
17757 0           $part->{selected} = 1;
17758 0           $addedPart = 1;
17759             }
17760              
17761 0 0         last if ! $addedPart;
17762             }
17763              
17764             # Include the selected items
17765 0           for my $item (values %{$o->{items}}) {
  0            
17766 0 0         next if ! $item->{part}->{selected};
17767 0           $item->setPart($newPart);
17768 0           $item->addToRecord($record);
17769             }
17770              
17771             # Serialize the new part
17772 0           my $key = CDS->randomKey;
17773 0           my $newObject = $record->toObject->crypt($key);
17774 0           my $newHash = $newObject->calculateHash;
17775 0           $newPart->{hashAndKey} = CDS::HashAndKey->new($newHash, $key);
17776 0           $newPart->{isMerged} = 1;
17777 0           $o->{parts}->{$newHash->bytes} = $newPart;
17778 0           $o->{privateRoot}->unsaved->state->addObject($newHash, $newObject);
17779 0           $o->{privateRoot}->dataChanged;
17780             }
17781              
17782             # Remove obsolete parts
17783 0           for my $part (values %{$o->{parts}}) {
  0            
17784 0 0         next if ! $part->{isMerged};
17785 0 0         next if $part->{count};
17786 0           delete $o->{parts}->{$part->{hashAndKey}->hash->bytes};
17787 0           $o->{privateRoot}->dataChanged;
17788             }
17789              
17790             # Propagate the unsaved state
17791 0           $o->{privateRoot}->unsaved->state->merge($o->{unsaved}->savingState);
17792 0           $o->{unsaved}->savingDone;
17793 0           return 1;
17794             }
17795              
17796             package CDS::UnionList::Item;
17797              
17798             sub new {
17799 0     0     my $class = shift;
17800 0           my $unionList = shift;
17801 0           my $id = shift;
17802              
17803 0           $unionList->{unused}->{count} += 1;
17804             return bless {
17805             unionList => $unionList,
17806             id => $id,
17807             part => $unionList->{unused},
17808 0           }, $class;
17809             }
17810              
17811 0     0     sub unionList { shift->{unionList} }
17812 0     0     sub id { shift->{id} }
17813              
17814             sub setPart {
17815 0     0     my $o = shift;
17816 0           my $part = shift;
17817              
17818 0           $o->{part}->{count} -= 1;
17819 0           $o->{part} = $part;
17820 0           $o->{part}->{count} += 1;
17821             }
17822              
17823             # abstract sub addToRecord($o, $record)
17824              
17825             package CDS::UnionList::Part;
17826              
17827             sub new {
17828 0     0     my $class = shift;
17829              
17830 0           return bless {
17831             isMerged => 0,
17832             hashAndKey => undef,
17833             size => 0,
17834             count => 0,
17835             selected => 0,
17836             };
17837             }
17838              
17839             package CDS::Unsaved;
17840              
17841 1     1   4069 use parent -norequire, 'CDS::Store';
  1         2  
  1         4  
17842              
17843             sub new {
17844 0     0     my $class = shift;
17845 0           my $store = shift;
17846              
17847 0           return bless {
17848             state => CDS::Unsaved::State->new,
17849             savingState => undef,
17850             store => $store,
17851             };
17852             }
17853              
17854 0     0     sub state { shift->{state} }
17855 0     0     sub savingState { shift->{savingState} }
17856              
17857             # *** Saving, state propagation
17858              
17859             sub isSaving {
17860 0     0     my $o = shift;
17861 0           defined $o->{savingState} }
17862              
17863             sub startSaving {
17864 0     0     my $o = shift;
17865              
17866 0 0         die 'Start saving, but already saving' if $o->{savingState};
17867 0           $o->{savingState} = $o->{state};
17868 0           $o->{state} = CDS::Unsaved::State->new;
17869             }
17870              
17871             sub savingDone {
17872 0     0     my $o = shift;
17873              
17874 0 0         die 'Not in saving state' if ! $o->{savingState};
17875 0           $o->{savingState} = undef;
17876             }
17877              
17878             sub savingFailed {
17879 0     0     my $o = shift;
17880              
17881 0 0         die 'Not in saving state' if ! $o->{savingState};
17882 0           $o->{state}->merge($o->{savingState});
17883 0           $o->{savingState} = undef;
17884             }
17885              
17886             # *** Store interface
17887              
17888             sub id {
17889 0     0     my $o = shift;
17890 0           'Unsaved'."\n".unpack('H*', CDS->randomBytes(16))."\n".$o->{store}->id }
17891              
17892             sub get {
17893 0     0     my $o = shift;
17894 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
17895 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
17896              
17897 0           my $stateObject = $o->{state}->{objects}->{$hash->bytes};
17898 0 0         return $stateObject->{object} if $stateObject;
17899              
17900 0 0         if ($o->{savingState}) {
17901 0           my $savingStateObject = $o->{savingState}->{objects}->{$hash->bytes};
17902 0 0         return $savingStateObject->{object} if $savingStateObject;
17903             }
17904              
17905 0           return $o->{store}->get($hash, $keyPair);
17906             }
17907              
17908             sub book {
17909 0     0     my $o = shift;
17910 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
17911 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
17912              
17913 0           return $o->{store}->book($hash, $keyPair);
17914             }
17915              
17916             sub put {
17917 0     0     my $o = shift;
17918 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
17919 0 0 0       my $object = shift; die 'wrong type '.ref($object).' for $object' if defined $object && ref $object ne 'CDS::Object';
  0            
17920 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
17921              
17922 0           return $o->{store}->put($hash, $object, $keyPair);
17923             }
17924              
17925             sub list {
17926 0     0     my $o = shift;
17927 0 0 0       my $accountHash = shift; die 'wrong type '.ref($accountHash).' for $accountHash' if defined $accountHash && ref $accountHash ne 'CDS::Hash';
  0            
17928 0           my $boxLabel = shift;
17929 0           my $timeout = shift;
17930 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
17931              
17932 0           return $o->{store}->list($accountHash, $boxLabel, $timeout, $keyPair);
17933             }
17934              
17935             sub modify {
17936 0     0     my $o = shift;
17937 0           my $additions = shift;
17938 0           my $removals = shift;
17939 0 0 0       my $keyPair = shift; die 'wrong type '.ref($keyPair).' for $keyPair' if defined $keyPair && ref $keyPair ne 'CDS::KeyPair';
  0            
17940              
17941 0           return $o->{store}->modify($additions, $removals, $keyPair);
17942             }
17943              
17944             package CDS::Unsaved::State;
17945              
17946             sub new {
17947 0     0     my $class = shift;
17948              
17949 0           return bless {
17950             objects => {},
17951             mergedSources => [],
17952             dataSavedHandlers => [],
17953             };
17954             }
17955              
17956 0     0     sub objects { shift->{objects} }
17957             sub mergedSources {
17958 0     0     my $o = shift;
17959 0           @{$o->{mergedSources}} }
  0            
17960             sub dataSavedHandlers {
17961 0     0     my $o = shift;
17962 0           @{$o->{dataSavedHandlers}} }
  0            
17963              
17964             sub addObject {
17965 0     0     my $o = shift;
17966 0 0 0       my $hash = shift; die 'wrong type '.ref($hash).' for $hash' if defined $hash && ref $hash ne 'CDS::Hash';
  0            
17967 0 0 0       my $object = shift; die 'wrong type '.ref($object).' for $object' if defined $object && ref $object ne 'CDS::Object';
  0            
17968              
17969 0           $o->{objects}->{$hash->bytes} = {hash => $hash, object => $object};
17970             }
17971              
17972             sub addMergedSource {
17973 0     0     my $o = shift;
17974              
17975 0           push @{$o->{mergedSources}}, @_;
  0            
17976             }
17977              
17978             sub addDataSavedHandler {
17979 0     0     my $o = shift;
17980              
17981 0           push @{$o->{dataSavedHandlers}}, @_;
  0            
17982             }
17983              
17984             sub merge {
17985 0     0     my $o = shift;
17986 0           my $state = shift;
17987              
17988 0           for my $key (keys %{$state->{objects}}) {
  0            
17989 0           $o->{objects}->{$key} = $state->{objects}->{$key};
17990             }
17991              
17992 0           push @{$o->{mergedSources}}, @{$state->{mergedSources}};
  0            
  0            
17993 0           push @{$o->{dataSavedHandlers}}, @{$state->{dataSavedHandlers}};
  0            
  0            
17994             }