File Coverage

lib/Data/Identifier/Generate.pm
Criterion Covered Total %
statement 112 254 44.0
branch 53 160 33.1
condition 45 157 28.6
subroutine 15 21 71.4
pod 9 9 100.0
total 234 601 38.9


line stmt bran cond sub pod time code
1             # Copyright (c) 2023-2026 Philipp Schafft
2              
3             # licensed under Artistic License 2.0 (see LICENSE file)
4              
5             # ABSTRACT: format independent identifier object
6              
7              
8             package Data::Identifier::Generate;
9              
10 3     3   222107 use v5.20;
  3         7  
11 3     3   13 use strict;
  3         4  
  3         62  
12 3     3   10 use warnings;
  3         3  
  3         126  
13              
14 3     3   13 use Carp;
  3         4  
  3         222  
15 3     3   32 use Encode qw(encode);
  3         3  
  3         227  
16 3     3   1408 use Digest;
  3         1925  
  3         82  
17              
18 3     3   570 use Data::Identifier;
  3         5  
  3         17  
19              
20             use constant {
21 3         9244 NS_GTE => '90355e77-6942-4d82-a0b6-3a6de65bf948',
22              
23             WK_UNSIGNED_INTEGER_GENERATOR => '53863a15-68d4-448d-bd69-a9b19289a191',
24             WK_SIGNED_INTEGER_GENERATOR => 'e8aa9e01-8d37-4b4b-8899-42ca0a2a906f',
25             WK_UNICODE_CHARACTER_GENERATOR => 'd74f8c35-bcb8-465c-9a77-01010e8ed25c',
26             WK_RGB_COLOUR_GENERATOR => '55febcc4-6655-4397-ae3d-2353b5856b34',
27             WK_DATE_GENERATOR => '97b7f241-e1c5-4f02-ae3c-8e31e501e1dc',
28             #WK_LANGUAGE_GENERATOR => '',
29             WK_MULTIPLICITY_GENERATOR => '19659233-0a22-412c-bdf1-8ee9f8fc4086',
30             WK_MINIMUM_MULTIPLICITY_GENERATOR => '5ec197c3-1406-467c-96c7-4b1a6ec2c5c9',
31 3     3   16 };
  3         4  
32              
33              
34             our $VERSION = v0.30;
35              
36             my %_multiplicity_prefix = (
37             total => '4.1',
38             minimum => '4.3',
39             );
40              
41             my %_multiplicity_names = (
42             0 => 'noone',
43             1 => 'solo',
44             2 => 'duo',
45             3 => 'trio',
46             );
47              
48             my %_multiplicity_generators = (
49             total => WK_MULTIPLICITY_GENERATOR,
50             minimum => WK_MINIMUM_MULTIPLICITY_GENERATOR,
51             );
52              
53             my %_gte_simple_profiles = (
54             'ab332382-a6f8-4a24-914d-5f823dd866c1' => {
55             namespace => 'a13377a3-88d4-484e-90a8-245afb22a793',
56             order => 'MFHCSmfhcs',
57             case_folding => undef,
58             strip_slash => undef,
59             strip_spaces => undef,
60             },
61             'e537db94-85b9-4125-972a-cc2ea1fdf51d' => {
62             namespace => '52f20647-1dc7-4234-81ad-0639ab6cef60',
63             order => 'FAfa',
64             case_folding => undef,
65             strip_slash => undef,
66             strip_spaces => undef,
67             },
68             '60764502-18cb-41c9-8531-e4c8b43140b9' => {
69             namespace => '1e2edf8d-d459-47cb-9d6e-0690f404fadf',
70             order => 'MFHCSmfhcs',
71             case_folding => undef,
72             strip_slash => undef,
73             strip_spaces => undef,
74             },
75             '5aad9d75-7020-41a4-8ec6-2bf09566f985' => {
76             namespace => 'fc9741b4-2ac7-412b-9d36-2b675fc0482b',
77             order => 'NDTBndtb',
78             case_folding => undef,
79             strip_slash => undef,
80             strip_spaces => undef,
81             },
82             );
83              
84              
85             #@returns Data::Identifier
86             sub integer {
87 48     48 1 2422 my ($pkg, $request, %opts) = @_;
88 48         58 $opts{request} = $request;
89 48         63 $opts{style} = 'integer-based';
90 48         160 $opts{namespace} = Data::Identifier->NS_INT();
91 48   33     131 $opts{displayname}//= $request;
92 48 100       82 $opts{generator} = $request >= 0 ? WK_UNSIGNED_INTEGER_GENERATOR : WK_SIGNED_INTEGER_GENERATOR;
93              
94 48         114 return $pkg->generic(%opts);
95             }
96              
97              
98             sub unicode_character {
99 3     3 1 1764 my ($pkg, $type, $request, %opts) = @_;
100 3         4 my $unicode_cp;
101             my $unicode_cp_str;
102              
103 3 50       6 croak 'No type given' unless defined $type;
104 3 50 33     11 croak 'No/Bad request given' unless defined($request) && length($request);
105              
106 3 50       10 if ($type eq 'unicode') {
    50          
    50          
107 0 0       0 if ($request =~ /^[Uu]\+([0-9a-fA-F]+)$/) {
    0          
108 0         0 $unicode_cp = hex($1);
109             } elsif ($request =~ /^[0-9]+\z/) {
110 0         0 $unicode_cp = int($request);
111             } else {
112 0         0 croak 'Bad request given: '.$request;
113             }
114             } elsif ($type eq 'ascii') {
115 0 0       0 if ($request =~ /^[0-9]+\z/) {
116 0         0 $unicode_cp = int($request);
117             } else {
118 0         0 croak 'Bad request given: '.$request;
119             }
120 0 0 0     0 croak 'US-ASCII character out of range: '.$unicode_cp if $unicode_cp < 0 || $unicode_cp > 0x7F;
121             } elsif ($type eq 'raw') {
122 3 50       5 croak 'Raw value is not exactly one character long' unless length($request) == 1;
123 3         4 $unicode_cp = ord($request);
124             } else {
125 0         0 croak 'Bad type given: '.$type;
126             }
127              
128 3 50 33     8 croak 'Unicode character out of range: '.$unicode_cp if $unicode_cp < 0 || $unicode_cp > 0x10FFFF;
129              
130 3         10 $unicode_cp_str = sprintf('U+%04X', $unicode_cp);
131              
132 3 50 33     34 if ($unicode_cp == 0xFFFC || $unicode_cp == 0xFFFD || $unicode_cp == 0xFEFF || $unicode_cp == 0xFFFE) {
      33        
      33        
133 0 0       0 croak 'Rejected use of special character: '.$unicode_cp_str unless $opts{allow_special};
134             }
135              
136 3   33     11 $opts{displayname} //= $unicode_cp_str;
137              
138 3         10 return Data::Identifier->new(unicodecp => $unicode_cp_str, displayname => $opts{displayname}, generator => WK_UNICODE_CHARACTER_GENERATOR, request => $unicode_cp_str);
139             }
140              
141              
142             #@returns Data::Identifier
143             sub colour {
144 5     5 1 3309 my ($pkg, $colour, %opts) = @_;
145 5         9 $opts{request} = $colour;
146 5         9 $opts{style} = 'colour';
147 5         6 $opts{namespace} = '88d3944f-a13b-4e35-89eb-e3c1fbe53e76';
148 5         6 $opts{generator} = WK_RGB_COLOUR_GENERATOR;
149 5         15 return $pkg->generic(%opts);
150             }
151              
152              
153             #@returns Data::Identifier
154             sub date {
155 9     9 1 5230 my ($pkg, $request, %opts) = @_;
156 9         17 my ($year, $month, $day);
157 9         0 my $precision;
158              
159 9 50       18 if (ref($request)) {
160 0 0       0 if (eval {$request->can('epoch')}) {
  0         0  
161 0         0 $request = $request->epoch;
162             } else {
163 0         0 return $pkg->date(scalar($request->()), %opts);
164             }
165             }
166              
167 9         46 ($year, $month, $day) = $request =~ /^([12][0-9]{3})(?:-([01][0-9])(?:-([0-3][0-9]))?)?Z$/;
168              
169 9 100 100     25 unless (length($year // '') == 4) {
170 3 50 33     35 if ($request eq 'now' || $request eq 'today') {
    50          
171 0         0 $request = time();
172             } elsif ($request =~ /^(?:0|-?[1-9][0-9]*)$/) {
173 3         4 $request = int($request);
174 3 50       8 if ($request > 32503680000) {
175 0         0 croak 'Unlikely far date given. Likely miliseconds are passed as seconds?';
176             }
177             } else {
178 0         0 croak 'Invalid format';
179             }
180              
181 3         13 (undef,undef,undef,$day,$month,$year) = gmtime($request);
182 3         4 $year += 1900;
183 3         5 $month += 1;
184             }
185              
186 9         13 foreach my $entry ($year, $month, $day) {
187 27   100     40 $entry = int($entry // 0);
188             }
189              
190 9 50 33     32 croak 'Invalid year' if $year && ($year < 1583 || $year > 9999);
      33        
191 9 50 33     25 croak 'Invalid month' if $month && ($month < 1 || $month > 12);
      66        
192 9 50 33     34 croak 'Invalid day' if $day && ($day < 1 || $day > 31);
      66        
193              
194 9 50       10 $month = 0 unless $year;
195 9 100       37 $day = 0 unless $month;
196              
197 9 100 66     39 $precision = $opts{precision} // ($day ? 'day' : undef) // ($month ? 'month' : undef) // 'year';
    100 100        
      100        
198 9 100 66     26 if ($precision eq 'day' && $day) {
    100 66        
    50 33        
199 7         24 $request = sprintf('%04u-%02u-%02uZ', $year, $month, $day);
200             } elsif ($precision eq 'month' && $month) {
201 1         4 $request = sprintf('%04u-%02uZ', $year, $month);
202             } elsif ($precision eq 'year' && $year) {
203 1         4 $request = sprintf('%04uZ', $year);
204             } else {
205 0         0 croak 'Bad precision: '.$precision;
206             }
207              
208 9         14 $opts{request} = $request;
209 9   33     25 $opts{input} //= $request; # force raw value!
210 9         11 $opts{style} = undef;
211 9         27 $opts{namespace} = Data::Identifier->NS_DATE();
212 9   33     27 $opts{displayname}//= $request;
213 9         10 $opts{generator} = WK_DATE_GENERATOR;
214              
215 9         27 return $pkg->generic(%opts);
216             }
217              
218              
219             sub language {
220 0     0 1 0 my ($pkg, $req, %opts) = @_;
221 0         0 my $name;
222             my $identifier;
223              
224 0         0 require I18N::LangTags::List;
225              
226 0         0 $opts{request} = $req;
227 0         0 $opts{style} = 'id-based';
228 0         0 $opts{namespace} = '47dd950c-9089-4956-87c1-54c122533219';
229             #$opts{generator} = WK_LANGUAGE_GENERATOR;
230              
231 0 0       0 croak 'Bad language: '.$req unless I18N::LangTags::List::is_decent($req);
232              
233 0         0 $name = I18N::LangTags::List::name($req);
234              
235 0 0 0     0 unless (defined($name) && length($name)) {
236 0         0 croak 'Bad language: '.$req;
237             }
238              
239 0   0     0 $opts{displayname} //= $name;
240 0         0 $identifier = $pkg->generic(%opts);
241              
242 0   0     0 $identifier->{id_cache} //= {};
243 0   0     0 $identifier->{id_cache}->{'d0a4c6e2-ce2f-4d4c-b079-60065ac681f1'} //= $req;
244              
245 0         0 return $identifier;
246             }
247              
248              
249             sub multiplicity {
250 0     0 1 0 my ($pkg, $subtype, $request, %opts) = @_;
251 0   0     0 my $prefix = $_multiplicity_prefix{$subtype} // croak 'Invalid subtype: '.$subtype;
252 0         0 my $identifier;
253             my $oid;
254              
255 0 0 0     0 croak 'Invalid value: '.$request unless $request eq '0' || $request =~ /^[1-9][0-9]*$/;
256              
257 0         0 $oid = '1.3.6.1.4.1.46942.16.2.'.$prefix.'.'.$request;
258              
259 0         0 $opts{request} = $request;
260 0         0 $opts{input} = $prefix.'.'.$request;
261 0         0 $opts{namespace} = NS_GTE;
262 0   0     0 $opts{displayname}//= $_multiplicity_names{$request};
263 0   0     0 $opts{displayname}//= $request;
264 0         0 $opts{generator} = $_multiplicity_generators{$subtype};
265 0         0 $identifier = $pkg->generic(%opts);
266              
267 0   0     0 $identifier->{id_cache} //= {};
268 0   0     0 $identifier->{id_cache}->{Data::Identifier->WK_OID} //= $oid;
269              
270 0 0       0 if (defined $_multiplicity_names{$request}) {
271 0         0 $identifier->register;
272             }
273              
274 0         0 return $identifier;
275             }
276              
277              
278             sub gte_simple {
279 0     0 1 0 my ($pkg, $profile, $request, %opts) = @_;
280 0         0 my %order;
281             my $normal;
282              
283 0 0       0 croak 'Called in list context' if wantarray;
284              
285 0 0       0 $profile = $profile->ise if eval {$profile->can('ise')};
  0         0  
286 0   0     0 $profile = $_gte_simple_profiles{$profile} // $profile;
287              
288             {
289 0         0 my $i = 0;
  0         0  
290 0         0 %order = map {$_ => $i++} split(//, $profile->{order});
  0         0  
291             }
292              
293 0 0       0 if (defined(my $folding = $profile->{case_folding})) {
294 0 0       0 if ($folding eq 'none') {
    0          
    0          
295             # no-op
296             } elsif ($folding eq 'upper') {
297 0         0 $request = uc($request);
298             } elsif ($folding eq 'lower') {
299 0         0 $request = lc($request);
300             } else {
301 0         0 croak 'Unsupported/invalid folding rule: '.$folding;
302             }
303             }
304              
305 0 0       0 if ($profile->{strip_slash}) {
306 0         0 $request =~ s#/+##g;
307             }
308              
309 0 0       0 if ($profile->{strip_spaces}) {
310 0         0 $request =~ s#\s+##g;
311             }
312              
313             $normal = join('',
314 0         0 sort {$order{$a} <=> $order{$b}}
315 0 0       0 map {croak 'Invalid input element: '.$_ unless defined $order{$_}; $_}
  0         0  
  0         0  
316             split //, $request);
317              
318 0 0       0 if (defined(my $info = delete $opts{info})) {
319 0         0 $info->{count} = length($normal);
320 0         0 $info->{request} = $normal;
321             }
322              
323 0         0 $opts{input} = $normal;
324 0         0 $opts{request} = $normal;
325 0   0     0 $opts{namespace} //= $profile->{namespace};
326 0   0     0 $opts{displayname}//= $normal;
327 0         0 return $pkg->generic(%opts);
328             }
329              
330              
331             sub unit {
332 0     0 1 0 my ($pkg, $request, %opts) = @_;
333              
334 0         0 require Data::Identifier::Util;
335              
336 0         0 return Data::Identifier::Util->render_unit_request('Data::Identifier' => $request, %opts);
337             }
338              
339              
340             #@returns Data::Identifier
341             sub generic {
342 76     76 1 206 my ($pkg, %opts) = @_;
343              
344 76 50       135 if (defined(my $type)) {
345 0   0     0 $opts{namespace} //= $type->namespace;
346             }
347              
348 76 100 66     231 if (defined(my $style = $opts{style}) && defined(my $request = $opts{request})) {
349 67 100       118 if ($style eq 'integer-based') {
    50          
    100          
    50          
    50          
    50          
350 48 50       207 croak 'Invalid request' unless $request =~ /^(?:0|-?[1-9][0-9]*)$/;
351 48   66     123 $opts{input} //= $request;
352             } elsif ($style eq 'id-based') {
353 0         0 my ($input, $name);
354              
355 0 0       0 if (($input, $name) = $request =~ /^(#?[a-zA-Z0-9\-\.\+]+) (.+)$/) {
    0          
356             # noop
357             } elsif (($input) = $request =~ /^(#?[a-zA-Z0-9\-\.\+]+)$/) {
358 0         0 $name = undef;
359             } else {
360 0         0 croak 'Invalid format, expected: "id name", or "id", got: '.$request;
361             }
362              
363 0   0     0 $opts{input} //= lc($input);
364 0   0     0 $opts{displayname} //= $name;
365              
366             } elsif ($style eq 'name-based') {
367 14   33     114 $opts{input} //= encode('UTF-8', $request);
368 14   33     488 $opts{displayname} //= $request;
369             } elsif ($style eq 'tag-based') {
370 0 0       0 if (ref($request)) {
    0          
371 0         0 my $identifier = Data::Identifier->new(from => $request);
372 0   0     0 $opts{input} //= $identifier->uuid;
373 0   0     0 $opts{displayname} //= $identifier->{displayname}; # steal the raw value here.
374             } elsif ($request =~ Data::Identifier->RE_UUID) {
375 0   0     0 $opts{input} //= $request;
376             } else {
377 0         0 croak 'Invalid format for tag-based generator: '.$request;
378             }
379             } elsif ($style eq 'tagcombiner') {
380 0 0       0 if (ref($request) eq 'ARRAY') {
381 0         0 my %uuids;
382              
383 0         0 foreach my $entry (@{$request}) {
  0         0  
384 0 0       0 if (ref($entry)) {
    0          
385 0         0 $uuids{Data::Identifier->new(from => $entry)->uuid} = undef;
386             } elsif ($entry =~ Data::Identifier->RE_UUID) {
387 0         0 $uuids{$entry} = undef;
388             } else {
389 0         0 croak 'Invalid format for tag-based generator: '.$entry;
390             }
391             }
392              
393 0 0       0 croak 'Less than two tags being combined' unless scalar(keys %uuids) > 1;
394              
395 0   0     0 $opts{input} //= join(',', sort keys %uuids);
396             } else {
397 0         0 croak 'Invalid request for tagcombiner generator: '.$request;
398             }
399             } elsif ($style eq 'colour') {
400 5 50       7 if (defined $request) {
401 5         8 my $req = lc($request);
402              
403 5 50       37 $req = sprintf('#%s%s%s', $1 x 6, $2 x 6, $3 x 6) if $req =~ /^#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/;
404              
405 5 50       11 if ($req =~ /^#[a-f0-9]{36}$/) {
406 5   33     19 $opts{input} //= $req;
407             } else {
408 0         0 croak 'Bad format for colour';
409             }
410             }
411             } else {
412 0         0 croak 'Style not supported: '.$style;
413             }
414             }
415              
416 76 50       97 croak 'No valid style/request or input is provided' unless defined $opts{input};
417              
418             {
419 76 100       70 my $ns = ref($opts{namespace}) ? $opts{namespace}->uuid(no_defaults => 1) : $opts{namespace};
  76         153  
420 76         140 my $uuid = $pkg->_uuid_v5($ns, $opts{input});
421 76         269 my $tag = Data::Identifier->new(uuid => $uuid, displayname => $opts{displayname});
422 76   66     378 $tag->{$_} //= $opts{$_} foreach qw(generator request);
423 76 100 66     244 $tag->{generator} = Data::Identifier->new(from => $tag->{generator}) if defined($tag->{generator}) && !ref($tag->{generator});
424 76         305 return $tag;
425             }
426             }
427              
428             # ---- Private helpers ----
429              
430             sub _available_module {
431 0     0   0 my (@modules) = @_;
432 0         0 state $tried = {};
433              
434 0         0 foreach my $module (@modules) {
435 0 0       0 if (exists $tried->{$module}) {
436 0 0       0 next unless $tried->{$module};
437 0         0 return $module;
438             } else {
439 0         0 my $res = eval {
440 0         0 my $modname = $module =~ s#::#/#gr;
441 0         0 require $modname.'.pm';
442 0         0 1;
443             };
444 0         0 $tried->{$module} = $res;
445 0 0       0 return $module if $res;
446             }
447             }
448              
449 0         0 croak 'Found none of modules ['.join(', ', @modules).']';
450             }
451              
452             sub _finish {
453 83     83   179 my ($raw, $version) = @_;
454 83         243 substr($raw, 6, 1, chr((ord(substr($raw, 6, 1)) & 0x0F) | ($version << 4)));
455 83         150 substr($raw, 8, 1, chr((ord(substr($raw, 8, 1)) & 0x3F) | 0x80));
456 83         678 return join('-', unpack('H8H4H4H4H12', $raw));
457             }
458              
459             sub _random {
460 0     0   0 my ($pkg, %opts) = @_;
461 0   0     0 my $sources = $opts{sources} // (state $default_sources = [qw(Crypt::URandom UUID4::Tiny Math::Random::Secure UUID::URandom UUID::Tiny::Patch::UseMRS)]);
462 0         0 my $source = _available_module(@{$sources});
  0         0  
463 0         0 my $raw;
464              
465             # Secure:
466 0 0       0 if ($source eq 'Crypt::URandom') {
    0          
    0          
    0          
    0          
    0          
    0          
467 0         0 $raw = Crypt::URandom::urandom(16);
468             } elsif ($source eq 'UUID4::Tiny') {
469 0         0 return UUID4::Tiny::create_uuid_string();
470             } elsif ($source eq 'Math::Random::Secure') {
471 0         0 $raw = join('', map {chr Math::Random::Secure::irand(256)} 0..15);
  0         0  
472             } elsif ($source eq 'UUID::URandom') {
473 0         0 return UUID::URandom::create_uuid_string();
474             } elsif ($source eq 'UUID::Tiny::Patch::UseMRS') {
475 0         0 return UUID::Tiny::create_uuid_as_string(UUID::Tiny::UUID_RANDOM());
476              
477             # Insecure:
478             } elsif ($source eq 'UUID::Tiny') {
479 0         0 return UUID::Tiny::create_uuid_as_string(UUID::Tiny::UUID_RANDOM());
480             } elsif ($source eq 'Data::UUID') {
481 0         0 require Data::UUID;
482 0         0 return Data::UUID->create_str;
483             #} elsif ($source eq '') {
484             } else {
485 0         0 croak 'Invalid/unsupported source';
486             }
487              
488 0 0 0     0 if (defined($raw) && length($raw) == 16) {
489 0         0 return _finish($raw, 4);
490             }
491              
492 0         0 croak 'Bug!';
493             }
494              
495             sub _uuid_v5 {
496 83     83   130 my ($pkg, $ns, $data) = @_;
497 83         207 my $digest = Digest->new('SHA-1');
498              
499 83 50       10969 $ns = $ns->uuid(no_defaults => 1) if ref $ns;
500              
501 83         312 $ns = pack('H*', $ns =~ tr/-//dr);
502              
503 83 50       158 croak 'Invalid namespace given' unless length($ns) == 16;
504              
505 83         239 $digest->add($ns);
506 83         137 $digest->add($data);
507              
508 83         420 return _finish(substr($digest->digest, 0, 16), 5);
509             }
510              
511             1;
512              
513             __END__