File Coverage

blib/lib/Spreadsheet/CSV.pm
Criterion Covered Total %
statement 22 24 91.6
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 30 32 93.7


line stmt bran cond sub pod time code
1             package Spreadsheet::CSV;
2              
3 1     1   21018 use 5.006;
  1         3  
  1         54  
4 1     1   6 use strict;
  1         2  
  1         45  
5 1     1   5 use warnings FATAL => 'all';
  1         7  
  1         54  
6 1     1   6 use Fcntl();
  1         1  
  1         16  
7 1     1   2683 use Spreadsheet::ParseExcel();
  1         65783  
  1         36  
8 1     1   852 use Text::CSV_XS();
  1         32214  
  1         66  
9 1     1   1303 use Archive::Zip();
  1         55365  
  1         23  
10 1     1   192 use XML::Parser();
  0            
  0            
11             use English qw( -no_match_vars );
12             use Compress::Zlib();
13             use Carp();
14             use IO::File();
15             use charnames ':full';
16              
17             sub _MAGIC_NUMBER_BUFFER_SIZE { return 2 } # for .zip and .gz files
18             sub _GRANDPARENT_INDEX { return -2 }
19             sub _PARENT_INDEX { return -1 }
20             sub _EXCEL_COLUMN_RADIX { return 26 }
21             sub _BUFFER_SIZE { return 4096 }
22              
23             our $VERSION = '0.20';
24              
25             sub new {
26             my ( $class, $params ) = @_;
27             my $self = {};
28             if ( defined $params->{worksheet_name} ) {
29             $self->{worksheet_name} = $params->{worksheet_name};
30             }
31             elsif ( defined $params->{worksheet_number} ) {
32             if ( $params->{worksheet_number} =~ /^\d+$/smx ) {
33             $self->{worksheet_number} = $params->{worksheet_number};
34             }
35             else {
36             Carp::croak('worksheet_number parameter is not a number');
37             }
38             }
39             else {
40             $self->{worksheet_number} = 1;
41             }
42             delete $params->{worksheet_number};
43             delete $params->{worksheet_name};
44             $self->{csv} = Text::CSV_XS->new( { binary => 1 } );
45             bless $self, $class;
46             return $self;
47             }
48              
49             sub getline {
50             my ( $self, $handle ) = @_;
51             if ( ( defined $self->{handle} ) && ( $self->{handle} eq $handle ) ) {
52             }
53             else {
54             $self->{eof} = q[];
55             if ( $self->_setup_handle($handle) ) {
56             }
57             else {
58             return;
59             }
60             }
61             my $row = $self->{cells}->[ $self->{row_index}++ ];
62             if ( !defined $row ) {
63             $self->{eof} = 1;
64             }
65             return $row;
66             }
67              
68             sub eof {
69             my ($self) = @_;
70             return $self->{eof};
71             }
72              
73             sub error_diag {
74             my ($self) = @_;
75             return $self->{_ERROR_DIAG};
76             }
77              
78             sub _stuff_input_into_tmp_file {
79             my ( $self, $input_handle ) = @_;
80             seek $input_handle, 0, Fcntl::SEEK_SET()
81             or
82             Carp::croak("Failed to seek to start of filehandle:$EXTENDED_OS_ERROR");
83             my $handle = IO::File->new_tmpfile()
84             or Carp::croak("Failed to create temporary file:$EXTENDED_OS_ERROR");
85             my $result;
86             while ( $result = read $input_handle, my $buffer, _BUFFER_SIZE() ) {
87             print {$handle} $buffer
88             or
89             Carp::croak("Failed to write to temporary file:$EXTENDED_OS_ERROR");
90             }
91             defined $result
92             or Carp::croak "Failed to read from input file:$EXTENDED_OS_ERROR";
93             seek $handle, 0, Fcntl::SEEK_SET()
94             or
95             Carp::croak("Failed to seek to start of filehandle:$EXTENDED_OS_ERROR");
96             seek $input_handle, 0, Fcntl::SEEK_SET()
97             or
98             Carp::croak("Failed to seek to start of filehandle:$EXTENDED_OS_ERROR");
99             return $handle;
100             }
101              
102             sub _xls_parser {
103             my ($self) = @_;
104             my $parser = Spreadsheet::ParseExcel->new(
105             CellHandler => sub {
106              
107             my ( $workbook, $sheet_index, $row, $col, $cell ) = @_;
108              
109             my $worksheet = $workbook->worksheet($sheet_index);
110             my $process_worksheet = 0;
111             if ( ( defined $self->{worksheet_name} )
112             && ( $self->{worksheet_name} eq $worksheet->get_name() ) )
113             {
114             $self->{xls_worksheet_found} = 1;
115             $process_worksheet = 1;
116             }
117             elsif (( $self->{worksheet_number} )
118             && ( ( $self->{worksheet_number} - 1 ) == $sheet_index ) )
119             {
120             $self->{xls_worksheet_found} = 1;
121             $process_worksheet = 1;
122             }
123             if ($process_worksheet) {
124             $self->{cells}->[$row]->[$col] = $cell->{_Value};
125             $self->{cells}->[$row]->[$col] =~
126             s/\N{CARRIAGE RETURN}\N{LINE FEED}/\N{LINE FEED}/smxg;
127             $self->{cells}->[$row]->[$col] =~
128             s/\N{LINE FEED}\N{CARRIAGE RETURN}/\N{LINE FEED}/smxg;
129             }
130             },
131             NotSetCell => 1,
132             );
133             return $parser;
134             }
135              
136             sub _setup_handle {
137             my ( $self, $input_handle ) = @_;
138             my $magic_bytes = $self->_sniff_magic_bytes($input_handle);
139             my $handle = $self->_stuff_input_into_tmp_file($input_handle);
140             my $parser = $self->_xls_parser();
141             if ( $magic_bytes =~ /^PK/smx ) {
142             if ( defined $self->_setup_zip($handle) ) {
143             $self->{handle} = $input_handle;
144             }
145             else {
146             return;
147             }
148             }
149             elsif ( $magic_bytes =~ /^\037\213/smx ) {
150             if ( $self->_setup_compress_zlib_spreadsheet($handle) ) {
151             $self->{handle} = $input_handle;
152             }
153             else {
154             return;
155             }
156             }
157             elsif ( ( defined $parser ) && ( my $workbook = $parser->parse($handle) ) )
158             {
159             if ( $self->_setup_xls_spreadsheet($workbook) ) {
160             $self->{handle} = $input_handle;
161             }
162             elsif ( !$self->{xls_worksheet_found} ) {
163             $self->{_ERROR_DIAG} = 'ENOENT - Worksheet '
164             . (
165             defined $self->{worksheet_name}
166             ? $self->{worksheet_name}
167             : $self->{worksheet_number}
168             ) . ' not found';
169             return;
170             }
171             else {
172             return;
173             }
174             }
175             else {
176             $handle = $self->_stuff_input_into_tmp_file($input_handle);
177             if (!$self->_check_file_utf8($input_handle)) {
178             $self->{_ERROR_DIAG} = 'CSV - Failed to parse as CSV';
179             return;
180             }
181             binmode $handle, ':encoding(UTF-8)';
182             $self->{cells} = [];
183             my $parsed_ok;
184             eval {
185             while ( my $row = $self->{csv}->getline($handle) ) {
186             push @{ $self->{cells} }, $row;
187             }
188             $parsed_ok = 1;
189             } or do {
190             $self->{_ERROR_DIAG} = 'CSV - Failed to parse as CSV';
191             return;
192             };
193             if ( ($parsed_ok) && ( $self->{csv}->eof() ) ) {
194             $self->{content_type} = 'text/csv'; # according to RFC 4180
195             $self->{type} = 'csv';
196             $self->{row_index} = 0;
197             $self->{handle} = $input_handle;
198             }
199             else {
200             $self->{_ERROR_DIAG} = 'CSV - Failed to parse as CSV:'
201             . ( 0 + $self->{csv}->error_diag() );
202             return;
203             }
204             }
205             return $self;
206             }
207              
208             sub _check_file_utf8 {
209             my ($self, $handle) = @_;
210             while(my $line = <$handle>) {
211             if (!utf8::decode(my $text = $line)) {
212             return;
213             }
214             }
215             return 1;
216             }
217              
218             sub _setup_zip {
219             my ( $self, $handle ) = @_;
220             my $zip = Archive::Zip->new();
221             Archive::Zip::setErrorHandler( sub { chomp $_[0]; die "$_[0]\n"; } );
222             my $result;
223             my $zip_error = 'Corrupt ZIP file';
224             eval { $result = $zip->readFromFileHandle($handle); }
225             or do { chomp $EVAL_ERROR; $zip_error = $EVAL_ERROR };
226             if ( ( defined $result ) && ( $result == Archive::Zip::AZ_OK() ) ) {
227             }
228             else {
229             $self->{_ERROR_DIAG} = "ZIP - $zip_error";
230             return;
231             }
232              
233             if ( $self->_parse_archive_zip_spreadsheet($zip) ) {
234             return 1;
235             }
236             else {
237             return;
238             }
239             }
240              
241             sub _sniff_magic_bytes {
242             my ( $self, $handle ) = @_;
243             seek $handle, 0, Fcntl::SEEK_SET()
244             or
245             Carp::croak("Failed to seek to start of filehandle:$EXTENDED_OS_ERROR");
246             defined read $handle, my $magic_bytes, _MAGIC_NUMBER_BUFFER_SIZE()
247             or Carp::croak("Failed to read from filehandle:$EXTENDED_OS_ERROR");
248             seek $handle, 0, Fcntl::SEEK_SET()
249             or
250             Carp::croak("Failed to seek to start of filehandle:$EXTENDED_OS_ERROR");
251             return $magic_bytes;
252             }
253              
254             sub _handle_workbook_type_zips {
255             my ( $self, $zip ) = @_;
256             my $shared_strings = $self->_xlsx_shared_strings($zip);
257             if ( !defined $shared_strings ) {
258             return;
259             }
260             my $worksheet_path = $self->_xlsx_worksheet_path($zip);
261             if ( ( defined $worksheet_path )
262             && ( my $worksheet = $zip->memberNamed($worksheet_path) ) )
263             {
264             $self->{'content_type'} =
265             'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
266             $self->{type} = 'xlsx';
267             $self->{zip} = $zip;
268             $self->{row_index} = 0;
269             my $content = $worksheet->contents();
270             my $cells =
271             $self->_xlsx_cells( $content, $shared_strings, $worksheet_path );
272             if ( defined $cells ) {
273             $self->{cells} = $cells;
274             return 1;
275             }
276             else {
277             return;
278             }
279             }
280             elsif ( defined $worksheet_path ) {
281             $self->{_ERROR_DIAG} =
282             q[ZIP - Missing '] . $worksheet_path . q[' file in .xlsx file];
283             return;
284             }
285             else {
286             return;
287             }
288             }
289              
290             sub _handle_mimetype_type_zips {
291             my ( $self, $zip ) = @_;
292             my $member = $zip->memberNamed('mimetype');
293             my $content_type;
294             delete $self->{type};
295             if ( defined $member ) {
296             $content_type = $member->contents();
297             if ( $content_type eq 'application/vnd.oasis.opendocument.spreadsheet' )
298             {
299             $self->{type} = 'ods';
300             }
301             elsif ( $content_type eq 'application/vnd.sun.xml.calc' ) {
302             $self->{type} = 'sxc';
303             }
304             elsif ( $content_type eq 'application/x-kspread' ) {
305             $self->{type} = 'ksp';
306             }
307             }
308             if ( ( $self->{type} ) && ( $self->{type} eq 'ksp' ) ) {
309             $self->{content_type} = $content_type;
310             my $maindoc_member = $zip->memberNamed('maindoc.xml');
311             if ( defined $maindoc_member ) {
312             $self->{zip} = $zip;
313             $self->{row_index} = 0;
314             my $maindoc_data = $maindoc_member->contents();
315             my $cells = $self->_ksp_cells($maindoc_data);
316             if ( defined $cells ) {
317             $self->{cells} = $cells;
318             return 1;
319             }
320             else {
321             return;
322             }
323             }
324             else {
325             $self->{_ERROR_DIAG} =
326             q[ZIP - Missing 'content.xml' file in .]
327             . ( lc $self->{type} )
328             . q[ file];
329             return;
330             }
331             }
332             elsif ( $self->{type} ) {
333             $self->{content_type} = $content_type;
334             my $content_member = $zip->memberNamed('content.xml');
335             if ( defined $content_member ) {
336             $self->{zip} = $zip;
337             $self->{row_index} = 0;
338             my $content_data = $content_member->contents();
339             my $cells = $self->_ods_cells($content_data);
340             if ( defined $cells ) {
341             $self->{cells} = $cells;
342             return 1;
343             }
344             else {
345             return;
346             }
347             }
348             else {
349             $self->{_ERROR_DIAG} =
350             q[ZIP - Missing 'content.xml' file in .]
351             . ( lc $self->{type} )
352             . q[ file];
353             return;
354             }
355             }
356             else {
357             $self->{_ERROR_DIAG} =
358             q[ZIP - mimetype file does not contain any known MIME Types in OpenOffice document];
359             return;
360             }
361             }
362              
363             sub _parse_archive_zip_spreadsheet {
364             my ( $self, $zip ) = @_;
365             if ( $zip->memberNamed('xl/workbook.xml') ) {
366             if ( !defined $self->_handle_workbook_type_zips($zip) ) {
367             return;
368             }
369             }
370             elsif ( $zip->memberNamed('mimetype') ) {
371             if ( !defined $self->_handle_mimetype_type_zips($zip) ) {
372             return;
373             }
374             }
375             else {
376             $self->{_ERROR_DIAG} =
377             q[ZIP - Missing any identifiable spreadsheet file in ZIP archive];
378             return;
379             }
380             return 1;
381             }
382              
383             sub _setup_compress_zlib_spreadsheet {
384             my ( $self, $handle ) = @_;
385             seek $handle, 0, Fcntl::SEEK_SET()
386             or
387             Carp::croak("Failed to seek to start of filehandle:$EXTENDED_OS_ERROR");
388              
389             my $contents;
390             my $gzip_error = 'Corrupt GZIP';
391             my $result = 0;
392             eval {
393             my $gz = Compress::Zlib::gzopen( $handle, 'rb' )
394             or die "Cannot open handle: $Compress::Zlib::gzerrno\n";
395              
396             while ( $gz->gzread( my $uncompressed ) > 0 ) {
397             $contents .= $uncompressed;
398             }
399             if ( $gz->gzerror() != Compress::Zlib::Z_STREAM_END() ) {
400             die "Failed to read from handle:$Compress::Zlib::gzerrno\n";
401             }
402             $gz->gzclose()
403             and die "Failed to close compression:$Compress::Zlib::gzerrno\n";
404             $result = 1;
405             } or do {
406             chomp $EVAL_ERROR;
407             if ($EVAL_ERROR) {
408             $gzip_error = $EVAL_ERROR;
409             }
410             };
411             if ( $result == 0 ) {
412             $self->{_ERROR_DIAG} = q[GZIP - ] . $gzip_error;
413             return;
414             }
415             $self->{content_type} = 'application/x-gnumeric';
416             $self->{type} = 'gnumeric';
417             $self->{row_index} = 0;
418             my $cells = $self->_gnumeric_cells($contents);
419             if ( defined $cells ) {
420             $self->{cells} = $cells;
421             }
422             else {
423             return;
424             }
425             return 1;
426             }
427              
428             sub _setup_xls_spreadsheet {
429             my ( $self, $workbook ) = @_;
430             my $worksheet;
431             if ( defined $self->{worksheet_name} ) {
432             $worksheet = $workbook->worksheet( $self->{worksheet_name} );
433             }
434             elsif ( $self->{worksheet_number} ) {
435             $worksheet = $workbook->worksheet( $self->{worksheet_number} - 1 );
436             }
437             if ( !defined $worksheet ) {
438             return;
439             }
440             $self->{content_type} = 'application/vnd.ms-excel';
441             $self->{type} = 'xls';
442             $self->{row_index} = 0;
443             return 1;
444             }
445              
446             sub _process_worksheet {
447             my ( $self, $current_worksheet_name, $current_worksheet_index ) = @_;
448             my $process_worksheet = 0;
449             if ( ( defined $self->{worksheet_name} )
450             && ( $self->{worksheet_name} eq $current_worksheet_name ) )
451             {
452             $process_worksheet = 1;
453             }
454             elsif (( $self->{worksheet_number} )
455             && ( ( $self->{worksheet_number} - 1 ) == $current_worksheet_index ) )
456             {
457             $process_worksheet = 1;
458             }
459             return $process_worksheet;
460             }
461              
462             sub _ksp_cells {
463             my ( $self, $maindoc_content ) = @_;
464             my $cells;
465             my $current_worksheet_name;
466             my $parameter_stack;
467             my $element_stack;
468             my $worksheet_count;
469             my $process_worksheet = 0;
470             my $xml = XML::Parser->new(
471             Handlers => {
472             Entity => sub {
473             die
474             "XML Entities have been detected and rejected in the XML, due to security concerns\n";
475             },
476             Start => sub {
477             my ( $expat, $element_name, %element_parameters ) = @_;
478             push @{$element_stack}, $element_name;
479             push @{$parameter_stack}, \%element_parameters;
480             if ( $element_name eq 'table' ) {
481             if ( defined $worksheet_count ) {
482             $worksheet_count += 1;
483             }
484             else {
485             $worksheet_count = 0;
486             }
487             $process_worksheet =
488             $self->_process_worksheet( $element_parameters{name},
489             $worksheet_count );
490             }
491             },
492             End => sub {
493             my ( $expat, $element_name ) = @_;
494             pop @{$parameter_stack};
495             if ( $element_name ne pop @{$element_stack} ) {
496             Carp::croak(
497             'Internal confusion in processing XML elements');
498             }
499             },
500             Char => sub {
501             my ( $expat, $content ) = @_;
502             if ($process_worksheet) {
503             if ( ( defined $element_stack->[ _GRANDPARENT_INDEX() ] )
504             && (
505             $element_stack->[ _GRANDPARENT_INDEX() ] eq 'cell' )
506             && ( $element_stack->[ _PARENT_INDEX() ] eq 'text' ) )
507             {
508             $cells->[ $parameter_stack->[ _GRANDPARENT_INDEX() ]
509             ->{row} - 1 ]
510             ->[ $parameter_stack->[ _GRANDPARENT_INDEX() ]
511             ->{column} - 1 ] .= $content;
512             }
513             }
514             }
515             }
516             );
517             eval { $xml->parse($maindoc_content); } or do {
518             chomp $EVAL_ERROR;
519             $EVAL_ERROR =~ s/^\s*//smx;
520             $self->{_ERROR_DIAG} = "XML - Invalid XML in maindoc.xml:$EVAL_ERROR";
521             return;
522             };
523             if ( defined $cells ) {
524             return $cells;
525             }
526             else {
527             $self->{_ERROR_DIAG} = 'ENOENT - Worksheet '
528             . (
529             defined $self->{worksheet_name}
530             ? $self->{worksheet_name}
531             : $self->{worksheet_number}
532             ) . ' not found';
533             return;
534             }
535             }
536              
537             sub _ods_cells {
538             my ( $self, $ods_content ) = @_;
539             my $cells;
540             my $current_row;
541             my $current_column_number;
542             my $element_stack;
543             my $process_worksheet = 0;
544             my $worksheet_count;
545             my $xml = XML::Parser->new(
546             Handlers => {
547             Entity => sub {
548             die
549             "XML Entities have been detected and rejected in the XML, due to security concerns\n";
550             },
551             Start => sub {
552             my ( $expat, $element_name, %element_parameters ) = @_;
553             push @{$element_stack}, $element_name;
554             my ( $prefix, $suffix ) = split /:/smx, $element_name;
555             if ( $prefix eq 'table' ) {
556             if ( $suffix eq 'table-row' ) {
557             $current_row = [];
558             $current_column_number = 0 - 1;
559             }
560             elsif ( $suffix eq 'table' ) {
561             if ( defined $worksheet_count ) {
562             $worksheet_count += 1;
563             }
564             else {
565             $worksheet_count = 0;
566             }
567             $process_worksheet =
568             $self->_process_worksheet(
569             $element_parameters{'table:name'},
570             $worksheet_count );
571             }
572             elsif ( $suffix eq 'table-cell' ) {
573             $current_column_number += 1;
574             }
575             }
576             elsif ( $prefix eq 'text' ) {
577             if ( $suffix eq 'p' ) {
578             if ( ( defined $current_row )
579             && (
580             defined $current_row->[$current_column_number] )
581             )
582             {
583             $current_row->[$current_column_number] .= "\n";
584             }
585             }
586             elsif ( $suffix eq 's' ) {
587             if ( ( defined $current_row )
588             && (
589             defined $current_row->[$current_column_number] )
590             )
591             {
592             $current_row->[$current_column_number] .= q[ ];
593             }
594             }
595             }
596             },
597             End => sub {
598             my ( $expat, $element_name ) = @_;
599             if ( $element_name ne pop @{$element_stack} ) {
600             Carp::croak(
601             'Internal confusion in processing XML elements');
602             }
603             if ( $element_name eq 'table:table-row' ) {
604             if ( @{$current_row} ) {
605             push @{$cells}, $current_row;
606             }
607             }
608             elsif ( $element_name eq 'table:table' ) {
609             $process_worksheet = 0;
610             }
611             },
612             Char => sub {
613             my ( $expat, $content ) = @_;
614             if ($process_worksheet) {
615             if ( $element_stack->[_PARENT_INDEX] eq 'text:p' ) {
616             if ( $element_stack->[ _GRANDPARENT_INDEX() ] eq
617             'table:table-cell' )
618             {
619             if (
620             defined $current_row->[$current_column_number] )
621             {
622             $current_row->[$current_column_number] .=
623             $content;
624             }
625             else {
626             $current_row->[$current_column_number] =
627             $content;
628             }
629             }
630             }
631             }
632             }
633             }
634             );
635             eval { $xml->parse($ods_content); } or do {
636             chomp $EVAL_ERROR;
637             $EVAL_ERROR =~ s/^\s*//smx;
638             $self->{_ERROR_DIAG} = "XML - Invalid XML in content.xml:$EVAL_ERROR";
639             return;
640             };
641             if ( defined $cells ) {
642             return $cells;
643             }
644             else {
645             $self->{_ERROR_DIAG} = 'ENOENT - Worksheet '
646             . (
647             defined $self->{worksheet_name}
648             ? $self->{worksheet_name}
649             : $self->{worksheet_number}
650             ) . ' not found';
651             return;
652             }
653             }
654              
655             sub _gnumeric_cells {
656             my ( $self, $gnumeric_content ) = @_;
657             my $cells;
658             my $current_row_number;
659             my $current_column_number;
660             my $current_worksheet_name;
661             my $current_sheet_cells;
662             my $element_stack;
663             my $process_worksheet = 0;
664             my $worksheet_count;
665             my $xml = XML::Parser->new(
666             Handlers => {
667             Entity => sub {
668             die
669             "XML Entities have been detected and rejected in the XML, due to security concerns\n";
670             },
671             Start => sub {
672             my ( $expat, $element_name, %element_parameters ) = @_;
673             push @{$element_stack}, $element_name;
674             if ( $element_name eq 'gnm:Cell' ) {
675             $current_row_number = $element_parameters{Row};
676             $current_column_number = $element_parameters{Col};
677             }
678             elsif ( $element_name eq 'gnm:Sheet' ) {
679             $current_sheet_cells = [];
680             }
681             },
682             End => sub {
683             my ( $expat, $element_name ) = @_;
684             if ( $element_name ne pop @{$element_stack} ) {
685             Carp::croak(
686             'Internal confusion in processing XML elements');
687             }
688             elsif ( $element_name eq 'gnm:Sheet' ) {
689             if ( defined $worksheet_count ) {
690             $worksheet_count += 1;
691             }
692             else {
693             $worksheet_count = 0;
694             }
695             if (
696             $self->_process_worksheet(
697             $current_worksheet_name, $worksheet_count
698             )
699             )
700             {
701             $cells = $current_sheet_cells;
702             }
703             }
704             },
705             Char => sub {
706             my ( $expat, $content ) = @_;
707             if ( $element_stack->[ _PARENT_INDEX() ] eq 'gnm:Name' ) {
708             if ( $element_stack->[ _GRANDPARENT_INDEX() ] eq
709             'gnm:Sheet' )
710             {
711             $current_worksheet_name = $content;
712             }
713             }
714             if ( $element_stack->[ _PARENT_INDEX() ] eq 'gnm:Cell' ) {
715             if ( $content eq "\N{LINE FEED}" ) {
716             $current_sheet_cells->[$current_row_number]
717             ->[$current_column_number] .=
718             $expat->original_string();
719             }
720             else {
721             $current_sheet_cells->[$current_row_number]
722             ->[$current_column_number] .= $content;
723             }
724             }
725             }
726             }
727             );
728             eval { $xml->parse($gnumeric_content); } or do {
729             chomp $EVAL_ERROR;
730             $EVAL_ERROR =~ s/^\s*//smx;
731             $self->{_ERROR_DIAG} =
732             "XML - Invalid XML in gzipped gnumeric file:$EVAL_ERROR";
733             return;
734             };
735             if ( defined $cells ) {
736             return $cells;
737             }
738             else {
739             $self->{_ERROR_DIAG} = 'ENOENT - Worksheet '
740             . (
741             defined $self->{worksheet_name}
742             ? $self->{worksheet_name}
743             : $self->{worksheet_number}
744             ) . ' not found';
745             return;
746             }
747             }
748              
749             sub _xlsx_shared_strings {
750             my ( $self, $zip ) = @_;
751             my $member = $zip->memberNamed('xl/sharedStrings.xml');
752             if ( !$member ) {
753             $self->{_ERROR_DIAG} = q[ZIP - Missing 'xl/sharedStrings.xml' file];
754             return;
755             }
756             my $shared_string_content = $member->contents();
757             my $element_stack;
758             my $current_index;
759             my $shared_strings = {};
760             my $xml = XML::Parser->new(
761             Handlers => {
762             Entity => sub {
763             die
764             "XML Entities have been detected and rejected in the XML, due to security concerns\n";
765             },
766             Start => sub {
767             my ( $expat, $element_name, %element_parameters ) = @_;
768             push @{$element_stack}, $element_name;
769             if ( $element_name eq 'sst' ) {
770             }
771             elsif ( $element_name eq 'si' ) {
772             if ( defined $current_index ) {
773             $current_index += 1;
774             }
775             else {
776             $current_index = 0;
777             }
778             }
779             elsif ( $element_name eq 't' ) {
780             }
781             },
782             End => sub {
783             my ( $expat, $element_name ) = @_;
784             if ( $element_name ne pop @{$element_stack} ) {
785             Carp::croak(
786             'Internal confusion in processing XML elements');
787             }
788             },
789             Char => sub {
790             my ( $expat, $content ) = @_;
791             $content =~ s/_x000D_//smxg;
792             if ( defined $shared_strings->{$current_index} ) {
793             $shared_strings->{$current_index} .= $content;
794             }
795             else {
796             $shared_strings->{$current_index} = $content;
797             }
798             }
799             }
800             );
801             eval { $xml->parse($shared_string_content); } or do {
802             chomp $EVAL_ERROR;
803             $EVAL_ERROR =~ s/^\s*//smx;
804             $self->{_ERROR_DIAG} =
805             "XML - Invalid XML in sharedStrings.xml:$EVAL_ERROR";
806             return;
807             };
808             return $shared_strings;
809             }
810              
811             sub _xlsx_worksheet_path {
812             my ( $self, $zip ) = @_;
813             my $member = $zip->memberNamed('xl/workbook.xml');
814             if ( !$member ) {
815             $self->{_ERROR_DIAG} = q[ZIP - Missing 'xl/workbook.xml' file];
816             return;
817             }
818             my $content = $member->contents();
819             my $worksheet_number;
820             my $sheets = [];
821             my $worksheet_count;
822             my $xml = XML::Parser->new(
823             Handlers => {
824             Entity => sub {
825             die
826             "XML Entities have been detected and rejected in the XML, due to security concerns\n";
827             },
828             Start => sub {
829             my ( $expat, $element_name, %element_parameters ) = @_;
830             if ( $element_name eq 'sheet' ) {
831             push @{$sheets}, \%element_parameters;
832             if ( !defined $worksheet_number ) {
833             if ( defined $worksheet_count ) {
834             $worksheet_count += 1;
835             }
836             else {
837             $worksheet_count = 0;
838             }
839             if (
840             ( defined $self->{worksheet_name} )
841             && ( defined $element_parameters{name} )
842             && ( $self->{worksheet_name} eq
843             $element_parameters{name} )
844             )
845             {
846             $worksheet_number = $worksheet_count + 1;
847             }
848             elsif (
849             ( $self->{worksheet_number} )
850             && ( ( $self->{worksheet_number} - 1 ) ==
851             $worksheet_count )
852             )
853             {
854             $worksheet_number = $worksheet_count + 1;
855             }
856             }
857             }
858             }
859             }
860             );
861             eval { $xml->parse($content); } or do {
862             chomp $EVAL_ERROR;
863             $EVAL_ERROR =~ s/^\s*//smx;
864             $self->{_ERROR_DIAG} =
865             "XML - Invalid XML in xl/workbook.xml:$EVAL_ERROR";
866             return;
867             };
868             if ( !defined $worksheet_number ) {
869             $self->{_ERROR_DIAG} = 'ENOENT - Worksheet '
870             . (
871             defined $self->{worksheet_name}
872             ? $self->{worksheet_name}
873             : $self->{worksheet_number}
874             ) . ' not found';
875             return;
876             }
877             return 'xl/worksheets/sheet' . $worksheet_number . '.xml';
878             }
879              
880             sub _xlsx_cells {
881             my ( $self, $xlsx_content, $shared_strings, $worksheet_path ) = @_;
882             my $cells = [];
883             my $current_worksheet_name;
884             my $current_sheet_cells = [];
885             my $parameter_stack;
886             my $element_stack;
887             my $process_worksheet = 0;
888             my $xml = XML::Parser->new(
889             Handlers => {
890             Entity => sub {
891             die
892             "XML Entities have been detected and rejected in the XML, due to security concerns\n";
893             },
894             Start => sub {
895             my ( $expat, $element_name, %element_parameters ) = @_;
896             push @{$element_stack}, $element_name;
897             push @{$parameter_stack}, \%element_parameters;
898             },
899             End => sub {
900             my ( $expat, $element_name ) = @_;
901             pop @{$parameter_stack};
902             if ( $element_name ne pop @{$element_stack} ) {
903             Carp::croak(
904             'Internal confusion in processing XML elements');
905             }
906             },
907             Char => sub {
908             my ( $expat, $content ) = @_;
909             if ( ( $element_stack->[ _GRANDPARENT_INDEX() ] eq 'c' )
910             && ( $element_stack->[ _PARENT_INDEX() ] eq 'v' ) )
911             {
912             my $cell_reference =
913             $parameter_stack->[ _GRANDPARENT_INDEX() ]->{r};
914             if ( $cell_reference =~ /^([[:upper:]]+)(\d+)$/smx ) {
915             my ( $column_designator, $row_number ) = ( $1, $2 - 1 );
916             my $column_number = 0;
917             my $letter_index = 0;
918             foreach
919             my $letter ( reverse split //smx, $column_designator )
920             {
921             $column_number +=
922             ( ( ord uc $letter ) - ( ord 'A' ) ) *
923             ( _EXCEL_COLUMN_RADIX()**$letter_index );
924             $letter_index += 1;
925             }
926             if (
927             (
928             defined
929             $parameter_stack->[ _GRANDPARENT_INDEX() ]->{t}
930             )
931             && ( $parameter_stack->[ _GRANDPARENT_INDEX() ]->{t}
932             eq 's' )
933             )
934             {
935             $cells->[$row_number]->[$column_number] =
936             $shared_strings->{$content};
937             }
938             else {
939             $cells->[$row_number]->[$column_number] = $content;
940             }
941             }
942             else {
943             Carp::croak('Unable to determine cell reference');
944             }
945             }
946             }
947             }
948             );
949             eval { $xml->parse($xlsx_content); } or do {
950             chomp $EVAL_ERROR;
951             $self->{_ERROR_DIAG} =
952             "XML - Invalid XML in $worksheet_path:$EVAL_ERROR";
953             return;
954             };
955             return $cells;
956             }
957              
958             sub content_type {
959             my ($self) = @_;
960             return $self->{content_type};
961             }
962              
963             sub suffix {
964             my ($self) = @_;
965             return $self->{type};
966             }
967              
968             1;
969             __END__