File Coverage

blib/lib/Locale/MO/File.pm
Criterion Covered Total %
statement 101 101 100.0
branch 35 42 83.3
condition 9 15 60.0
subroutine 17 17 100.0
pod 1 1 100.0
total 163 176 92.6


line stmt bran cond sub pod time code
1             package Locale::MO::File; ## no critic (TidyCode)
2            
3 8     8   909327 use strict;
  8         62  
  8         236  
4 8     8   44 use warnings;
  8         16  
  8         258  
5 8     8   3022 use charnames qw(:full);
  8         179347  
  8         55  
6 8     8   5768 use namespace::autoclean;
  8         137357  
  8         38  
7 8     8   743 use Carp qw(confess);
  8         17  
  8         389  
8 8     8   3784 use Const::Fast qw(const);
  8         8105  
  8         51  
9 8     8   5117 use Encode qw(find_encoding);
  8         76295  
  8         575  
10 8     8   1027 use English qw(-no_match_vars $INPUT_RECORD_SEPARATOR $OS_ERROR);
  8         6663  
  8         60  
11             require IO::File;
12 8     8   5559 use Moo;
  8         54986  
  8         40  
13 8     8   15161 use MooX::StrictConstructor;
  8         108195  
  8         38  
14 8     8   198551 use MooX::Types::MooseLike::Base qw(Bool Str ArrayRef FileHandle);
  8         53093  
  8         805  
15 8     8   4622 use Params::Validate qw(validate_with SCALAR ARRAYREF);
  8         22369  
  8         1211  
16            
17             our $VERSION = '0.09';
18            
19             const my $INTEGER_LENGTH => length pack 'N', 0;
20             const my $REVISION_OFFSET => $INTEGER_LENGTH;
21             const my $MAPS_OFFSET => $INTEGER_LENGTH * 7;
22             const my $MAGIC_NUMBER => 0x95_04_12_DE;
23             const our $CONTEXT_SEPARATOR => "\N{END OF TRANSMISSION}";
24             const our $PLURAL_SEPARATOR => "\N{NULL}";
25            
26             has filename => (
27             is => 'rw',
28             isa => Str,
29             reader => 'get_filename',
30             writer => 'set_filename',
31             clearer => 'clear_filename',
32             );
33             has file_handle => (
34             is => 'rw',
35             isa => FileHandle,
36             reader => 'get_file_handle',
37             writer => 'set_file_handle',
38             clearer => 'clear_file_handle',
39             );
40             has encoding => (
41             is => 'rw',
42             isa => Str,
43             reader => 'get_encoding',
44             writer => 'set_encoding',
45             clearer => 'clear_encoding',
46             );
47             has newline => (
48             is => 'rw',
49             isa => Str,
50             reader => 'get_newline',
51             writer => 'set_newline',
52             clearer => 'clear_newline',
53             );
54             has is_big_endian => (
55             is => 'rw',
56             isa => Bool,
57             reader => 'is_big_endian',
58             writer => 'set_is_big_endian',
59             clearer => 'clear_is_big_endian',
60             );
61             has messages => (
62             is => 'rw',
63             isa => ArrayRef,
64             default => sub { return [] },
65             lazy => 1,
66             reader => 'get_messages',
67             writer => 'set_messages',
68             );
69            
70             sub _encode_and_replace_newline {
71 142     142   270 my ($self, $string) = @_;
72            
73 142 100       322 if ( $self->get_encoding ) {
74 40 50       97 my $encoder = find_encoding( $self->get_encoding )
75             or confess 'Can not find encoding for ', $self->get_encoding;
76 40         1421 $string = $encoder->encode($string);
77             }
78 142 100       293 if ( $self->get_newline ) {
79 40         105 $string =~ s{ \r? \n }{ $self->get_newline }xmsge;
  12         50  
80             }
81            
82 142         267 return $string;
83             }
84            
85             sub _decode_and_replace_newline {
86 88     88   240 my ($self, $string) = @_;
87            
88 88 100       226 if ( $self->get_encoding ) {
89 36 50       126 my $encoder = find_encoding( $self->get_encoding )
90             or confess 'Can not find encoding for ', $self->get_encoding;
91 36         1129 $string = $encoder->decode($string, Encode::FB_CROAK);
92             }
93 88 100       367 if ( $self->get_newline ) {
94 36         100 $string =~ s{ \r? \n }{ $self->get_newline }xmsge;
  12         55  
95             }
96            
97 88         317 return $string;
98             }
99            
100             sub _pack_message {
101 71     71   16602 my ($self, $message) = @_;
102            
103             my ($msgid, $msgstr) = map {
104 71         119 ( exists $message->{$_} && defined $message->{$_} )
105 142 100 66     635 ? $message->{$_}
106             : q{};
107             } qw(msgid msgstr);
108            
109             # original
110             $msgid = $self->_encode_and_replace_newline(
111             (
112             (
113             exists $message->{msgctxt}
114             && defined $message->{msgctxt}
115             && length $message->{msgctxt}
116             )
117             ? $message->{msgctxt} . $CONTEXT_SEPARATOR . $msgid
118             : $msgid
119             )
120             . (
121             (
122             exists $message->{msgid_plural}
123             && defined $message->{msgid_plural}
124             && length $message->{msgid_plural}
125             )
126             ? $PLURAL_SEPARATOR . $message->{msgid_plural}
127 71 100 66     603 : q{}
    100 66        
128             ),
129             );
130            
131             # translation
132             $msgstr = $self->_encode_and_replace_newline(
133             length $msgstr
134             ? $msgstr
135             : join
136             $PLURAL_SEPARATOR,
137             map {
138 69 50       189 defined $_ ? $_ : q{}
139 71 50       200 } @{ $message->{msgstr_plural} || [] }
  33 100       87  
140             );
141            
142             return {
143 71         349 msgid => $msgid,
144             msgstr => $msgstr,
145             };
146             }
147            
148             sub _unpack_message {
149 48     48   91 my ($self, $message) = @_;
150            
151             my ($msgid, $msgstr) = map {
152             ( defined && length )
153 96 100 66     519 ? $self->_decode_and_replace_newline($_)
154             : q{};
155 48         85 } @{$message}{qw(msgid msgstr)};
  48         99  
156            
157             # return value
158 48         95 my %message;
159            
160             # split original
161 48         315 my @strings = split m{ \Q$CONTEXT_SEPARATOR\E }xms, $msgid;
162 48 100       128 if ( @strings > 1 ) {
163 16         61 ( $message{msgctxt}, $msgid ) = @strings;
164             }
165 48         200 my @plurals = split m{ \Q$PLURAL_SEPARATOR\E }xms, $msgid;
166 48         102 my $is_plural = @plurals > 1;
167 48 100       139 if ( $is_plural ) {
168 22         148 @message{qw(msgid msgid_plural)} = @plurals;
169             }
170             else {
171 26         69 $message{msgid} = $msgid;
172             }
173            
174             # split translation
175 48         234 @plurals = split m{ \Q$PLURAL_SEPARATOR\E }xms, $msgstr, -1; ## no critic (MagicNumbers)
176 48 100       117 if ( $is_plural ) {
177 22         56 $message{msgstr_plural} = \@plurals;
178             }
179             else {
180 26         67 $message{msgstr} = $plurals[0];
181             }
182            
183 48         268 return \%message;
184             }
185            
186             before 'write_file' => sub {
187             my $self = shift;
188            
189             my $index = 0;
190             my $chars_callback = sub {
191             my $string = shift;
192             STRING: for ( ref $string ? @{$string} : $string ) {
193             defined
194             or next STRING;
195             m{ \Q$CONTEXT_SEPARATOR\E | \Q$PLURAL_SEPARATOR\E }xmso
196             and return;
197             }
198             return 1;
199             };
200             for my $message ( @{ $self->get_messages } ) {
201             validate_with(
202             params => (
203             ref $message eq 'HASH'
204             ? $message
205             : confess "messages[$index] is not a hash reference"
206             ),
207             spec => {
208             msgctxt => {
209             type => SCALAR,
210             optional => 1,
211             callbacks => {
212             'no control chars' => $chars_callback,
213             },
214             },
215             msgid => {
216             type => SCALAR,
217             optional => 1,
218             callbacks => {
219             'no control chars' => $chars_callback,
220             },
221             },
222             msgid_plural => {
223             type => SCALAR,
224             optional => 1,
225             callbacks => {
226             'no control chars' => $chars_callback,
227             },
228             },
229             msgstr => {
230             type => SCALAR,
231             optional => 1,
232             callbacks => {
233             'no control chars' => $chars_callback,
234             },
235             },
236             msgstr_plural => {
237             type => ARRAYREF,
238             optional => 1,
239             callbacks => {
240             'msgstr not set' => sub {
241             return ! (
242             exists $message->{msgstr_plural}
243             && exists $message->{msgstr}
244             );
245             },
246             'no control chars' => $chars_callback,
247             },
248             },
249             },
250             called => "messages[$index]",
251             );
252             ++$index;
253             }
254            
255             return $self;
256             };
257            
258             sub write_file {
259             my $self = shift;
260            
261             my $messages = [
262             sort {
263             $a->{msgid} cmp $b->{msgid};
264             }
265             map {
266             $self->_pack_message($_);
267             } @{ $self->get_messages }
268             ];
269            
270             my $number_of_strings = @{$messages};
271            
272             # Set the byte order of the MO file creator
273             my $template = $self->is_big_endian ? q{N} : q{V};
274            
275             my $maps = q{};
276             my $strings = q{};
277             my $current_offset
278             = $MAPS_OFFSET
279             # length of map
280             + $INTEGER_LENGTH * 4 * $number_of_strings; ## no critic (MagicNumbers)
281             for my $key (qw(msgid msgstr)) {
282             for my $message ( @{$messages} ) {
283             my $string = $message->{$key};
284             my $length = length $string;
285             my $map = pack $template x 2, $length, $current_offset;
286             $maps .= $map;
287             $string .= $PLURAL_SEPARATOR;
288             $strings .= $string;
289             $current_offset += length $string;
290             }
291             }
292            
293             my $offset_original
294             = $MAPS_OFFSET;
295             my $offset_translated
296             = $MAPS_OFFSET
297             + $INTEGER_LENGTH * 2 * $number_of_strings;
298             my $content
299             = (
300             pack $template x 7, ## no critic (MagicNumbers)
301             $MAGIC_NUMBER,
302             0, # revision
303             $number_of_strings,
304             $offset_original,
305             $offset_translated,
306             0, # hash size
307             0, # hash offset
308             )
309             . $maps
310             . $strings;
311            
312             my $filename = $self->get_filename;
313             defined $filename
314             or confess 'Filename not set';
315             my $file_handle
316             = $self->get_file_handle
317             || IO::File->new($filename, '> :raw')
318             || confess "Can not open mo file $filename $OS_ERROR";
319             $file_handle->print($content)
320             or confess "Can not write mo file $filename $OS_ERROR";
321             if ( ! $self->get_file_handle ) {
322             $file_handle->close
323             or confess "Can not close mo file $filename $OS_ERROR";
324             }
325            
326             return $self;
327             }
328            
329             sub read_file {
330 9     9 1 101206 my $self = shift;
331            
332 9         42 my $filename = $self->get_filename;
333 9 50       38 defined $filename
334             or confess 'filename not set';
335 9   33     79 my $file_handle
336             = $self->get_file_handle
337             || IO::File->new($filename, '< :raw')
338             || confess "Can not open mo file $filename $OS_ERROR";
339 9         641 my $content = do {
340 9         46 local $INPUT_RECORD_SEPARATOR = ();
341 9         293 <$file_handle>;
342             };
343 9 100       76 if ( ! $self->get_file_handle ) {
344 5         30 $file_handle->close;
345             }
346            
347             # Find the byte order of the MO file creator
348 9         130 my $magic_number = substr $content, 0, $INTEGER_LENGTH;
349 9 50       77 my $template =
    100          
350             ( $magic_number eq pack 'V', $MAGIC_NUMBER )
351             # Little endian
352             ? q{V}
353             : ( $magic_number eq pack 'N', $MAGIC_NUMBER )
354             # Big endian
355             ? q{N}
356             # Wrong magic number. Not a valid MO file.
357             : confess "MO file expected: $filename";
358            
359 9         95 my ($revision, $number_of_strings, $offset_original, $offset_translated)
360             = unpack
361             $template x 4, ## no critic (MagicNumbers)
362             substr
363             $content,
364             $REVISION_OFFSET,
365             $INTEGER_LENGTH * 4; ## no critic (MagicNumbers)
366 9 50       58 $revision > 0
367             and confess "Revision > 0 is unknown: $revision";
368            
369 9         244 $self->set_messages(\my @messages);
370 9         431 for my $index (0 .. $number_of_strings - 1) {
371 48         102 my $key = 'msgid';
372 48         69 my $message;
373 48         101 for my $offset ($offset_original, $offset_translated) {
374 96         497 my ($string_length, $string_offset)
375             = unpack
376             $template x 2,
377             substr
378             $content,
379             $offset + $index * $INTEGER_LENGTH * 2,
380             $INTEGER_LENGTH * 2;
381 96         353 $message->{$key}
382             = substr $content, $string_offset, $string_length;
383 96         293 $key = 'msgstr';
384             }
385 48         123 $messages[$index] = $self->_unpack_message($message);
386             }
387            
388 9         55 return $self;
389             }
390            
391             __PACKAGE__->meta->make_immutable;
392            
393             1;
394            
395             __END__