1-- Legal licensing note:
2
3--  Copyright (c) 1999 .. 2018 Gautier de Montmollin
4--  SWITZERLAND
5
6--  Permission is hereby granted, free of charge, to any person obtaining a copy
7--  of this software and associated documentation files (the "Software"), to deal
8--  in the Software without restriction, including without limitation the rights
9--  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10--  copies of the Software, and to permit persons to whom the Software is
11--  furnished to do so, subject to the following conditions:
12
13--  The above copyright notice and this permission notice shall be included in
14--  all copies or substantial portions of the Software.
15
16--  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17--  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18--  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19--  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20--  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21--  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22--  THE SOFTWARE.
23
24-- NB: this is the MIT License, as found 12-Sep-2007 on the site
25-- http://www.opensource.org/licenses/mit-license.php
26
27with Zip.Headers;
28
29with Ada.Characters.Handling;
30with Ada.Unchecked_Deallocation;
31with Ada.Exceptions;                    use Ada.Exceptions;
32with Ada.IO_Exceptions;
33with Ada.Strings.Fixed;
34with Ada.Strings.Unbounded;
35
36package body Zip is
37
38  use Interfaces;
39
40  procedure Dispose is new Ada.Unchecked_Deallocation( Dir_node, p_Dir_node );
41  procedure Dispose is new Ada.Unchecked_Deallocation( String, p_String );
42
43  package Binary_tree_rebalancing is
44    procedure Rebalance( root: in out p_Dir_node );
45  end Binary_tree_rebalancing;
46
47  package body Binary_tree_rebalancing is
48
49    -------------------------------------------------------------------
50    -- Tree Rebalancing in Optimal Time and Space                    --
51    -- QUENTIN F. STOUT and BETTE L. WARREN                          --
52    -- Communications of the ACM September 1986 Volume 29 Number 9   --
53    -------------------------------------------------------------------
54    -- http://www.eecs.umich.edu/~qstout/pap/CACM86.pdf
55    --
56    -- Translated by (New) P2Ada v. 15-Nov-2006
57
58    procedure Tree_to_vine( root: p_Dir_node; size: out Integer )
59      --  transform the tree with pseudo-root
60      --   "root^" into a vine with pseudo-root
61      --   node "root^", and store the number of
62      --   nodes in "size"
63    is
64      vine_tail, remainder, temp: p_Dir_node;
65    begin
66      vine_tail := root;
67      remainder := vine_tail.right;
68      size := 0;
69      while remainder /= null loop
70        if remainder.left = null then
71          --  move vine-tail down one:
72          vine_tail := remainder;
73          remainder := remainder.right;
74          size := size + 1;
75        else
76          --  rotate:
77          temp := remainder.left;
78          remainder.left := temp.right;
79          temp.right := remainder;
80          remainder := temp;
81          vine_tail.right := temp;
82        end if;
83      end loop;
84    end Tree_to_vine;
85
86    procedure Vine_to_tree( root: p_Dir_node; size_given: Integer ) is
87      --  convert the vine with "size" nodes and pseudo-root
88      --  node "root^" into a balanced tree
89      leaf_count: Integer;
90      size : Integer:= size_given;
91
92      procedure Compression( root_compress: p_Dir_node; count: Integer ) is
93        --  Compress "count" spine nodes in the tree with pseudo-root "root_compress^"
94        scanner, child: p_Dir_node;
95      begin
96        scanner := root_compress;
97        for counter in reverse 1 .. count loop
98          child         := scanner.right;
99          scanner.right := child.right;
100          scanner       := scanner.right;
101          child.right   := scanner.left;
102          scanner.left  := child;
103        end loop;
104      end Compression;
105
106      -- Returns n - 2 ** Integer( Float'Floor( log( Float(n) ) / log(2.0) ) )
107      -- without Float-Point calculation and rounding errors with too short floats
108      function Remove_leading_binary_1( n: Integer ) return Integer is
109        x: Integer:= 2**16; -- supposed maximum
110      begin
111        if n < 1 then
112          return n;
113        end if;
114        while n mod x = n loop
115          x:= x / 2;
116        end loop;
117        return n mod x;
118      end Remove_leading_binary_1;
119
120    begin --  Vine_to_tree
121      leaf_count := Remove_leading_binary_1(size + 1);
122      Compression(root, leaf_count); -- create deepest leaves
123      -- use Perfect_leaves instead for a perfectly balanced tree
124      size := size - leaf_count;
125      while size > 1 loop
126        Compression(root, size / 2);
127        size := size / 2;
128      end loop;
129    end Vine_to_tree;
130
131    procedure Rebalance( root: in out p_Dir_node ) is
132      --  Rebalance the binary search tree with root "root.all",
133      --  with the result also rooted at "root.all".
134      --  Uses the Tree_to_vine and Vine_to_tree procedures.
135      pseudo_root: p_Dir_node;
136      size: Integer;
137    begin
138      pseudo_root:= new Dir_node(name_len => 0);
139      pseudo_root.right := root;
140      Tree_to_vine(pseudo_root, size);
141      Vine_to_tree(pseudo_root, size);
142      root := pseudo_root.right;
143      Dispose(pseudo_root);
144    end Rebalance;
145
146  end Binary_tree_rebalancing;
147
148  -- 19-Jun-2001: Enhanced file name identification
149  --              a) when case insensitive  -> all UPPER (current)
150  --              b) '\' and '/' identified -> all '/'   (new)
151
152  function Normalize( s: String; case_sensitive: Boolean ) return String is
153    sn: String( s'Range );
154  begin
155    if case_sensitive then
156      sn:= s;
157    else
158      sn:= Ada.Characters.Handling.To_Upper(s);
159    end if;
160    for i in sn'Range loop
161      if sn(i)= '\' then
162        sn(i):='/';
163      end if;
164    end loop;
165    return sn;
166  end Normalize;
167
168  boolean_to_encoding: constant array(Boolean) of Zip_name_encoding:=
169    (False => IBM_437, True => UTF_8);
170
171  -------------------------------------------------------------
172  -- Load Zip_info from a stream containing the .zip archive --
173  -------------------------------------------------------------
174
175  procedure Load(
176    info            :    out Zip_info;
177    from            : in out Zip_Streams.Root_Zipstream_Type'Class;
178    case_sensitive  : in     Boolean:= False;
179    duplicate_names : in     Duplicate_name_policy:= error_on_duplicate
180  )
181  is
182    procedure Insert(
183      dico_name        : String; -- UPPER if case-insensitive search
184      file_name        : String;
185      file_index       : Zip_Streams.ZS_Index_Type;
186      comp_size,
187      uncomp_size      : File_size_type;
188      crc_32           : Unsigned_32;
189      date_time        : Time;
190      method           : PKZip_method;
191      name_encoding    : Zip_name_encoding;
192      read_only        : Boolean;
193      encrypted_2_x    : Boolean;
194      root_node        : in out p_Dir_node
195      )
196    is
197      procedure Insert_into_tree(node: in out p_Dir_node) is
198      begin
199        if node = null then
200          node:= new Dir_node'
201            ( (name_len          => file_name'Length,
202               left              => null,
203               right             => null,
204               dico_name         => dico_name,
205               file_name         => file_name,
206               file_index        => file_index,
207               comp_size         => comp_size,
208               uncomp_size       => uncomp_size,
209               crc_32            => crc_32,
210               date_time         => date_time,
211               method            => method,
212               name_encoding     => name_encoding,
213               read_only         => read_only,
214               encrypted_2_x     => encrypted_2_x,
215               user_code         => 0
216               )
217            );
218        elsif dico_name > node.dico_name then
219          Insert_into_tree(node.right);
220        elsif dico_name < node.dico_name then
221          Insert_into_tree(node.left);
222        else
223          --  Here we have a case where the entry name already exists in the dictionary.
224          case duplicate_names is
225            when error_on_duplicate =>
226              Ada.Exceptions.Raise_Exception
227                (Duplicate_name'Identity,
228                 "Same full entry name (in dictionary: " & dico_name &
229                 ") appears twice in archive directory; " &
230                 "procedure Load was called with strict name policy."
231                );
232            when admit_duplicates =>
233              if file_index > node.file_index then
234                Insert_into_tree(node.right);
235              elsif file_index < node.file_index then
236                Insert_into_tree(node.left);
237              else
238                Ada.Exceptions.Raise_Exception
239                  (Duplicate_name'Identity,
240                   "Archive directory corrupt: same full entry name (in dictionary: " &
241                   dico_name & "), with same data position, appear twice."
242                  );
243              end if;
244          end case;
245        end if;
246      end Insert_into_tree;
247      --
248    begin
249      Insert_into_tree(root_node);
250    end Insert;
251
252    the_end: Zip.Headers.End_of_Central_Dir;
253    header : Zip.Headers.Central_File_Header;
254    p      : p_Dir_node:= null;
255    zip_info_already_loaded: exception;
256    main_comment: p_String;
257  begin -- Load Zip_info
258    if info.loaded then
259      raise zip_info_already_loaded;
260    end if; -- 15-Apr-2002
261    Zip.Headers.Load(from, the_end);
262    -- We take the opportunity to read the main comment, which is right
263    -- after the end-of-central-directory block.
264    main_comment:= new String(1..Integer(the_end.main_comment_length));
265    String'Read(from'Access, main_comment.all);
266    -- Process central directory:
267    Zip_Streams.Set_Index(
268      from,
269      Zip_Streams.ZS_Index_Type(1 + the_end.central_dir_offset) + the_end.offset_shifting
270    );
271
272    for i in 1..the_end.total_entries loop
273      Zip.Headers.Read_and_check(from, header );
274      declare
275        this_name: String(1..Natural(header.short_info.filename_length));
276        use Zip_Streams;
277      begin
278        String'Read(from'Access, this_name);
279        -- Skip extra field and entry comment.
280        Set_Index(
281          from,
282          Index( from ) +
283          ZS_Size_Type (
284            header.short_info.extra_field_length +
285            header.comment_length
286          )
287        );
288        -- Now the whole i_th central directory entry is behind
289        Insert( dico_name   => Normalize(this_name, case_sensitive),
290                file_name   => Normalize(this_name, True),
291                file_index  => Zip_Streams.ZS_Index_Type (1 + header.local_header_offset) +
292                               the_end.offset_shifting,
293                comp_size   => header.short_info.dd.compressed_size,
294                uncomp_size => header.short_info.dd.uncompressed_size,
295                crc_32      => header.short_info.dd.crc_32,
296                date_time   => header.short_info.file_timedate,
297                method      => Method_from_code(header.short_info.zip_type),
298                name_encoding =>
299                  boolean_to_encoding(
300                   (header.short_info.bit_flag and
301                    Zip.Headers.Language_Encoding_Flag_Bit) /= 0),
302                read_only   => header.made_by_version / 256 = 0 and -- DOS-like
303                               (header.external_attributes and 1) = 1,
304                encrypted_2_x => (header.short_info.bit_flag and Zip.Headers.Encryption_Flag_Bit) /= 0,
305                root_node     => p );
306        -- Since the files are usually well ordered, the tree as inserted
307        -- is very unbalanced; we need to rebalance it from time to time
308        -- during loading, otherwise the insertion slows down dramatically
309        -- for zip files with plenty of files - converges to
310        -- O(total_entries ** 2)...
311        if i mod 256 = 0 then
312          Binary_tree_rebalancing.Rebalance(p);
313        end if;
314      end;
315    end loop;
316    Binary_tree_rebalancing.Rebalance(p);
317    info:= ( loaded             => True,
318             case_sensitive     => case_sensitive,
319             zip_file_name      => new String'("This is a stream, no direct file!"),
320             zip_input_stream   => from'Unchecked_Access,
321             dir_binary_tree    => p,
322             total_entries      => Integer(the_end.total_entries),
323             zip_file_comment   => main_comment,
324             zip_archive_format => Zip_32
325           );
326  exception
327    when Zip.Headers.bad_end =>
328      Raise_Exception (Zip.Archive_corrupted'Identity, "Bad (or no) end-of-central-directory");
329    when Zip.Headers.bad_central_header =>
330      Raise_Exception (Zip.Archive_corrupted'Identity, "Bad central directory entry header");
331  end Load;
332
333  -----------------------------------------------------------
334  -- Load Zip_info from a file containing the .zip archive --
335  -----------------------------------------------------------
336
337  procedure Load(
338    info            : out Zip_info;
339    from            : in  String; -- Zip file name
340    case_sensitive  : in  Boolean:= False;
341    duplicate_names : in  Duplicate_name_policy:= error_on_duplicate
342  )
343  is
344    use Zip_Streams;
345    MyStream   : aliased File_Zipstream;
346  begin
347    Set_Name (MyStream, from);
348    begin
349      Open (MyStream, In_File);
350    exception
351      when others =>
352        Raise_Exception (Zip_file_open_error'Identity, "Archive: [" & from & ']');
353    end;
354    -- Call the stream version of Load(...)
355    Load(
356      info,
357      MyStream,
358      case_sensitive,
359      duplicate_names
360    );
361    Close (MyStream);
362    Dispose(info.zip_file_name);
363    info.zip_file_name:= new String'(from);
364    info.zip_input_stream:= null; -- forget about the stream!
365  exception
366    when others =>
367      if Is_Open(MyStream) then
368        Close(MyStream);
369      end if;
370      raise;
371  end Load;
372
373  function Is_loaded (info: in Zip_info) return Boolean is
374  begin
375    return info.loaded;
376  end Is_loaded;
377
378  function Zip_name( info: in Zip_info ) return String is
379  begin
380    if not info.loaded then
381      raise Forgot_to_load_zip_info;
382    end if;
383    return info.zip_file_name.all;
384  end Zip_name;
385
386  function Zip_comment( info: in Zip_info ) return String is
387  begin
388    if not info.loaded then
389      raise Forgot_to_load_zip_info;
390    end if;
391    return info.zip_file_comment.all;
392  end Zip_comment;
393
394  function Zip_stream( info: in Zip_info ) return Zip_Streams.Zipstream_Class_Access
395  is
396  begin
397    if not info.loaded then
398      raise Forgot_to_load_zip_info;
399    end if;
400    return info.zip_input_stream;
401  end Zip_stream;
402
403  function Entries( info: in Zip_info ) return Natural is
404  begin
405    return info.total_entries;
406  end Entries;
407
408  ------------
409  -- Delete --
410  ------------
411
412  procedure Delete (info : in out Zip_info) is
413
414    procedure Delete( p: in out p_Dir_node ) is
415    begin
416      if p/=null then
417         Delete(p.left);
418         Delete(p.right);
419         Dispose(p);
420         p:= null;
421      end if;
422    end Delete;
423
424  begin
425    if not info.loaded then
426      raise Forgot_to_load_zip_info;
427    end if;
428    Delete( info.dir_binary_tree );
429    Dispose( info.zip_file_name );
430    Dispose( info.zip_file_comment );
431    info.loaded:= False; -- <-- added 14-Jan-2002
432  end Delete;
433
434  -- Traverse a whole Zip_info directory in sorted order, giving the
435  -- name for each entry to an user-defined "Action" procedure.
436
437  generic
438    with procedure Action_private( dn: in out Dir_node );
439    -- Dir_node is private: only known to us, contents subject to change
440  procedure Traverse_private( z: Zip_info );
441
442  procedure Traverse_private( z: Zip_info ) is
443
444    procedure Traverse_tree( p: p_Dir_node ) is
445    begin
446      if p /= null then
447        Traverse_tree (p.left);
448        Action_private (p.all);
449        Traverse_tree (p.right);
450      end if;
451    end Traverse_tree;
452
453  begin
454    Traverse_tree (z.dir_binary_tree);
455  end Traverse_private;
456
457  -----------------------
458  --  Public versions  --
459  -----------------------
460
461  procedure Traverse( z: Zip_info ) is
462    procedure My_Action_private( dn: in out Dir_node ) is
463    pragma Inline(My_Action_private);
464    begin
465      Action(dn.file_name);
466    end My_Action_private;
467    procedure My_Traverse_private is new Traverse_private(My_Action_private);
468  begin
469    My_Traverse_private(z);
470  end Traverse;
471
472  procedure Traverse_Unicode( z: Zip_info ) is
473    procedure My_Action_private( dn: in out Dir_node ) is
474    pragma Inline(My_Action_private);
475    begin
476      Action(dn.file_name, dn.name_encoding);
477    end My_Action_private;
478    procedure My_Traverse_private is new Traverse_private(My_Action_private);
479  begin
480    My_Traverse_private(z);
481  end Traverse_Unicode;
482
483  procedure Traverse_verbose( z: Zip_info ) is
484    procedure My_Action_private( dn: in out Dir_node ) is
485    pragma Inline(My_Action_private);
486    begin
487      Action(
488        dn.file_name,
489        dn.file_index,
490        dn.comp_size,
491        dn.uncomp_size,
492        dn.crc_32,
493        dn.date_time,
494        dn.method,
495        dn.name_encoding,
496        dn.read_only,
497        dn.encrypted_2_x,
498        dn.user_code
499      );
500    end My_Action_private;
501    procedure My_Traverse_private is new Traverse_private(My_Action_private);
502  begin
503    My_Traverse_private(z);
504  end Traverse_verbose;
505
506  procedure Tree_stat(
507    z        : in     Zip_info;
508    total    :    out Natural;
509    max_depth:    out Natural;
510    avg_depth:    out Float
511  )
512  is
513    sum_depth: Natural:= 0;
514
515    procedure Traverse_tree( p: p_Dir_node; depth: Natural ) is
516    begin
517      if p /= null then
518        total:= total + 1;
519        if depth > max_depth then
520          max_depth:= depth;
521        end if;
522        sum_depth:= sum_depth + depth;
523        Traverse_tree (p.left, depth + 1);
524        Traverse_tree (p.right, depth + 1);
525      end if;
526    end Traverse_tree;
527
528  begin
529    total:= 0;
530    max_depth:= 0;
531    Traverse_tree(z.dir_binary_tree, 0);
532    if total = 0 then
533      avg_depth:= 0.0;
534    else
535      avg_depth:= Float(sum_depth) / Float(total);
536    end if;
537  end Tree_stat;
538
539  -- 13-May-2001: Find_first_offset
540
541  -- For an all-files unzipping of an appended (e.g. self-extracting) archive
542  -- (not beginning with ZIP contents), we cannot start with
543  -- index 1 in file.
544  -- But the offset of first entry in ZIP directory is not valid either,
545  -- as this excerpt of appnote.txt states:
546
547  -- "   4)  The entries in the central directory may not necessarily
548  --         be in the same order that files appear in the zipfile.    "
549
550  procedure Find_first_offset(
551    file           : in out Zip_Streams.Root_Zipstream_Type'Class;
552    file_index     :    out Zip_Streams.ZS_Index_Type
553  )
554  is
555    the_end   : Zip.Headers.End_of_Central_Dir;
556    header    : Zip.Headers.Central_File_Header;
557    min_offset: File_size_type;
558    use Zip_Streams;
559  begin
560    Zip.Headers.Load(file, the_end);
561    Set_Index(
562      file,
563      ZS_Index_Type (1 + the_end.central_dir_offset) + the_end.offset_shifting
564    );
565
566    min_offset:= the_end.central_dir_offset; -- will be lowered if the archive is not empty.
567
568    if the_end.total_entries = 0 then
569      raise Archive_is_empty;
570    end if;
571
572    for i in 1..the_end.total_entries loop
573      Zip.Headers.Read_and_check(file, header );
574      Set_Index( file,
575        Index( file ) +
576        ZS_Size_Type
577             ( header.short_info.filename_length +
578               header.short_info.extra_field_length +
579               header.comment_length
580              )
581      );
582      -- Now the whole i_th central directory entry is behind
583
584      if header.local_header_offset < min_offset then
585        min_offset:= header.local_header_offset;
586      end if;
587    end loop;
588
589    file_index:= Zip_Streams.ZS_Index_Type (1 + min_offset) + the_end.offset_shifting;
590
591  exception
592    when Zip.Headers.bad_end | Ada.IO_Exceptions.End_Error =>
593      Raise_Exception (Zip.Archive_corrupted'Identity, "Bad (or no) end-of-central-directory");
594    when Zip.Headers.bad_central_header =>
595      Raise_Exception (Zip.Archive_corrupted'Identity, "Bad central directory entry header");
596  end Find_first_offset;
597
598  -- Internal: find offset of a zipped file by reading sequentially the
599  -- central directory :-(
600
601  procedure Find_offset(
602    file           : in out Zip_Streams.Root_Zipstream_Type'Class;
603    name           : in     String;
604    case_sensitive : in     Boolean;
605    file_index     :    out Zip_Streams.ZS_Index_Type;
606    comp_size      :    out File_size_type;
607    uncomp_size    :    out File_size_type;
608    crc_32         :    out Interfaces.Unsigned_32
609  )
610  is
611    the_end: Zip.Headers.End_of_Central_Dir;
612    header : Zip.Headers.Central_File_Header;
613    use Zip_Streams;
614  begin
615    Zip.Headers.Load(file, the_end);
616    Set_Index(file, ZS_Index_Type(1 + the_end.central_dir_offset) + the_end.offset_shifting);
617    for i in 1..the_end.total_entries loop
618      Zip.Headers.Read_and_check(file, header);
619      declare
620        this_name: String(1..Natural(header.short_info.filename_length));
621      begin
622        String'Read(file'Access, this_name);
623        Set_Index( file,
624          Index( file ) +
625          ZS_Size_Type(
626                  header.short_info.extra_field_length +
627                  header.comment_length
628          )
629        );
630        -- Now the whole i_th central directory entry is behind
631        if Normalize(this_name,case_sensitive) =
632           Normalize(name,case_sensitive)
633        then
634          -- Name found in central directory !
635          file_index := Zip_Streams.ZS_Index_Type (1 + header.local_header_offset) + the_end.offset_shifting;
636          comp_size  := File_size_type(header.short_info.dd.compressed_size);
637          uncomp_size:= File_size_type(header.short_info.dd.uncompressed_size);
638          crc_32     := header.short_info.dd.crc_32;
639          return;
640        end if;
641      end;
642    end loop;
643    Raise_Exception (File_name_not_found'Identity, "Entry: [" & name & ']');
644  exception
645    when Zip.Headers.bad_end =>
646      Raise_Exception (Zip.Archive_corrupted'Identity, "Bad (or no) end-of-central-directory");
647    when Zip.Headers.bad_central_header =>
648      Raise_Exception (Zip.Archive_corrupted'Identity, "Bad central directory entry header");
649  end Find_offset;
650
651  -- Internal: find offset of a zipped file using the zip_info tree 8-)
652
653  procedure Find_offset(
654    info           : in     Zip_info;
655    name           : in     String;
656    name_encoding  :    out Zip_name_encoding;
657    file_index     :    out Zip_Streams.ZS_Index_Type;
658    comp_size      :    out File_size_type;
659    uncomp_size    :    out File_size_type;
660    crc_32         :    out Interfaces.Unsigned_32
661  )
662  is
663    aux: p_Dir_node:= info.dir_binary_tree;
664    up_name: constant String:= Normalize(name, info.case_sensitive);
665  begin
666    if not info.loaded then
667      raise Forgot_to_load_zip_info;
668    end if;
669    while aux /= null loop
670      if up_name > aux.dico_name then
671        aux:= aux.right;
672      elsif up_name < aux.dico_name then
673        aux:= aux.left;
674      else  -- entry found !
675        name_encoding := aux.name_encoding;
676        file_index    := aux.file_index;
677        comp_size     := aux.comp_size;
678        uncomp_size   := aux.uncomp_size;
679        crc_32        := aux.crc_32;
680        return;
681      end if;
682    end loop;
683    Ada.Exceptions.Raise_Exception(
684      File_name_not_found'Identity,
685      "Archive: [" & info.zip_file_name.all & "], entry: [" & name & ']'
686    );
687  end Find_offset;
688
689  procedure Find_offset_without_directory(
690    info           : in     Zip_info;
691    name           : in     String;
692    name_encoding  :    out Zip_name_encoding;
693    file_index     :    out Zip_Streams.ZS_Index_Type;
694    comp_size      :    out File_size_type;
695    uncomp_size    :    out File_size_type;
696    crc_32         :    out Interfaces.Unsigned_32
697  )
698  is
699    function Trash_dir( n: String ) return String is
700      idx: Integer:= n'First - 1;
701    begin
702      for i in n'Range loop
703        if n(i)= '/' or n(i)='\' then
704          idx:= i;
705        end if;
706      end loop;
707      -- idx points on the index just before the interesting part
708      return Normalize(n( idx+1 .. n'Last ), info.case_sensitive);
709    end Trash_dir;
710
711    simple_name: constant String:= Trash_dir(name);
712
713    Found: exception;
714
715    procedure Check_entry(
716      entry_name          : String; -- 'name' is compressed entry's name
717      entry_index         : Zip_Streams.ZS_Index_Type;
718      entry_comp_size     : File_size_type;
719      entry_uncomp_size   : File_size_type;
720      entry_crc_32        : Interfaces.Unsigned_32;
721      date_time           : Time;
722      method              : PKZip_method;
723      entry_name_encoding : Zip_name_encoding;
724      read_only           : Boolean;
725      encrypted_2_x       : Boolean; -- PKZip 2.x encryption
726      user_code           : in out Integer
727    )
728    is
729    pragma Unreferenced (date_time, method, read_only, encrypted_2_x, user_code);
730    begin
731      if Trash_dir(entry_name) = simple_name then
732        name_encoding := entry_name_encoding;
733        file_index    := entry_index;
734        comp_size     := entry_comp_size;
735        uncomp_size   := entry_uncomp_size;
736        crc_32        := entry_crc_32;
737        raise Found;
738      end if;
739    end Check_entry;
740    --
741    procedure Search is new Traverse_verbose(Check_entry);
742    --
743  begin
744    begin
745      Search(info);
746    exception
747      when Found =>
748        return;
749    end;
750    raise File_name_not_found;
751  end Find_offset_without_directory;
752
753  function Exists(
754    info           : in     Zip_info;
755    name           : in     String
756  )
757  return Boolean
758  is
759    aux: p_Dir_node:= info.dir_binary_tree;
760    up_name: constant String:= Normalize(name, info.case_sensitive);
761  begin
762    if not info.loaded then
763      raise Forgot_to_load_zip_info;
764    end if;
765    while aux /= null loop
766      if up_name > aux.dico_name then
767        aux:= aux.right;
768      elsif up_name < aux.dico_name then
769        aux:= aux.left;
770      else  -- entry found !
771        return True;
772      end if;
773    end loop;
774    return False;
775  end Exists;
776
777  procedure Set_user_code(
778    info           : in Zip_info;
779    name           : in String;
780    code           : in Integer
781  )
782  is
783    aux: p_Dir_node:= info.dir_binary_tree;
784    up_name: constant String:= Normalize(name, info.case_sensitive);
785  begin
786    if not info.loaded then
787      raise Forgot_to_load_zip_info;
788    end if;
789    while aux /= null loop
790      if up_name > aux.dico_name then
791        aux:= aux.right;
792      elsif up_name < aux.dico_name then
793        aux:= aux.left;
794      else  -- entry found !
795        aux.user_code:= code;
796        return;
797      end if;
798    end loop;
799    Ada.Exceptions.Raise_Exception(
800      File_name_not_found'Identity,
801      "Archive: [" & info.zip_file_name.all & "], entry: [" & name & ']'
802    );
803  end Set_user_code;
804
805  function User_code(
806    info           : in Zip_info;
807    name           : in String
808  )
809  return Integer
810  is
811    aux: p_Dir_node:= info.dir_binary_tree;
812    up_name: constant String:= Normalize(name, info.case_sensitive);
813  begin
814    if not info.loaded then
815      raise Forgot_to_load_zip_info;
816    end if;
817    while aux /= null loop
818      if up_name > aux.dico_name then
819        aux:= aux.right;
820      elsif up_name < aux.dico_name then
821        aux:= aux.left;
822      else  -- entry found !
823        return aux.user_code;
824      end if;
825    end loop;
826    Ada.Exceptions.Raise_Exception(
827      File_name_not_found'Identity,
828      "Archive: [" & info.zip_file_name.all & "], entry: [" & name & ']'
829    );
830    return 0;  --  Fake, since exception has been raised just before. Removes an OA warning.
831  end User_code;
832
833  procedure Get_sizes(
834    info           : in     Zip_info;
835    name           : in     String;
836    comp_size      :    out File_size_type;
837    uncomp_size    :    out File_size_type
838  )
839  is
840    dummy_file_index: Zip_Streams.ZS_Index_Type;
841    dummy_name_encoding: Zip_name_encoding;
842    dummy_crc_32: Interfaces.Unsigned_32;
843  begin
844    Find_offset(
845      info, name, dummy_name_encoding, dummy_file_index,
846      comp_size, uncomp_size, dummy_crc_32
847    );
848  end Get_sizes;
849
850  -- Workaround for the severe xxx'Read xxx'Write performance
851  -- problems in the GNAT and ObjectAda compilers (as in 2009)
852  -- This is possible if and only if Byte = Stream_Element and
853  -- arrays types are both packed and aligned the same way.
854  --
855  subtype Size_test_a is Byte_Buffer(1..19);
856  subtype Size_test_b is Ada.Streams.Stream_Element_Array(1..19);
857  workaround_possible: constant Boolean:=
858    Size_test_a'Size = Size_test_b'Size and
859    Size_test_a'Alignment = Size_test_b'Alignment;
860
861  -- BlockRead - general-purpose procedure (nothing really specific
862  -- to Zip / UnZip): reads either the whole buffer from a file, or
863  -- if the end of the file lays inbetween, a part of the buffer.
864
865  procedure BlockRead(
866    file         : in     Ada.Streams.Stream_IO.File_Type;
867    buffer       :    out Byte_Buffer;
868    actually_read:    out Natural
869  )
870  is
871    use Ada.Streams, Ada.Streams.Stream_IO;
872    SE_Buffer   : Stream_Element_Array (1 .. buffer'Length);
873    for SE_Buffer'Address use buffer'Address;
874    pragma Import (Ada, SE_Buffer);
875    Last_Read   : Stream_Element_Offset;
876  begin
877    if workaround_possible then
878      Read(Stream(file).all, SE_Buffer, Last_Read);
879      actually_read:= Natural(Last_Read);
880    else
881      if End_Of_File(file) then
882        actually_read:= 0;
883      else
884        actually_read:=
885          Integer'Min( buffer'Length, Integer(Size(file) - Index(file) + 1) );
886        Byte_Buffer'Read(
887          Stream(file),
888          buffer(buffer'First .. buffer'First + actually_read - 1)
889        );
890      end if;
891    end if;
892  end BlockRead;
893
894  procedure BlockRead(
895    stream       : in out Zip_Streams.Root_Zipstream_Type'Class;
896    buffer       :    out Byte_Buffer;
897    actually_read:    out Natural
898  )
899  is
900    use Ada.Streams, Zip_Streams;
901    SE_Buffer   : Stream_Element_Array (1 .. buffer'Length);
902    for SE_Buffer'Address use buffer'Address;
903    pragma Import (Ada, SE_Buffer);
904    Last_Read   : Stream_Element_Offset;
905  begin
906    if workaround_possible then
907      Read(stream, SE_Buffer, Last_Read);
908      actually_read:= Natural(Last_Read);
909    else
910      if End_Of_Stream(stream) then
911        actually_read:= 0;
912      else
913        actually_read:=
914          Integer'Min( buffer'Length, Integer(Size(stream) - Index(stream) + 1) );
915        Byte_Buffer'Read(
916          stream'Access,
917          buffer(buffer'First .. buffer'First + actually_read - 1)
918        );
919      end if;
920    end if;
921  end BlockRead;
922
923  procedure BlockRead(
924    stream : in out Zip_Streams.Root_Zipstream_Type'Class;
925    buffer :    out Byte_Buffer
926  )
927  is
928    actually_read: Natural;
929  begin
930    BlockRead(stream, buffer, actually_read);
931    if actually_read < buffer'Length then
932      raise Ada.IO_Exceptions.End_Error;
933    end if;
934  end BlockRead;
935
936  procedure BlockWrite(
937    stream : in out Ada.Streams.Root_Stream_Type'Class;
938    buffer : in     Byte_Buffer
939  )
940  is
941    use Ada.Streams;
942    SE_Buffer   : Stream_Element_Array (1 .. buffer'Length);
943    for SE_Buffer'Address use buffer'Address;
944    pragma Import (Ada, SE_Buffer);
945  begin
946    if workaround_possible then
947      Ada.Streams.Write(stream, SE_Buffer);
948    else
949      Byte_Buffer'Write(stream'Access, buffer);
950      -- ^This is 30x to 70x slower on GNAT 2009 !
951    end if;
952  end BlockWrite;
953
954  function Image(m: PKZip_method) return String is
955  begin
956    case m is
957      when store     => return "Store";
958      when shrink    => return "Shrink";
959      when reduce_1  => return "Reduce 1";
960      when reduce_2  => return "Reduce 2";
961      when reduce_3  => return "Reduce 3";
962      when reduce_4  => return "Reduce 4";
963      when implode   => return "Implode";
964      when tokenize  => return "Tokenize";
965      when deflate   => return "Deflate";
966      when deflate_e => return "Deflate64";
967      when bzip2     => return "BZip2";
968      when lzma_meth => return "LZMA";
969      when ppmd      => return "PPMd";
970      when unknown   => return "(unknown)";
971    end case;
972  end Image;
973
974  function Method_from_code(x: Natural) return PKZip_method is
975    -- An enumeration clause might be more elegant, but needs
976    -- curiously an Unchecked_Conversion... (RM 13.4)
977  begin
978    case x is
979      when compression_format_code.store      => return store;
980      when compression_format_code.shrink     => return shrink;
981      when compression_format_code.reduce     => return reduce_1;
982      when compression_format_code.reduce + 1 => return reduce_2;
983      when compression_format_code.reduce + 2 => return reduce_3;
984      when compression_format_code.reduce + 3 => return reduce_4;
985      when compression_format_code.implode    => return implode;
986      when compression_format_code.tokenize   => return tokenize;
987      when compression_format_code.deflate    => return deflate;
988      when compression_format_code.deflate_e  => return deflate_e;
989      when compression_format_code.bzip2      => return bzip2;
990      when compression_format_code.lzma       => return lzma_meth;
991      when compression_format_code.ppmd       => return ppmd;
992      when others => return unknown;
993    end case;
994  end Method_from_code;
995
996  function Method_from_code(x: Interfaces.Unsigned_16) return PKZip_method is
997  begin
998    return Method_from_code(Natural(x));
999  end Method_from_code;
1000
1001  -- Copy a chunk from a stream into another one, using a temporary buffer
1002  procedure Copy_chunk (
1003    from       : in out Zip_Streams.Root_Zipstream_Type'Class;
1004    into       : in out Ada.Streams.Root_Stream_Type'Class;
1005    bytes      : Natural;
1006    buffer_size: Positive:= 1024*1024;
1007    Feedback   : Feedback_proc:= null
1008  )
1009  is
1010    buf: Zip.Byte_Buffer(1..buffer_size);
1011    actually_read, remains: Natural;
1012    user_abort: Boolean:= False;
1013  begin
1014    remains:= bytes;
1015    while remains > 0 loop
1016      if Feedback /= null then
1017        Feedback(
1018          100 - Integer(100.0 * Float(remains) / Float(bytes)),
1019          False,
1020          user_abort
1021        );
1022        -- !! do something if user_abort = True !!
1023      end if;
1024      Zip.BlockRead(from, buf(1..Integer'Min(remains, buf'Last)), actually_read);
1025      if actually_read = 0 then -- premature end, unexpected
1026        raise Zip.Archive_corrupted;
1027      end if;
1028      remains:= remains - actually_read;
1029      Zip.BlockWrite(into, buf(1..actually_read));
1030    end loop;
1031  end Copy_chunk;
1032
1033  -- Copy a whole file into a stream, using a temporary buffer
1034  procedure Copy_file(
1035    file_name  : String;
1036    into       : in out Ada.Streams.Root_Stream_Type'Class;
1037    buffer_size: Positive:= 1024*1024
1038  )
1039  is
1040    use Ada.Streams.Stream_IO;
1041    f: File_Type;
1042    buf: Zip.Byte_Buffer(1..buffer_size);
1043    actually_read: Natural;
1044  begin
1045    Open(f, In_File, file_name);
1046    loop
1047      Zip.BlockRead(f, buf, actually_read);
1048      exit when actually_read = 0; -- this is expected
1049      Zip.BlockWrite(into, buf(1..actually_read));
1050    end loop;
1051    Close(f);
1052  end Copy_file;
1053
1054  -- This does the same as Ada 2005's Ada.Directories.Exists
1055  -- Just there as helper for Ada 95 only systems
1056  --
1057  function Exists(name:String) return Boolean is
1058    use Ada.Text_IO, Ada.Strings.Fixed;
1059    f: File_Type;
1060  begin
1061    if Index(name, "*") > 0 then
1062      return False;
1063    end if;
1064    Open(f,In_File,name, Form => Ada.Strings.Unbounded.To_String (Zip_Streams.Form_For_IO_Open_and_Create));
1065    Close(f);
1066    return True;
1067  exception
1068    when Name_Error =>
1069      return False; -- The file cannot exist !
1070    when Use_Error =>
1071      return True;  -- The file exist and is already opened !
1072  end Exists;
1073
1074  procedure Put_Multi_Line(
1075    out_file :        Ada.Text_IO.File_Type;
1076    text     :        String
1077  )
1078  is
1079    last_char: Character:= ' ';
1080    c: Character;
1081  begin
1082    for i in text'Range loop
1083      c:= text(i);
1084      case c is
1085        when ASCII.CR =>
1086          Ada.Text_IO.New_Line(out_file);
1087        when ASCII.LF =>
1088          if last_char /= ASCII.CR then Ada.Text_IO.New_Line(out_file); end if;
1089        when others =>
1090          Ada.Text_IO.Put(out_file, c);
1091      end case;
1092      last_char:= c;
1093    end loop;
1094  end Put_Multi_Line;
1095
1096  procedure Write_as_text(
1097    out_file :        Ada.Text_IO.File_Type;
1098    buffer   :        Byte_Buffer;
1099    last_char: in out Character -- track line-ending characters across writes
1100  )
1101  is
1102    c: Character;
1103  begin
1104    for i in buffer'Range loop
1105      c:= Character'Val(buffer(i));
1106      case c is
1107        when ASCII.CR =>
1108          Ada.Text_IO.New_Line(out_file);
1109        when ASCII.LF =>
1110          if last_char /= ASCII.CR then Ada.Text_IO.New_Line(out_file); end if;
1111        when others =>
1112          Ada.Text_IO.Put(out_file, c);
1113      end case;
1114      last_char:= c;
1115    end loop;
1116  end Write_as_text;
1117
1118  function Hexadecimal(x: Interfaces.Unsigned_32) return String
1119  is
1120    package MIO is new Ada.Text_IO.Modular_IO(Interfaces.Unsigned_32);
1121    str: String(1..12);
1122    use Ada.Strings.Fixed;
1123  begin
1124    MIO.Put(str, x, 16);
1125    return str(Index(str,"#")+1..11);
1126  end Hexadecimal;
1127
1128end Zip;
1129