File Coverage

blib/lib/Locale/MO/File.pm
Criterion Covered Total %
statement 36 101 35.6
branch 0 42 0.0
condition 0 15 0.0
subroutine 12 17 70.5
pod 1 1 100.0
total 49 176 27.8


line stmt bran cond sub pod time code
1             package Locale::MO::File; ## no critic (TidyCode)
2            
3 3     3   63912 use strict;
  3         5  
  3         77  
4 3     3   9 use warnings;
  3         4  
  3         58  
5 3     3   2027 use charnames qw(:full);
  3         68895  
  3         15  
6 3     3   1839 use namespace::autoclean;
  3         38338  
  3         17  
7 3     3   186 use Carp qw(confess);
  3         4  
  3         131  
8 3     3   1386 use Const::Fast qw(const);
  3         2229  
  3         14  
9 3     3   1645 use Encode qw(find_encoding);
  3         21008  
  3         194  
10 3     3   813 use English qw(-no_match_vars $INPUT_RECORD_SEPARATOR);
  3         4776  
  3         15  
11             require IO::File;
12 3     3   2269 use Moo;
  3         22400  
  3         14  
13 3     3   4956 use MooX::StrictConstructor;
  3         22296  
  3         14  
14 3     3   47381 use MooX::Types::MooseLike::Base qw(Bool Str ArrayRef FileHandle);
  3         11760  
  3         220  
15 3     3   1488 use Params::Validate qw(validate_with SCALAR ARRAYREF);
  3         5925  
  3         315  
16            
17             our $VERSION = '0.06';
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 0     0     my ($self, $string) = @_;
72            
73 0 0         if ( $self->get_encoding ) {
74 0 0         my $encoder = find_encoding( $self->get_encoding )
75             or confess 'Can not find encoding for ', $self->get_encoding;
76 0           $string = $encoder->encode($string);
77             }
78 0 0         if ( $self->get_newline ) {
79 0           $string =~ s{ \r? \n }{ $self->get_newline }xmsge;
  0            
80             }
81            
82 0           return $string;
83             }
84            
85             sub _decode_and_replace_newline {
86 0     0     my ($self, $string) = @_;
87            
88 0 0         if ( $self->get_encoding ) {
89 0 0         my $encoder = find_encoding( $self->get_encoding )
90             or confess 'Can not find encoding for ', $self->get_encoding;
91 0           $string = $encoder->decode($string, Encode::FB_CROAK);
92             }
93 0 0         if ( $self->get_newline ) {
94 0           $string =~ s{ \r? \n }{ $self->get_newline }xmsge;
  0            
95             }
96            
97 0           return $string;
98             }
99            
100             sub _pack_message {
101 0     0     my ($self, $message) = @_;
102            
103 0 0 0       my ($msgid, $msgstr) = map {
104 0           ( exists $message->{$_} && defined $message->{$_} )
105             ? $message->{$_}
106             : q{};
107             } qw(msgid msgstr);
108            
109             # original
110 0 0 0       $msgid = $self->_encode_and_replace_newline(
    0 0        
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             : q{}
128             ),
129             );
130            
131             # translation
132 0 0         $msgstr = $self->_encode_and_replace_newline(
133             length $msgstr
134             ? $msgstr
135             : join
136             $PLURAL_SEPARATOR,
137             map {
138 0 0         defined $_ ? $_ : q{}
139 0 0         } @{ $message->{msgstr_plural} || [] }
140             );
141            
142             return {
143 0           msgid => $msgid,
144             msgstr => $msgstr,
145             };
146             }
147            
148             sub _unpack_message {
149 0     0     my ($self, $message) = @_;
150            
151 0 0 0       my ($msgid, $msgstr) = map {
152 0           ( defined && length )
153             ? $self->_decode_and_replace_newline($_)
154             : q{};
155 0           } @{$message}{qw(msgid msgstr)};
156            
157             # return value
158 0           my %message;
159            
160             # split original
161 0           my @string = split $CONTEXT_SEPARATOR, $msgid;
162 0 0         if ( @string > 1 ) {
163 0           ( $message{msgctxt}, $msgid ) = @string;
164             }
165 0           my @plural = split $PLURAL_SEPARATOR, $msgid;
166 0           my $is_plural = @plural > 1;
167 0 0         if ( $is_plural ) {
168 0           @message{qw(msgid msgid_plural)} = @plural;
169             }
170             else {
171 0           $message{msgid} = $msgid;
172             }
173            
174             # split translation
175 0           @plural = split $PLURAL_SEPARATOR, $msgstr;
176 0 0         if ( $is_plural ) {
177 0           $message{msgstr_plural} = \@plural;
178             }
179             else {
180 0           $message{msgstr} = $plural[0];
181             }
182            
183 0           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";
319             $file_handle->print($content)
320             or confess "Can not write mo file $filename";
321             if ( ! $self->get_file_handle ) {
322             $file_handle->close
323             or confess "Can not close mo file $filename";
324             }
325            
326             return $self;
327             }
328            
329             sub read_file {
330 0     0 1   my $self = shift;
331            
332 0           my $filename = $self->get_filename;
333 0 0         defined $filename
334             or confess 'filename not set';
335 0   0       my $file_handle
336             = $self->get_file_handle
337             || IO::File->new($filename, '< :raw')
338             || confess "Can not open mo file $filename";
339 0           my $content = do {
340 0           local $INPUT_RECORD_SEPARATOR = ();
341 0           <$file_handle>;
342             };
343 0 0         if ( ! $self->get_file_handle ) {
344 0           $file_handle->close;
345             }
346            
347             # Find the byte order of the MO file creator
348 0           my $magic_number = substr $content, 0, $INTEGER_LENGTH;
349 0 0         my $template =
    0          
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 0           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 0 0         $revision > 0
367             and confess "Revision > 0 is unknown: $revision";
368            
369 0           $self->set_messages(\my @messages);
370 0           for my $index (0 .. $number_of_strings - 1) {
371 0           my $key = 'msgid';
372 0           my $message;
373 0           for my $offset ($offset_original, $offset_translated) {
374 0           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 0           $message->{$key}
382             = substr $content, $string_offset, $string_length;
383 0           $key = 'msgstr';
384             }
385 0           $messages[$index] = $self->_unpack_message($message);
386             }
387            
388 0           return $self;
389             }
390            
391             __PACKAGE__->meta->make_immutable;
392            
393             1;
394            
395             __END__