File Coverage

lib/Data/Identifier/Generate.pm
Criterion Covered Total %
statement 163 268 60.8
branch 71 170 41.7
condition 69 173 39.8
subroutine 20 23 86.9
pod 9 9 100.0
total 332 643 51.6


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   219122 use v5.20;
  3         6  
11 3     3   11 use strict;
  3         4  
  3         51  
12 3     3   11 use warnings;
  3         3  
  3         125  
13              
14 3     3   12 use Carp;
  3         3  
  3         247  
15 3     3   13 use Encode qw(encode);
  3         11  
  3         202  
16 3     3   1429 use Digest;
  3         1686  
  3         82  
17              
18 3     3   561 use Data::Identifier;
  3         5  
  3         16  
19              
20             use constant {
21 3         751 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   17 };
  3         4  
32              
33              
34             our $VERSION = v0.31;
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             my %_generators = (
85             WK_UNSIGNED_INTEGER_GENERATOR() => {
86             style => 'integer-based',
87             namespace => Data::Identifier->NS_INT(),
88             },
89             WK_SIGNED_INTEGER_GENERATOR() => {
90             style => 'integer-based',
91             namespace => Data::Identifier->NS_INT(),
92             },
93             WK_UNICODE_CHARACTER_GENERATOR() => {},
94             WK_RGB_COLOUR_GENERATOR() => {
95             style => 'colour',
96             namespace => '88d3944f-a13b-4e35-89eb-e3c1fbe53e76',
97             },
98             WK_DATE_GENERATOR() => {
99             namespace => Data::Identifier->NS_DATE(),
100 3     3   1899 icontext => "\N{TEAR-OFF CALENDAR}",
  3         19146  
  3         16  
101             },
102             #WK_LANGUAGE_GENERATOR() => {},
103             WK_MULTIPLICITY_GENERATOR() => {
104             namespace => NS_GTE,
105             },
106             WK_MINIMUM_MULTIPLICITY_GENERATOR() => {
107             namespace => NS_GTE,
108             },
109             );
110              
111              
112             #@returns Data::Identifier
113             sub integer {
114 70     70 1 2932 my ($pkg, $request, %opts) = @_;
115 70         95 $opts{request} = $request;
116 70   33     181 $opts{displayname}//= $request;
117 70 100       134 $opts{generator} = $request >= 0 ? WK_UNSIGNED_INTEGER_GENERATOR : WK_SIGNED_INTEGER_GENERATOR;
118              
119             # We currently don't set one for $request == 0
120 70 100       94 if ($request > 0) {
    100          
121 66   50     162 $opts{icontext} //= "\N{DOUBLE-STRUCK CAPITAL N}";
122             } elsif ($request < 0) {
123 1   50     5 $opts{icontext} //= "\N{DOUBLE-STRUCK CAPITAL Z}";
124             }
125              
126 70         159 return $pkg->generic(%opts);
127             }
128              
129              
130             #@returns Data::Identifier
131             sub unicode_character {
132 3     3 1 2145 my ($pkg, $type, $request, %opts) = @_;
133 3         59 my $unicode_cp;
134             my $unicode_cp_str;
135              
136 3 50       12 croak 'No type given' unless defined $type;
137 3 50 33     16 croak 'No/Bad request given' unless defined($request) && length($request);
138              
139 3 50       18 if ($type eq 'unicode') {
    50          
    50          
140 0 0       0 if ($request =~ /^[Uu]\+([0-9a-fA-F]+)$/) {
    0          
141 0         0 $unicode_cp = hex($1);
142             } elsif ($request =~ /^[0-9]+\z/) {
143 0         0 $unicode_cp = int($request);
144             } else {
145 0         0 croak 'Bad request given: '.$request;
146             }
147             } elsif ($type eq 'ascii') {
148 0 0       0 if ($request =~ /^[0-9]+\z/) {
149 0         0 $unicode_cp = int($request);
150             } else {
151 0         0 croak 'Bad request given: '.$request;
152             }
153 0 0 0     0 croak 'US-ASCII character out of range: '.$unicode_cp if $unicode_cp < 0 || $unicode_cp > 0x7F;
154             } elsif ($type eq 'raw') {
155 3 50       10 croak 'Raw value is not exactly one character long' unless length($request) == 1;
156 3         6 $unicode_cp = ord($request);
157             } else {
158 0         0 croak 'Bad type given: '.$type;
159             }
160              
161 3 50 33     14 croak 'Unicode character out of range: '.$unicode_cp if $unicode_cp < 0 || $unicode_cp > 0x10FFFF;
162              
163 3         13 $unicode_cp_str = sprintf('U+%04X', $unicode_cp);
164              
165 3 50 33     23 if ($unicode_cp == 0xFFFC || $unicode_cp == 0xFFFD || $unicode_cp == 0xFEFF || $unicode_cp == 0xFFFE) {
      33        
      33        
166 0 0       0 croak 'Rejected use of special character: '.$unicode_cp_str unless $opts{allow_special};
167             }
168              
169 3   33     17 $opts{displayname} //= $unicode_cp_str;
170              
171 3         14 return Data::Identifier->new(unicodecp => $unicode_cp_str, displayname => $opts{displayname}, generator => WK_UNICODE_CHARACTER_GENERATOR, request => $unicode_cp_str);
172             }
173              
174              
175             #@returns Data::Identifier
176             sub colour {
177 5     5 1 4240 my ($pkg, $colour, %opts) = @_;
178 5         14 $opts{request} = $colour;
179 5         11 $opts{generator} = WK_RGB_COLOUR_GENERATOR;
180              
181 5         21 return $pkg->generic(%opts);
182             }
183              
184              
185             #@returns Data::Identifier
186             sub date {
187 9     9 1 7136 my ($pkg, $request, %opts) = @_;
188 9         31 my ($year, $month, $day);
189 9         0 my $precision;
190              
191 9 50       28 if (ref($request)) {
192 0 0       0 if (eval {$request->can('epoch')}) {
  0         0  
193 0         0 $request = $request->epoch;
194             } else {
195 0         0 return $pkg->date(scalar($request->()), %opts);
196             }
197             }
198              
199 9         59 ($year, $month, $day) = $request =~ /^([12][0-9]{3})(?:-([01][0-9])(?:-([0-3][0-9]))?)?Z$/;
200              
201 9 100 100     72 unless (length($year // '') == 4) {
202 3 50 33     32 if ($request eq 'now' || $request eq 'today') {
    50          
203 0         0 $request = time();
204             } elsif ($request =~ /^(?:0|-?[1-9][0-9]*)$/) {
205 3         7 $request = int($request);
206 3 50       9 if ($request > 32503680000) {
207 0         0 croak 'Unlikely far date given. Likely miliseconds are passed as seconds?';
208             }
209             } else {
210 0         0 croak 'Invalid format';
211             }
212              
213 3         16 (undef,undef,undef,$day,$month,$year) = gmtime($request);
214 3         8 $year += 1900;
215 3         6 $month += 1;
216             }
217              
218 9         19 foreach my $entry ($year, $month, $day) {
219 27   100     69 $entry = int($entry // 0);
220             }
221              
222 9 50 33     51 croak 'Invalid year' if $year && ($year < 1583 || $year > 9999);
      33        
223 9 50 33     43 croak 'Invalid month' if $month && ($month < 1 || $month > 12);
      66        
224 9 50 33     37 croak 'Invalid day' if $day && ($day < 1 || $day > 31);
      66        
225              
226 9 50       20 $month = 0 unless $year;
227 9 100       19 $day = 0 unless $month;
228              
229 9 100 66     75 $precision = $opts{precision} // ($day ? 'day' : undef) // ($month ? 'month' : undef) // 'year';
    100 100        
      100        
230 9 100 66     45 if ($precision eq 'day' && $day) {
    100 66        
    50 33        
231 7         33 $request = sprintf('%04u-%02u-%02uZ', $year, $month, $day);
232             } elsif ($precision eq 'month' && $month) {
233 1         6 $request = sprintf('%04u-%02uZ', $year, $month);
234             } elsif ($precision eq 'year' && $year) {
235 1         5 $request = sprintf('%04uZ', $year);
236             } else {
237 0         0 croak 'Bad precision: '.$precision;
238             }
239              
240 9         24 $opts{request} = $request;
241 9   33     44 $opts{input} //= $request; # force raw value!
242 9         18 $opts{style} = undef;
243 9   33     38 $opts{displayname}//= $request;
244 9         19 $opts{generator} = WK_DATE_GENERATOR;
245              
246 9         39 return $pkg->generic(%opts);
247             }
248              
249              
250             #@returns Data::Identifier
251             sub language {
252 8     8 1 6230 my ($pkg, $req, %opts) = @_;
253 8         19 my $name;
254             my $identifier;
255              
256 8         975 require I18N::LangTags::List;
257              
258 8         9734 $opts{request} = $req;
259 8         20 $opts{style} = 'id-based';
260 8         16 $opts{namespace} = '47dd950c-9089-4956-87c1-54c122533219';
261             #$opts{generator} = WK_LANGUAGE_GENERATOR;
262              
263 8 50       24 croak 'Bad language: '.$req unless I18N::LangTags::List::is_decent($req);
264              
265 8         285 $name = I18N::LangTags::List::name($req);
266              
267 8 50 33     225 unless (defined($name) && length($name)) {
268 0         0 croak 'Bad language: '.$req;
269             }
270              
271 8   33     44 $opts{displayname} //= $name;
272 8         65 $identifier = $pkg->generic(%opts);
273              
274 8   50     30 $identifier->{id_cache} //= {};
275 8   33     22 $identifier->{id_cache}->{'d0a4c6e2-ce2f-4d4c-b079-60065ac681f1'} //= $req;
276              
277 8         24 return $identifier;
278             }
279              
280              
281             #@returns Data::Identifier
282             sub multiplicity {
283 8     8 1 6164 my ($pkg, $subtype, $request, %opts) = @_;
284 8   33     37 my $prefix = $_multiplicity_prefix{$subtype} // croak 'Invalid subtype: '.$subtype;
285 8         16 my $identifier;
286             my $oid;
287              
288 8 50 66     87 croak 'Invalid value: '.$request unless $request eq '0' || $request =~ /^[1-9][0-9]*$/;
289              
290 8         20 $oid = '1.3.6.1.4.1.46942.16.2.'.$prefix.'.'.$request;
291              
292 8         20 $opts{request} = $request;
293 8         104 $opts{input} = $prefix.'.'.$request;
294 8   33     70 $opts{displayname}//= $_multiplicity_names{$request};
295 8   33     21 $opts{displayname}//= $request;
296 8         22 $opts{generator} = $_multiplicity_generators{$subtype};
297 8         35 $identifier = $pkg->generic(%opts);
298              
299 8   50     27 $identifier->{id_cache} //= {};
300 8   33     41 $identifier->{id_cache}->{Data::Identifier->WK_OID} //= $oid;
301              
302 8 50       12 if (defined $_multiplicity_names{$request}) {
303 8         17 $identifier->register;
304             }
305              
306 8         25 return $identifier;
307             }
308              
309              
310             #@returns Data::Identifier
311             sub gte_simple {
312 0     0 1 0 my ($pkg, $profile, $request, %opts) = @_;
313 0         0 my %order;
314             my $normal;
315              
316 0 0       0 croak 'Called in list context' if wantarray;
317              
318 0 0       0 $profile = $profile->ise if eval {$profile->can('ise')};
  0         0  
319 0   0     0 $profile = $_gte_simple_profiles{$profile} // $profile;
320              
321             {
322 0         0 my $i = 0;
  0         0  
323 0         0 %order = map {$_ => $i++} split(//, $profile->{order});
  0         0  
324             }
325              
326 0 0       0 if (defined(my $folding = $profile->{case_folding})) {
327 0 0       0 if ($folding eq 'none') {
    0          
    0          
328             # no-op
329             } elsif ($folding eq 'upper') {
330 0         0 $request = uc($request);
331             } elsif ($folding eq 'lower') {
332 0         0 $request = lc($request);
333             } else {
334 0         0 croak 'Unsupported/invalid folding rule: '.$folding;
335             }
336             }
337              
338 0 0       0 if ($profile->{strip_slash}) {
339 0         0 $request =~ s#/+##g;
340             }
341              
342 0 0       0 if ($profile->{strip_spaces}) {
343 0         0 $request =~ s#\s+##g;
344             }
345              
346             $normal = join('',
347 0         0 sort {$order{$a} <=> $order{$b}}
348 0 0       0 map {croak 'Invalid input element: '.$_ unless defined $order{$_}; $_}
  0         0  
  0         0  
349             split //, $request);
350              
351 0 0       0 if (defined(my $info = delete $opts{info})) {
352 0         0 $info->{count} = length($normal);
353 0         0 $info->{request} = $normal;
354             }
355              
356 0         0 $opts{input} = $normal;
357 0         0 $opts{request} = $normal;
358 0   0     0 $opts{namespace} //= $profile->{namespace};
359 0   0     0 $opts{displayname}//= $normal;
360 0         0 return $pkg->generic(%opts);
361             }
362              
363              
364             #@returns Data::Identifier
365             sub unit {
366 7     7 1 5585 my ($pkg, $request, %opts) = @_;
367              
368 7         755 require Data::Identifier::Util;
369              
370 7         39 return Data::Identifier::Util->render_unit_request('Data::Identifier' => $request, %opts);
371             }
372              
373              
374             #@returns Data::Identifier
375             sub generic {
376 128     128 1 375 my ($pkg, %opts) = @_;
377 128         173 my $generator = $opts{generator};
378              
379 128 100       197 if (defined($generator)) {
380 120 100       306 $generator = Data::Identifier->new(from => $generator) unless ref($generator);
381              
382 120   100     177 %opts = (%{$_generators{$generator->ise}//{}}, %opts);
  120         248  
383             }
384              
385 128 50       305 if (defined(my $type = $opts{type})) {
386 0   0     0 $opts{namespace} //= $type->namespace;
387             }
388              
389 128 100 66     331 if (defined(my $style = $opts{style}) && defined(my $request = $opts{request})) {
390 105 100       225 if ($style eq 'integer-based') {
    100          
    100          
    50          
    50          
    50          
391 70 50       223 croak 'Invalid request' unless $request =~ /^(?:0|-?[1-9][0-9]*)$/;
392 70   66     172 $opts{input} //= $request;
393             } elsif ($style eq 'id-based') {
394 9         18 my ($input, $name);
395              
396 9 100       64 if (($input, $name) = $request =~ /^(#?[a-zA-Z0-9\-\.\+]+) (.+)$/) {
    50          
397             # noop
398             } elsif (($input) = $request =~ /^(#?[a-zA-Z0-9\-\.\+]+)$/) {
399 8         15 $name = undef;
400             } else {
401 0         0 croak 'Invalid format, expected: "id name", or "id", got: '.$request;
402             }
403              
404 9   33     62 $opts{input} //= lc($input);
405 9   66     26 $opts{displayname} //= $name;
406              
407             } elsif ($style eq 'name-based') {
408 21   33     189 $opts{input} //= encode('UTF-8', $request);
409 21   33     614 $opts{displayname} //= $request;
410             } elsif ($style eq 'tag-based') {
411 0 0       0 if (ref($request)) {
    0          
412 0         0 my $identifier = Data::Identifier->new(from => $request);
413 0   0     0 $opts{input} //= $identifier->uuid;
414 0   0     0 $opts{displayname} //= $identifier->{displayname}; # steal the raw value here.
415             } elsif ($request =~ Data::Identifier->RE_UUID) {
416 0   0     0 $opts{input} //= $request;
417             } else {
418 0         0 croak 'Invalid format for tag-based generator: '.$request;
419             }
420             } elsif ($style eq 'tagcombiner') {
421 0 0       0 if (ref($request) eq 'ARRAY') {
422 0         0 my %uuids;
423              
424 0         0 foreach my $entry (@{$request}) {
  0         0  
425 0 0       0 if (ref($entry)) {
    0          
426 0         0 $uuids{Data::Identifier->new(from => $entry)->uuid} = undef;
427             } elsif ($entry =~ Data::Identifier->RE_UUID) {
428 0         0 $uuids{$entry} = undef;
429             } else {
430 0         0 croak 'Invalid format for tag-based generator: '.$entry;
431             }
432             }
433              
434 0 0       0 croak 'Less than two tags being combined' unless scalar(keys %uuids) > 1;
435              
436 0   0     0 $opts{input} //= join(',', sort keys %uuids);
437             } else {
438 0         0 croak 'Invalid request for tagcombiner generator: '.$request;
439             }
440             } elsif ($style eq 'colour') {
441 5 50       12 if (defined $request) {
442 5         13 my $req = lc($request);
443              
444 5 50       51 if (exists &Data::URIID::Colour::new) {
445 0   0     0 eval { $opts{displaycolour} //= Data::URIID::Colour->new(rgb => $request) };
  0         0  
446             }
447              
448 5 50       59 $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})$/;
449              
450 5 50       18 if ($req =~ /^#[a-f0-9]{36}$/) {
451 5   33     24 $opts{input} //= $req;
452             } else {
453 0         0 croak 'Bad format for colour';
454             }
455             }
456             } else {
457 0         0 croak 'Style not supported: '.$style;
458             }
459             }
460              
461 128 100       431 croak 'No valid style/request or input is provided' unless defined $opts{input};
462              
463             {
464 127 100       125 my $ns = ref($opts{namespace}) ? $opts{namespace}->uuid(no_defaults => 1) : $opts{namespace};
  127         223  
465 127         287 my $uuid = $pkg->_uuid_v5($ns, $opts{input});
466 127         459 my $tag = Data::Identifier->new(uuid => $uuid, displayname => $opts{displayname});
467 127         135 foreach my $key (qw(request displaycolour description icontext)) {
468 508 100 100     910 $tag->{$key} //= $opts{$key} if defined $opts{$key};
469             }
470 127   100     462 $tag->{generator} //= $generator;
471 127         521 return $tag;
472             }
473             }
474              
475             # ---- Private helpers ----
476              
477             sub _register_generator {
478 1     1   3 my ($pkg, $generator, %opts) = @_;
479 1   50     2 my $g = $_generators{$generator->ise} //= {};
480              
481 1         3 foreach my $key (keys %opts) {
482 3   100     5 my $v = $opts{$key} // next;
483              
484 2   33     7 $g->{$key} //= $v;
485             }
486             }
487              
488             sub _available_module {
489 0     0   0 my (@modules) = @_;
490 0         0 state $tried = {};
491              
492 0         0 foreach my $module (@modules) {
493 0 0       0 if (exists $tried->{$module}) {
494 0 0       0 next unless $tried->{$module};
495 0         0 return $module;
496             } else {
497 0         0 my $res = eval {
498 0         0 my $modname = $module =~ s#::#/#gr;
499 0         0 require $modname.'.pm';
500 0         0 1;
501             };
502 0         0 $tried->{$module} = $res;
503 0 0       0 return $module if $res;
504             }
505             }
506              
507 0         0 croak 'Found none of modules ['.join(', ', @modules).']';
508             }
509              
510             sub _finish {
511 136     136   353 my ($raw, $version) = @_;
512 136         419 substr($raw, 6, 1, chr((ord(substr($raw, 6, 1)) & 0x0F) | ($version << 4)));
513 136         210 substr($raw, 8, 1, chr((ord(substr($raw, 8, 1)) & 0x3F) | 0x80));
514 136         1226 return join('-', unpack('H8H4H4H4H12', $raw));
515             }
516              
517             sub _random {
518 0     0   0 my ($pkg, %opts) = @_;
519 0   0     0 my $sources = $opts{sources} // (state $default_sources = [qw(Crypt::URandom UUID4::Tiny Math::Random::Secure UUID::URandom UUID::Tiny::Patch::UseMRS)]);
520 0         0 my $source = _available_module(@{$sources});
  0         0  
521 0         0 my $raw;
522              
523             # Secure:
524 0 0       0 if ($source eq 'Crypt::URandom') {
    0          
    0          
    0          
    0          
    0          
    0          
525 0         0 $raw = Crypt::URandom::urandom(16);
526             } elsif ($source eq 'UUID4::Tiny') {
527 0         0 return UUID4::Tiny::create_uuid_string();
528             } elsif ($source eq 'Math::Random::Secure') {
529 0         0 $raw = join('', map {chr Math::Random::Secure::irand(256)} 0..15);
  0         0  
530             } elsif ($source eq 'UUID::URandom') {
531 0         0 return UUID::URandom::create_uuid_string();
532             } elsif ($source eq 'UUID::Tiny::Patch::UseMRS') {
533 0         0 return UUID::Tiny::create_uuid_as_string(UUID::Tiny::UUID_RANDOM());
534              
535             # Insecure:
536             } elsif ($source eq 'UUID::Tiny') {
537 0         0 return UUID::Tiny::create_uuid_as_string(UUID::Tiny::UUID_RANDOM());
538             } elsif ($source eq 'Data::UUID') {
539 0         0 require Data::UUID;
540 0         0 return Data::UUID->create_str;
541             #} elsif ($source eq '') {
542             } else {
543 0         0 croak 'Invalid/unsupported source';
544             }
545              
546 0 0 0     0 if (defined($raw) && length($raw) == 16) {
547 0         0 return _finish($raw, 4);
548             }
549              
550 0         0 croak 'Bug!';
551             }
552              
553             sub _uuid_v5 {
554 136     136   245 my ($pkg, $ns, $data) = @_;
555 136         418 my $digest = Digest->new('SHA-1');
556              
557 136 50       12849 $ns = $ns->uuid(no_defaults => 1) if ref $ns;
558              
559 136         548 $ns = pack('H*', $ns =~ tr/-//dr);
560              
561 136 50       297 croak 'Invalid namespace given' unless length($ns) == 16;
562              
563 136         396 $digest->add($ns);
564 136         252 $digest->add($data);
565              
566 136         786 return _finish(substr($digest->digest, 0, 16), 5);
567             }
568              
569             1;
570              
571             __END__