File Coverage

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


line stmt bran cond sub pod time code
1             package Locale::MO::File; ## no critic (TidyCode)
2            
3 8     8   683048 use strict;
  8         66  
  8         201  
4 8     8   40 use warnings;
  8         11  
  8         202  
5 8     8   1946 use charnames qw(:full);
  8         154705  
  8         55  
6 8     8   4508 use namespace::autoclean;
  8         116471  
  8         41  
7 8     8   783 use Carp qw(confess);
  8         19  
  8         423  
8 8     8   2651 use Const::Fast qw(const);
  8         7959  
  8         51  
9 8     8   3628 use Encode qw(find_encoding);
  8         67452  
  8         558  
10 8     8   700 use English qw(-no_match_vars $INPUT_RECORD_SEPARATOR $OS_ERROR);
  8         5823  
  8         57  
11             require IO::File;
12 8     8   3818 use Moo;
  8         41830  
  8         44  
13 8     8   11310 use MooX::StrictConstructor;
  8         92447  
  8         39  
14 8     8   166648 use MooX::Types::MooseLike::Base qw(Bool Str ArrayRef FileHandle);
  8         41057  
  8         679  
15 8     8   2911 use Params::Validate qw(validate_with SCALAR ARRAYREF);
  8         16678  
  8         962  
16            
17             our $VERSION = '0.08';
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   257 my ($self, $string) = @_;
72            
73 142 100       305 if ( $self->get_encoding ) {
74 40 50       92 my $encoder = find_encoding( $self->get_encoding )
75             or confess 'Can not find encoding for ', $self->get_encoding;
76 40         1320 $string = $encoder->encode($string);
77             }
78 142 100       283 if ( $self->get_newline ) {
79 40         77 $string =~ s{ \r? \n }{ $self->get_newline }xmsge;
  12         43  
80             }
81            
82 142         251 return $string;
83             }
84            
85             sub _decode_and_replace_newline {
86 88     88   226 my ($self, $string) = @_;
87            
88 88 100       210 if ( $self->get_encoding ) {
89 36 50       85 my $encoder = find_encoding( $self->get_encoding )
90             or confess 'Can not find encoding for ', $self->get_encoding;
91 36         1011 $string = $encoder->decode($string, Encode::FB_CROAK);
92             }
93 88 100       333 if ( $self->get_newline ) {
94 36         92 $string =~ s{ \r? \n }{ $self->get_newline }xmsge;
  12         52  
95             }
96            
97 88         287 return $string;
98             }
99            
100             sub _pack_message {
101 71     71   13407 my ($self, $message) = @_;
102            
103             my ($msgid, $msgstr) = map {
104 71         122 ( exists $message->{$_} && defined $message->{$_} )
105 142 100 66     618 ? $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     574 : 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       163 defined $_ ? $_ : q{}
139 71 50       187 } @{ $message->{msgstr_plural} || [] }
  33 100       82  
140             );
141            
142             return {
143 71         315 msgid => $msgid,
144             msgstr => $msgstr,
145             };
146             }
147            
148             sub _unpack_message {
149 48     48   76 my ($self, $message) = @_;
150            
151             my ($msgid, $msgstr) = map {
152             ( defined && length )
153 96 100 66     477 ? $self->_decode_and_replace_newline($_)
154             : q{};
155 48         67 } @{$message}{qw(msgid msgstr)};
  48         97  
156            
157             # return value
158 48         84 my %message;
159            
160             # split original
161 48         293 my @strings = split m{ \Q$CONTEXT_SEPARATOR\E }xms, $msgid;
162 48 100       120 if ( @strings > 1 ) {
163 16         102 ( $message{msgctxt}, $msgid ) = @strings;
164             }
165 48         194 my @plurals = split m{ \Q$PLURAL_SEPARATOR\E }xms, $msgid;
166 48         91 my $is_plural = @plurals > 1;
167 48 100       92 if ( $is_plural ) {
168 22         74 @message{qw(msgid msgid_plural)} = @plurals;
169             }
170             else {
171 26         64 $message{msgid} = $msgid;
172             }
173            
174             # split translation
175             @plurals = split
176             m{ \Q$PLURAL_SEPARATOR\E }xms,
177             $msgstr,
178             # get back also all hanging empty stings
179 48         151 1 + do { my @separators = $msgstr =~ m{ \Q$PLURAL_SEPARATOR\E }xmsg };
  48         289  
180 48 100       112 if ( $is_plural ) {
181 22         46 $message{msgstr_plural} = \@plurals;
182             }
183             else {
184 26         58 $message{msgstr} = $plurals[0];
185             }
186            
187 48         242 return \%message;
188             }
189            
190             before 'write_file' => sub {
191             my $self = shift;
192            
193             my $index = 0;
194             my $chars_callback = sub {
195             my $string = shift;
196             STRING: for ( ref $string ? @{$string} : $string ) {
197             defined
198             or next STRING;
199             m{ \Q$CONTEXT_SEPARATOR\E | \Q$PLURAL_SEPARATOR\E }xmso
200             and return;
201             }
202             return 1;
203             };
204             for my $message ( @{ $self->get_messages } ) {
205             validate_with(
206             params => (
207             ref $message eq 'HASH'
208             ? $message
209             : confess "messages[$index] is not a hash reference"
210             ),
211             spec => {
212             msgctxt => {
213             type => SCALAR,
214             optional => 1,
215             callbacks => {
216             'no control chars' => $chars_callback,
217             },
218             },
219             msgid => {
220             type => SCALAR,
221             optional => 1,
222             callbacks => {
223             'no control chars' => $chars_callback,
224             },
225             },
226             msgid_plural => {
227             type => SCALAR,
228             optional => 1,
229             callbacks => {
230             'no control chars' => $chars_callback,
231             },
232             },
233             msgstr => {
234             type => SCALAR,
235             optional => 1,
236             callbacks => {
237             'no control chars' => $chars_callback,
238             },
239             },
240             msgstr_plural => {
241             type => ARRAYREF,
242             optional => 1,
243             callbacks => {
244             'msgstr not set' => sub {
245             return ! (
246             exists $message->{msgstr_plural}
247             && exists $message->{msgstr}
248             );
249             },
250             'no control chars' => $chars_callback,
251             },
252             },
253             },
254             called => "messages[$index]",
255             );
256             ++$index;
257             }
258            
259             return $self;
260             };
261            
262             sub write_file {
263             my $self = shift;
264            
265             my $messages = [
266             sort {
267             $a->{msgid} cmp $b->{msgid};
268             }
269             map {
270             $self->_pack_message($_);
271             } @{ $self->get_messages }
272             ];
273            
274             my $number_of_strings = @{$messages};
275            
276             # Set the byte order of the MO file creator
277             my $template = $self->is_big_endian ? q{N} : q{V};
278            
279             my $maps = q{};
280             my $strings = q{};
281             my $current_offset
282             = $MAPS_OFFSET
283             # length of map
284             + $INTEGER_LENGTH * 4 * $number_of_strings; ## no critic (MagicNumbers)
285             for my $key (qw(msgid msgstr)) {
286             for my $message ( @{$messages} ) {
287             my $string = $message->{$key};
288             my $length = length $string;
289             my $map = pack $template x 2, $length, $current_offset;
290             $maps .= $map;
291             $string .= $PLURAL_SEPARATOR;
292             $strings .= $string;
293             $current_offset += length $string;
294             }
295             }
296            
297             my $offset_original
298             = $MAPS_OFFSET;
299             my $offset_translated
300             = $MAPS_OFFSET
301             + $INTEGER_LENGTH * 2 * $number_of_strings;
302             my $content
303             = (
304             pack $template x 7, ## no critic (MagicNumbers)
305             $MAGIC_NUMBER,
306             0, # revision
307             $number_of_strings,
308             $offset_original,
309             $offset_translated,
310             0, # hash size
311             0, # hash offset
312             )
313             . $maps
314             . $strings;
315            
316             my $filename = $self->get_filename;
317             defined $filename
318             or confess 'Filename not set';
319             my $file_handle
320             = $self->get_file_handle
321             || IO::File->new($filename, '> :raw')
322             || confess "Can not open mo file $filename $OS_ERROR";
323             $file_handle->print($content)
324             or confess "Can not write mo file $filename $OS_ERROR";
325             if ( ! $self->get_file_handle ) {
326             $file_handle->close
327             or confess "Can not close mo file $filename $OS_ERROR";
328             }
329            
330             return $self;
331             }
332            
333             sub read_file {
334 9     9 1 94806 my $self = shift;
335            
336 9         39 my $filename = $self->get_filename;
337 9 50       33 defined $filename
338             or confess 'filename not set';
339 9   33     83 my $file_handle
340             = $self->get_file_handle
341             || IO::File->new($filename, '< :raw')
342             || confess "Can not open mo file $filename $OS_ERROR";
343 9         503 my $content = do {
344 9         44 local $INPUT_RECORD_SEPARATOR = ();
345 9         164 <$file_handle>;
346             };
347 9 100       54 if ( ! $self->get_file_handle ) {
348 5         25 $file_handle->close;
349             }
350            
351             # Find the byte order of the MO file creator
352 9         95 my $magic_number = substr $content, 0, $INTEGER_LENGTH;
353 9 50       76 my $template =
    100          
354             ( $magic_number eq pack 'V', $MAGIC_NUMBER )
355             # Little endian
356             ? q{V}
357             : ( $magic_number eq pack 'N', $MAGIC_NUMBER )
358             # Big endian
359             ? q{N}
360             # Wrong magic number. Not a valid MO file.
361             : confess "MO file expected: $filename";
362            
363 9         88 my ($revision, $number_of_strings, $offset_original, $offset_translated)
364             = unpack
365             $template x 4, ## no critic (MagicNumbers)
366             substr
367             $content,
368             $REVISION_OFFSET,
369             $INTEGER_LENGTH * 4; ## no critic (MagicNumbers)
370 9 50       52 $revision > 0
371             and confess "Revision > 0 is unknown: $revision";
372            
373 9         219 $self->set_messages(\my @messages);
374 9         411 for my $index (0 .. $number_of_strings - 1) {
375 48         118 my $key = 'msgid';
376 48         65 my $message;
377 48         87 for my $offset ($offset_original, $offset_translated) {
378 96         487 my ($string_length, $string_offset)
379             = unpack
380             $template x 2,
381             substr
382             $content,
383             $offset + $index * $INTEGER_LENGTH * 2,
384             $INTEGER_LENGTH * 2;
385 96         333 $message->{$key}
386             = substr $content, $string_offset, $string_length;
387 96         233 $key = 'msgstr';
388             }
389 48         118 $messages[$index] = $self->_unpack_message($message);
390             }
391            
392 9         50 return $self;
393             }
394            
395             __PACKAGE__->meta->make_immutable;
396            
397             1;
398            
399             __END__