Back to... Zip-Ada

Source file : zip.adb



--  Legal licensing note:

--  Copyright (c) 1999 .. 2023 Gautier de Montmollin
--  SWITZERLAND

--  Permission is hereby granted, free of charge, to any person obtaining a copy
--  of this software and associated documentation files (the "Software"), to deal
--  in the Software without restriction, including without limitation the rights
--  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
--  copies of the Software, and to permit persons to whom the Software is
--  furnished to do so, subject to the following conditions:

--  The above copyright notice and this permission notice shall be included in
--  all copies or substantial portions of the Software.

--  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
--  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
--  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
--  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
--  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
--  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
--  THE SOFTWARE.

--  NB: this is the MIT License, as found 12-Sep-2007 on the site
--  http://www.opensource.org/licenses/mit-license.php

with Zip.Headers;

with Ada.Characters.Handling,
     Ada.Exceptions,
     Ada.Unchecked_Deallocation,
     Ada.IO_Exceptions,
     Ada.Strings.Fixed,
     Ada.Strings.Unbounded;

package body Zip is

  use Interfaces;

  procedure Dispose is new Ada.Unchecked_Deallocation (Dir_node, p_Dir_node);
  procedure Dispose is new Ada.Unchecked_Deallocation (String, p_String);

  package Binary_tree_rebalancing is
    procedure Rebalance (root : in out p_Dir_node);
  end Binary_tree_rebalancing;

  package body Binary_tree_rebalancing is

    --------------------------------------------------------------------
    --  Tree Rebalancing in Optimal Time and Space                    --
    --  QUENTIN F. STOUT and BETTE L. WARREN                          --
    --  Communications of the ACM September 1986 Volume 29 Number 9   --
    --------------------------------------------------------------------
    --  http://www.eecs.umich.edu/~qstout/pap/CACM86.pdf
    --
    --  Translated by (New) P2Ada v. 15-Nov-2006

    procedure Tree_to_vine (root : p_Dir_node; size : out Integer)
      --  transform the tree with pseudo-root
      --   "root^" into a vine with pseudo-root
      --   node "root^", and store the number of
      --   nodes in "size"
    is
      vine_tail, remainder, temp : p_Dir_node;
    begin
      vine_tail := root;
      remainder := vine_tail.right;
      size := 0;
      while remainder /= null loop
        if remainder.left = null then
          --  move vine-tail down one:
          vine_tail := remainder;
          remainder := remainder.right;
          size := size + 1;
        else
          --  rotate:
          temp := remainder.left;
          remainder.left := temp.right;
          temp.right := remainder;
          remainder := temp;
          vine_tail.right := temp;
        end if;
      end loop;
    end Tree_to_vine;

    procedure Vine_to_tree (root : p_Dir_node; size_given : Integer) is
      --  convert the vine with "size" nodes and pseudo-root
      --  node "root^" into a balanced tree
      leaf_count : Integer;
      size : Integer := size_given;

      procedure Compression (root_compress : p_Dir_node; count : Integer) is
        --  Compress "count" spine nodes in the tree with pseudo-root "root_compress^"
        scanner, child : p_Dir_node;
      begin
        scanner := root_compress;
        for counter in reverse 1 .. count loop
          child         := scanner.right;
          scanner.right := child.right;
          scanner       := scanner.right;
          child.right   := scanner.left;
          scanner.left  := child;
        end loop;
      end Compression;

      --  Returns n - 2 ** Integer( Float'Floor( log( Float(n) ) / log(2.0) ) )
      --  without Float-Point calculation and rounding errors with too short floats
      function Remove_leading_binary_1 (n : Integer) return Integer is
        x : Integer := 2**16;  --  supposed maximum
      begin
        if n < 1 then
          return n;
        end if;
        while n mod x = n loop
          x := x / 2;
        end loop;
        return n mod x;
      end Remove_leading_binary_1;

    begin --  Vine_to_tree
      leaf_count := Remove_leading_binary_1 (size + 1);
      Compression (root, leaf_count);  --  create deepest leaves
      --  use Perfect_leaves instead for a perfectly balanced tree
      size := size - leaf_count;
      while size > 1 loop
        Compression (root, size / 2);
        size := size / 2;
      end loop;
    end Vine_to_tree;

    procedure Rebalance (root : in out p_Dir_node) is
      --  Rebalance the binary search tree with root "root.all",
      --  with the result also rooted at "root.all".
      --  Uses the Tree_to_vine and Vine_to_tree procedures.
      pseudo_root : p_Dir_node;
      size : Integer;
    begin
      pseudo_root := new Dir_node (name_len => 0);
      pseudo_root.right := root;
      Tree_to_vine (pseudo_root, size);
      Vine_to_tree (pseudo_root, size);
      root := pseudo_root.right;
      Dispose (pseudo_root);
    end Rebalance;

  end Binary_tree_rebalancing;

  --  19-Jun-2001: Enhanced file name identification
  --               a) when case insensitive  -> all UPPER (current)
  --               b) '\' and '/' identified -> all '/'   (new)

  function Normalize (s : String; case_sensitive : Boolean) return String is
    sn : String (s'Range);
  begin
    if case_sensitive then
      sn := s;
    else
      sn := Ada.Characters.Handling.To_Upper (s);
    end if;
    for i in sn'Range loop
      if sn (i) = '\' then
        sn (i) := '/';
      end if;
    end loop;
    return sn;
  end Normalize;

  boolean_to_encoding : constant array (Boolean) of Zip_Name_Encoding :=
    (False => IBM_437, True => UTF_8);

  -------------------------------------------------------------
  -- Load Zip_info from a stream containing the .zip archive --
  -------------------------------------------------------------

  procedure Load (
    info            :    out Zip_info;
    from            : in out Zip_Streams.Root_Zipstream_Type'Class;
    case_sensitive  : in     Boolean := False;
    duplicate_names : in     Duplicate_name_policy := error_on_duplicate
  )
  is
    procedure Insert (
      dico_name        : String; -- UPPER if case-insensitive search
      file_name        : String;
      file_index       : Zip_Streams.ZS_Index_Type;
      comp_size,
      uncomp_size      : Zip_64_Data_Size_Type;
      crc_32           : Unsigned_32;
      date_time        : Time;
      method           : PKZip_method;
      name_encoding    : Zip_Name_Encoding;
      read_only        : Boolean;
      encrypted_2_x    : Boolean;
      root_node        : in out p_Dir_node
      )
    is
      procedure Insert_into_tree (node : in out p_Dir_node) is
      begin
        if node = null then
          node := new Dir_node'
            ((name_len          => file_name'Length,
               left              => null,
               right             => null,
               dico_name         => dico_name,
               file_name         => file_name,
               file_index        => file_index,
               comp_size         => comp_size,
               uncomp_size       => uncomp_size,
               crc_32            => crc_32,
               date_time         => date_time,
               method            => method,
               name_encoding     => name_encoding,
               read_only         => read_only,
               encrypted_2_x     => encrypted_2_x,
               user_code         => 0
               )
            );
        elsif dico_name > node.dico_name then
          Insert_into_tree (node.right);
        elsif dico_name < node.dico_name then
          Insert_into_tree (node.left);
        else
          --  Here we have a case where the entry name already exists in the dictionary.
          case duplicate_names is
            when error_on_duplicate =>
              raise Duplicate_name with
                 "Same full entry name (in dictionary: " & dico_name &
                 ") appears twice in archive directory; " &
                 "procedure Load was called with strict name policy.";
            when admit_duplicates =>
              if file_index > node.file_index then
                Insert_into_tree (node.right);
              elsif file_index < node.file_index then
                Insert_into_tree (node.left);
              else
                raise Duplicate_name with
                   "Archive directory corrupt: same full entry name (in dictionary: " &
                   dico_name & "), with same data position, appear twice.";
              end if;
          end case;
        end if;
      end Insert_into_tree;
      --
    begin
      Insert_into_tree (root_node);
    end Insert;

    the_end : Zip.Headers.End_of_Central_Dir;
    header  : Zip.Headers.Central_File_Header;
    p       : p_Dir_node := null;
    main_comment : p_String;
  begin  --  Load Zip_info
    if info.loaded then
      Delete (info);
    end if;
    Zip.Headers.Load (from, the_end);
    --  We take the opportunity to read the main comment, which is right
    --  after the end-of-central-directory block.
    main_comment := new String (1 .. Integer (the_end.main_comment_length));
    String'Read (from'Access, main_comment.all);
    --  Process central directory:
    Zip_Streams.Set_Index (
      from,
      Zip_Streams.ZS_Index_Type (1 + the_end.central_dir_offset) + the_end.offset_shifting
    );

    for i in 1 .. the_end.total_entries loop
      Zip.Headers.Read_and_Check (from, header);
      declare
        this_name : String (1 .. Natural (header.short_info.filename_length));
        mem : Zip_Streams.ZS_Index_Type;
        head_extra : Headers.Local_File_Header_Extension;
      begin
        String'Read (from'Access, this_name);
        mem := from.Index;
        if header.short_info.extra_field_length >= 4 then
          Headers.Read_and_Check (from, head_extra);
          Headers.Interpret
            (head_extra,
             header.short_info.dd.uncompressed_size,
             header.short_info.dd.compressed_size,
             header.local_header_offset);
        end if;
        --  Skip extra field and entry comment.
        from.Set_Index
          (mem +
           Zip_Streams.ZS_Size_Type
             (header.short_info.extra_field_length +
              header.comment_length));
        --  Now the whole i_th central directory entry is behind
        Insert (dico_name   => Normalize (this_name, case_sensitive),
                file_name   => Normalize (this_name, True),
                file_index  => Zip_Streams.ZS_Index_Type (1 + header.local_header_offset) +
                               the_end.offset_shifting,
                comp_size   => header.short_info.dd.compressed_size,
                uncomp_size => header.short_info.dd.uncompressed_size,
                crc_32      => header.short_info.dd.crc_32,
                date_time   => header.short_info.file_timedate,
                method      => Method_from_Code (header.short_info.zip_type),
                name_encoding =>
                  boolean_to_encoding (
                   (header.short_info.bit_flag and
                    Zip.Headers.Language_Encoding_Flag_Bit) /= 0),
                read_only   => header.made_by_version / 256 = 0 and -- DOS-like
                               (header.external_attributes and 1) = 1,
                encrypted_2_x => (header.short_info.bit_flag and Zip.Headers.Encryption_Flag_Bit) /= 0,
                root_node     => p);
        --  Since the files are usually well ordered, the tree as inserted
        --  is very unbalanced; we need to rebalance it from time to time
        --  during loading, otherwise the insertion slows down dramatically
        --  for zip files with plenty of files - converges to
        --  O(total_entries ** 2)...
        if i mod 256 = 0 then
          Binary_tree_rebalancing.Rebalance (p);
        end if;
      end;
    end loop;
    Binary_tree_rebalancing.Rebalance (p);
    info.loaded             := True;
    info.case_sensitive     := case_sensitive;
    info.zip_file_name      := new String'("This is a stream, no direct file!");
    info.zip_input_stream   := from'Unchecked_Access;
    info.dir_binary_tree    := p;
    info.total_entries      := Integer (the_end.total_entries);
    info.zip_file_comment   := main_comment;
    info.zip_archive_format := Zip_32;
  exception
    when E : Zip.Headers.bad_end =>
      raise Zip.Archive_corrupted
        with "Bad (or no) end-of-central-directory " & Ada.Exceptions.Exception_Message (E);
    when Zip.Headers.bad_central_header =>
      raise Zip.Archive_corrupted with "Bad central directory entry header";
  end Load;

  -----------------------------------------------------------
  -- Load Zip_info from a file containing the .zip archive --
  -----------------------------------------------------------

  procedure Load (
    info            : out Zip_info;
    from            : in  String;  --  Zip file name
    case_sensitive  : in  Boolean := False;
    duplicate_names : in  Duplicate_name_policy := error_on_duplicate
  )
  is
    my_stream : aliased Zip_Streams.File_Zipstream;
  begin
    my_stream.Set_Name (from);
    begin
      my_stream.Open (Zip_Streams.In_File);
    exception
      when others =>
        raise Archive_open_error with "Archive: [" & from & ']';
    end;
    --  Call the stream version of Load(...)
    Load (
      info,
      my_stream,
      case_sensitive,
      duplicate_names
    );
    my_stream.Close;
    Dispose (info.zip_file_name);
    info.zip_file_name := new String'(from);
    info.zip_input_stream := null; -- forget about the stream!
  exception
    when others =>
      if my_stream.Is_Open then
        my_stream.Close;
      end if;
      raise;
  end Load;

  function Is_loaded (info : in Zip_info) return Boolean is
  begin
    return info.loaded;
  end Is_loaded;

  function Zip_Name (info : in Zip_info) return String is
  begin
    if not info.loaded then
      raise Forgot_to_load_zip_info;
    end if;
    return info.zip_file_name.all;
  end Zip_Name;

  function Zip_Comment (info : in Zip_info) return String is
  begin
    if not info.loaded then
      raise Forgot_to_load_zip_info;
    end if;
    return info.zip_file_comment.all;
  end Zip_Comment;

  function Zip_Stream (info : in Zip_info) return Zip_Streams.Zipstream_Class_Access
  is
  begin
    if not info.loaded then
      raise Forgot_to_load_zip_info;
    end if;
    return info.zip_input_stream;
  end Zip_Stream;

  function Entries (info : in Zip_info) return Natural is
  begin
    return info.total_entries;
  end Entries;

  ------------
  -- Delete --
  ------------

  procedure Delete (info : in out Zip_info) is

    procedure Delete (p : in out p_Dir_node) is
    begin
      if p /= null then
         Delete (p.left);
         Delete (p.right);
         Dispose (p);
         p := null;
      end if;
    end Delete;

  begin
    Delete (info.dir_binary_tree);
    Dispose (info.zip_file_name);
    Dispose (info.zip_file_comment);
    info.loaded := False;  --  <-- added 14-Jan-2002
  end Delete;

  --  Traverse a whole Zip_info directory in sorted order, giving the
  --  name for each entry to an user-defined "Action" procedure.

  generic
    with procedure Action_private (dn : in out Dir_node);
    --  Dir_node is private: only known to us, contents subject to change
  procedure Traverse_private (z : Zip_info);

  procedure Traverse_private (z : Zip_info) is

    procedure Traverse_tree (p : p_Dir_node) is
    begin
      if p /= null then
        Traverse_tree (p.left);
        Action_private (p.all);
        Traverse_tree (p.right);
      end if;
    end Traverse_tree;

  begin
    Traverse_tree (z.dir_binary_tree);
  end Traverse_private;

  -----------------------
  --  Public versions  --
  -----------------------

  procedure Traverse (z : Zip_info) is
    procedure My_Action_private (dn : in out Dir_node) is
    pragma Inline (My_Action_private);
    begin
      Action (dn.file_name);
    end My_Action_private;
    procedure My_Traverse_private is new Traverse_private (My_Action_private);
  begin
    My_Traverse_private (z);
  end Traverse;

  procedure Traverse_Unicode (z : Zip_info) is
    procedure My_Action_private (dn : in out Dir_node) is
    pragma Inline (My_Action_private);
    begin
      Action (dn.file_name, dn.name_encoding);
    end My_Action_private;
    procedure My_Traverse_private is new Traverse_private (My_Action_private);
  begin
    My_Traverse_private (z);
  end Traverse_Unicode;

  procedure Traverse_verbose (z : Zip_info) is
    procedure My_Action_private (dn : in out Dir_node) is
    pragma Inline (My_Action_private);
    begin
      Action (
        dn.file_name,
        dn.file_index,
        dn.comp_size,
        dn.uncomp_size,
        dn.crc_32,
        dn.date_time,
        dn.method,
        dn.name_encoding,
        dn.read_only,
        dn.encrypted_2_x,
        dn.user_code
      );
    end My_Action_private;
    procedure My_Traverse_private is new Traverse_private (My_Action_private);
  begin
    My_Traverse_private (z);
  end Traverse_verbose;

  procedure Tree_Stat
    (z         : in     Zip_info;
     total     :    out Natural;
     max_depth :    out Natural;
     avg_depth :    out Float)
  is
    sum_depth : Natural := 0;

    procedure Traverse_tree (p : p_Dir_node; depth : Natural) is
    begin
      if p /= null then
        total := total + 1;
        if depth > max_depth then
          max_depth := depth;
        end if;
        sum_depth := sum_depth + depth;
        Traverse_tree (p.left, depth + 1);
        Traverse_tree (p.right, depth + 1);
      end if;
    end Traverse_tree;

  begin
    total := 0;
    max_depth := 0;
    Traverse_tree (z.dir_binary_tree, 0);
    if total = 0 then
      avg_depth := 0.0;
    else
      avg_depth := Float (sum_depth) / Float (total);
    end if;
  end Tree_Stat;

  --  13-May-2001: Find_first_offset

  --  For an all-files unzipping of an appended (e.g. self-extracting) archive
  --  (not beginning with ZIP contents), we cannot start with
  --  index 1 in file.
  --  But the offset of first entry in ZIP directory is not valid either,
  --  as this excerpt of appnote.txt states:

  --  "   4)  The entries in the central directory may not necessarily
  --          be in the same order that files appear in the zipfile.    "

  procedure Find_first_Offset (
    file           : in out Zip_Streams.Root_Zipstream_Type'Class;
    file_index     :    out Zip_Streams.ZS_Index_Type
  )
  is
    the_end    : Zip.Headers.End_of_Central_Dir;
    header     : Zip.Headers.Central_File_Header;
    min_offset : Zip_64_Data_Size_Type;
    mem        : Zip_Streams.ZS_Index_Type;
    head_extra : Headers.Local_File_Header_Extension;
  begin
    Zip.Headers.Load (file, the_end);
    file.Set_Index
      (Zip_Streams.ZS_Index_Type (1 + the_end.central_dir_offset) + the_end.offset_shifting);

    min_offset := the_end.central_dir_offset; -- will be lowered if the archive is not empty.

    if the_end.total_entries = 0 then
      raise Archive_is_empty;
    end if;

    for i in 1 .. the_end.total_entries loop
      Headers.Read_and_Check (file, header);
      file.Set_Index (file.Index + Zip_Streams.ZS_Size_Type (header.short_info.filename_length));
      mem := file.Index;
      if header.short_info.extra_field_length >= 4 then
        Headers.Read_and_Check (file, head_extra);
        Headers.Interpret
          (head_extra,
           header.short_info.dd.uncompressed_size,
           header.short_info.dd.compressed_size,
           header.local_header_offset);
      end if;
      file.Set_Index
        (mem +
         Zip_Streams.ZS_Size_Type
           (header.short_info.extra_field_length +
            header.comment_length));
      --  Now the whole i_th central directory entry is behind

      if header.local_header_offset < min_offset then
        min_offset := header.local_header_offset;
      end if;
    end loop;

    file_index := Zip_Streams.ZS_Index_Type (1 + min_offset) + the_end.offset_shifting;

  exception
    when E : Zip.Headers.bad_end =>
      raise Zip.Archive_corrupted
        with "Bad (or no) end-of-central-directory " & Ada.Exceptions.Exception_Message (E);
    when Ada.IO_Exceptions.End_Error =>
      raise Zip.Archive_corrupted
        with "Bad (or no) end-of-central-directory (end of stream reached)";
    when Zip.Headers.bad_central_header =>
      raise Zip.Archive_corrupted with "Bad central directory entry header";
  end Find_first_Offset;

  --  Internal: find offset of a zipped file by reading sequentially the
  --  central directory :-(

  procedure Find_Offset (
    file           : in out Zip_Streams.Root_Zipstream_Type'Class;
    name           : in     String;
    case_sensitive : in     Boolean;
    file_index     :    out Zip_Streams.ZS_Index_Type;
    comp_size      :    out Zip_64_Data_Size_Type;
    uncomp_size    :    out Zip_64_Data_Size_Type;
    crc_32         :    out Interfaces.Unsigned_32
  )
  is
    the_end : Zip.Headers.End_of_Central_Dir;
    header  : Zip.Headers.Central_File_Header;
    mem : Zip_Streams.ZS_Index_Type;
    head_extra : Headers.Local_File_Header_Extension;
  begin
    Zip.Headers.Load (file, the_end);
    file.Set_Index
      (Zip_Streams.ZS_Index_Type (1 + the_end.central_dir_offset) + the_end.offset_shifting);
    for i in 1 .. the_end.total_entries loop
      Zip.Headers.Read_and_Check (file, header);
      declare
        this_name : String (1 .. Natural (header.short_info.filename_length));
      begin
        String'Read (file'Access, this_name);
        mem := file.Index;
        if header.short_info.extra_field_length >= 4 then
          Headers.Read_and_Check (file, head_extra);
          Headers.Interpret
            (head_extra,
             header.short_info.dd.uncompressed_size,
             header.short_info.dd.compressed_size,
             header.local_header_offset);
          end if;
        file.Set_Index
          (mem +
            Zip_Streams.ZS_Size_Type
              (header.short_info.extra_field_length +
               header.comment_length));
        --  Now the whole i_th central directory entry is behind
        if Normalize (this_name, case_sensitive) =
           Normalize (name, case_sensitive)
        then
          --  Name found in central directory !
          file_index  := Zip_Streams.ZS_Index_Type (1 + header.local_header_offset) + the_end.offset_shifting;
          comp_size   := Zip_64_Data_Size_Type (header.short_info.dd.compressed_size);
          uncomp_size := Zip_64_Data_Size_Type (header.short_info.dd.uncompressed_size);
          crc_32      := header.short_info.dd.crc_32;
          return;
        end if;
      end;
    end loop;
    raise Entry_name_not_found with "Entry: [" & name & ']';
  exception
    when Zip.Headers.bad_end =>
      raise Zip.Archive_corrupted with "Bad (or no) end-of-central-directory";
    when Zip.Headers.bad_central_header =>
      raise Zip.Archive_corrupted with "Bad central directory entry header";
  end Find_Offset;

  --  Internal: find offset of a zipped file using the zip_info tree 8-)

  procedure Find_Offset
    (info           : in     Zip_info;
     name           : in     String;
     name_encoding  :    out Zip_Name_Encoding;
     file_index     :    out Zip_Streams.ZS_Index_Type;
     comp_size      :    out Zip_64_Data_Size_Type;
     uncomp_size    :    out Zip_64_Data_Size_Type;
     crc_32         :    out Interfaces.Unsigned_32)
  is
    aux : p_Dir_node := info.dir_binary_tree;
    up_name : constant String := Normalize (name, info.case_sensitive);
  begin
    if not info.loaded then
      raise Forgot_to_load_zip_info;
    end if;
    while aux /= null loop
      if up_name > aux.dico_name then
        aux := aux.right;
      elsif up_name < aux.dico_name then
        aux := aux.left;
      else  -- entry found !
        name_encoding := aux.name_encoding;
        file_index    := aux.file_index;
        comp_size     := aux.comp_size;
        uncomp_size   := aux.uncomp_size;
        crc_32        := aux.crc_32;
        return;
      end if;
    end loop;
    raise Entry_name_not_found with "Archive: [" & info.zip_file_name.all & "], entry: [" & name & ']';
  end Find_Offset;

  procedure Find_Offset_without_Directory
    (info           : in     Zip_info;
     name           : in     String;
     name_encoding  :    out Zip_Name_Encoding;
     file_index     :    out Zip_Streams.ZS_Index_Type;
     comp_size      :    out Zip_64_Data_Size_Type;
     uncomp_size    :    out Zip_64_Data_Size_Type;
     crc_32         :    out Interfaces.Unsigned_32)
  is
    function Trash_dir (n : String) return String is
      idx : Integer := n'First - 1;
    begin
      for i in n'Range loop
        if n (i) in '/' | '\' then
          idx := i;
        end if;
      end loop;
      --  idx points on the index just before the interesting part
      return Normalize (n (idx + 1 .. n'Last), info.case_sensitive);
    end Trash_dir;

    simple_name : constant String := Trash_dir (name);

    Found : exception;

    procedure Check_entry (
      entry_name          : String; -- 'name' is compressed entry's name
      entry_index         : Zip_Streams.ZS_Index_Type;
      entry_comp_size     : Zip_64_Data_Size_Type;
      entry_uncomp_size   : Zip_64_Data_Size_Type;
      entry_crc_32        : Interfaces.Unsigned_32;
      date_time           : Time;
      method              : PKZip_method;
      entry_name_encoding : Zip_Name_Encoding;
      read_only           : Boolean;
      encrypted_2_x       : Boolean; -- PKZip 2.x encryption
      entry_user_code     : in out Integer
    )
    is
    pragma Unreferenced (date_time, method, read_only, encrypted_2_x, entry_user_code);
    begin
      if Trash_dir (entry_name) = simple_name then
        name_encoding := entry_name_encoding;
        file_index    := entry_index;
        comp_size     := entry_comp_size;
        uncomp_size   := entry_uncomp_size;
        crc_32        := entry_crc_32;
        raise Found;
      end if;
    end Check_entry;
    --
    procedure Search is new Traverse_verbose (Check_entry);
    --
  begin
    begin
      Search (info);
    exception
      when Found =>
        return;
    end;
    raise Entry_name_not_found with "Archive: [" & info.zip_file_name.all & "], entry: [" & name & ']';
  end Find_Offset_without_Directory;

  function Exists (info : Zip_info; name : String) return Boolean
  is
    aux : p_Dir_node := info.dir_binary_tree;
    up_name : constant String := Normalize (name, info.case_sensitive);
  begin
    if not info.loaded then
      raise Forgot_to_load_zip_info;
    end if;
    while aux /= null loop
      if up_name > aux.dico_name then
        aux := aux.right;
      elsif up_name < aux.dico_name then
        aux := aux.left;
      else  --  entry found !
        return True;
      end if;
    end loop;
    return False;
  end Exists;

  procedure Set_User_Code (info : Zip_info; name : String; code : Integer) is
    aux : p_Dir_node := info.dir_binary_tree;
    up_name : constant String := Normalize (name, info.case_sensitive);
  begin
    if not info.loaded then
      raise Forgot_to_load_zip_info;
    end if;
    while aux /= null loop
      if up_name > aux.dico_name then
        aux := aux.right;
      elsif up_name < aux.dico_name then
        aux := aux.left;
      else  --  entry found !
        aux.user_code := code;
        return;
      end if;
    end loop;
    raise Entry_name_not_found with "Archive: [" & info.zip_file_name.all & "], entry: [" & name & ']';
  end Set_User_Code;

  function User_Code (info : Zip_info; name : String) return Integer
  is
    aux : p_Dir_node := info.dir_binary_tree;
    up_name : constant String := Normalize (name, info.case_sensitive);
  begin
    if not info.loaded then
      raise Forgot_to_load_zip_info;
    end if;
    while aux /= null loop
      if up_name > aux.dico_name then
        aux := aux.right;
      elsif up_name < aux.dico_name then
        aux := aux.left;
      else  --  entry found !
        return aux.user_code;
      end if;
    end loop;
    raise Entry_name_not_found with "Archive: [" & info.zip_file_name.all & "], entry: [" & name & ']';
    return 0;  --  Fake, since exception has been raised just before. Removes an OA warning.
  end User_Code;

  procedure Get_Sizes
    (info           : in     Zip_info;
     name           : in     String;
     comp_size      :    out Zip_64_Data_Size_Type;
     uncomp_size    :    out Zip_64_Data_Size_Type)
  is
    dummy_file_index : Zip_Streams.ZS_Index_Type;
    dummy_name_encoding : Zip_Name_Encoding;
    dummy_crc_32 : Interfaces.Unsigned_32;
  begin
    Find_Offset
      (info, name, dummy_name_encoding, dummy_file_index,
       comp_size, uncomp_size, dummy_crc_32);
  end Get_Sizes;

  --  Workaround for the severe xxx'Read xxx'Write performance
  --  problems in the GNAT and ObjectAda compilers (as in 2009)
  --  This is possible if and only if Byte = Stream_Element and
  --  arrays types are both packed and aligned the same way.
  --
  subtype Size_test_a is Byte_Buffer (1 .. 19);
  subtype Size_test_b is Ada.Streams.Stream_Element_Array (1 .. 19);
  workaround_possible : constant Boolean :=
    Size_test_a'Size = Size_test_b'Size and
    Size_test_a'Alignment = Size_test_b'Alignment;

  --  Block_Read - general-purpose procedure (nothing really specific
  --  to Zip / UnZip): reads either the whole buffer from a file, or
  --  if the end of the file lays inbetween, a part of the buffer.

  procedure Block_Read
    (file          : in     Ada.Streams.Stream_IO.File_Type;
     buffer        :    out Byte_Buffer;
     actually_read :    out Natural)
  is
    use Ada.Streams, Ada.Streams.Stream_IO;
    SE_Buffer   : Stream_Element_Array (1 .. buffer'Length);
    for SE_Buffer'Address use buffer'Address;
    pragma Import (Ada, SE_Buffer);
    Last_Read   : Stream_Element_Offset;
  begin
    if workaround_possible then
      Read (Stream (file).all, SE_Buffer, Last_Read);
      actually_read := Natural (Last_Read);
    else
      if End_Of_File (file) then
        actually_read := 0;
      else
        actually_read :=
          Integer'Min (buffer'Length, Integer (Size (file) - Index (file) + 1));
        Byte_Buffer'Read (
          Stream (file),
          buffer (buffer'First .. buffer'First + actually_read - 1)
        );
      end if;
    end if;
  end Block_Read;

  procedure Block_Read
    (stream        : in out Zip_Streams.Root_Zipstream_Type'Class;
     buffer        :    out Byte_Buffer;
     actually_read :    out Natural)
  is
    use Ada.Streams;
    SE_Buffer   : Stream_Element_Array (1 .. buffer'Length);
    for SE_Buffer'Address use buffer'Address;
    pragma Import (Ada, SE_Buffer);
    Last_Read   : Stream_Element_Offset;
  begin
    if workaround_possible then
      stream.Read (SE_Buffer, Last_Read);
      actually_read := Natural (Last_Read);
    else
      if stream.End_Of_Stream then
        actually_read := 0;
      else
        actually_read :=
          Integer'Min (buffer'Length, Integer (stream.Size - stream.Index + 1));
        Byte_Buffer'Read (
          stream'Access,
          buffer (buffer'First .. buffer'First + actually_read - 1)
        );
      end if;
    end if;
  end Block_Read;

  procedure Block_Read
    (stream : in out Zip_Streams.Root_Zipstream_Type'Class;
     buffer :    out Byte_Buffer)
  is
    actually_read : Natural;
  begin
    Block_Read (stream, buffer, actually_read);
    if actually_read < buffer'Length then
      raise Ada.IO_Exceptions.End_Error;
    end if;
  end Block_Read;

  procedure Block_Write
    (stream : in out Ada.Streams.Root_Stream_Type'Class;
     buffer : in     Byte_Buffer)
  is
    use Ada.Streams;
    SE_Buffer   : Stream_Element_Array (1 .. buffer'Length);
    for SE_Buffer'Address use buffer'Address;
    pragma Import (Ada, SE_Buffer);
  begin
    if workaround_possible then
      Ada.Streams.Write (stream, SE_Buffer);
    else
      Byte_Buffer'Write (stream'Access, buffer);
      --  ^This is 30x to 70x slower on GNAT 2009 !
    end if;
  end Block_Write;

  function Image (m : PKZip_method) return String is
  begin
    case m is
      when store       => return "Store";
      when shrink      => return "Shrink";
      when reduce_1    => return "Reduce 1";
      when reduce_2    => return "Reduce 2";
      when reduce_3    => return "Reduce 3";
      when reduce_4    => return "Reduce 4";
      when implode     => return "Implode";
      when tokenize    => return "Tokenize";
      when deflate     => return "Deflate";
      when deflate_e   => return "Deflate64";
      when bzip2_meth       => return "BZip2";
      when lzma_meth   => return "LZMA";
      when zstandard   => return "Zstandard";
      when mp3_recomp  => return "MP3 recompression";
      when xz_recomp   => return "XZ recompression";
      when jpeg_recomp => return "JPEG recompression";
      when wavpack     => return "WAVE recompression";
      when ppmd        => return "PPMd";
      when unknown     => return "(unknown)";
    end case;
  end Image;

  function Method_from_Code (x : Natural) return PKZip_method is
    --  An enumeration clause might be more elegant instead of this function,
    --  but would need curiously an Unchecked_Conversion... (RM 13.4)
    use Compression_format_code;
  begin
    case x is
      when store_code      => return store;
      when shrink_code     => return shrink;
      when reduce_code     => return reduce_1;
      when reduce_code + 1 => return reduce_2;
      when reduce_code + 2 => return reduce_3;
      when reduce_code + 3 => return reduce_4;
      when implode_code    => return implode;
      when tokenize_code   => return tokenize;
      when deflate_code    => return deflate;
      when deflate_e_code  => return deflate_e;
      when bzip2_code      => return bzip2_meth;
      when lzma_code       => return lzma_meth;
      when zstandard_code  => return zstandard;
      when mp3_code        => return mp3_recomp;
      when xz_code         => return xz_recomp;
      when jpeg_code       => return jpeg_recomp;
      when wavpack_code    => return wavpack;
      when ppmd_code       => return ppmd;
      when others          => return unknown;
    end case;
  end Method_from_Code;

  function Method_from_Code (x : Interfaces.Unsigned_16) return PKZip_method is
  begin
    return Method_from_Code (Natural (x));
  end Method_from_Code;

  --  Copy a chunk from a stream into another one, using a temporary buffer
  procedure Copy_Chunk
    (from        : in out Zip_Streams.Root_Zipstream_Type'Class;
     into        : in out Ada.Streams.Root_Stream_Type'Class;
     bytes       : Natural;
     buffer_size : Positive := 1024 * 1024;
     Feedback    : Feedback_Proc := null)
  is
    buf : Zip.Byte_Buffer (1 .. buffer_size);
    actually_read, remains : Natural;
    user_abort : Boolean := False;
  begin
    remains := bytes;
    while remains > 0 loop
      if Feedback /= null then
        Feedback (
          100 - Integer (100.0 * Float (remains) / Float (bytes)),
          False,
          user_abort
        );
        --  !! do something if user_abort = True !!
      end if;
      Zip.Block_Read (from, buf (1 .. Integer'Min (remains, buf'Last)), actually_read);
      if actually_read = 0 then -- premature end, unexpected
        raise Zip.Archive_corrupted;
      end if;
      remains := remains - actually_read;
      Zip.Block_Write (into, buf (1 .. actually_read));
    end loop;
  end Copy_Chunk;

  --  Copy a whole file into a stream, using a temporary buffer
  procedure Copy_File
    (file_name   : String;
     into        : in out Ada.Streams.Root_Stream_Type'Class;
     buffer_size : Positive := 1024 * 1024)
  is
    use Ada.Streams.Stream_IO;
    f : File_Type;
    buf : Zip.Byte_Buffer (1 .. buffer_size);
    actually_read : Natural;
  begin
    Open (f, In_File, file_name);
    loop
      Zip.Block_Read (f, buf, actually_read);
      exit when actually_read = 0; -- this is expected
      Zip.Block_Write (into, buf (1 .. actually_read));
    end loop;
    Close (f);
  end Copy_File;

  --  This does the same as Ada 2005's Ada.Directories.Exists
  --  Just there as helper for Ada 95 only systems
  --
  function Exists (file_name : String) return Boolean is
    use Ada.Text_IO, Ada.Strings.Fixed;
    f : File_Type;
  begin
    if Index (file_name, "*") > 0 then
      return False;
    end if;
    Open (f, In_File, file_name, Form => Ada.Strings.Unbounded.To_String (Zip_Streams.Form_For_IO_Open_and_Create));
    Close (f);
    return True;
  exception
    when Name_Error =>
      return False;  --  The file cannot exist !
    when Use_Error =>
      return True;   --  The file exists and is already opened !
  end Exists;

  procedure Put_Multi_Line
    (out_file :        Ada.Text_IO.File_Type;
     text     :        String)
  is
    last_char : Character := ' ';
    c : Character;
  begin
    for i in text'Range loop
      c := text (i);
      case c is
        when ASCII.CR =>
          Ada.Text_IO.New_Line (out_file);
        when ASCII.LF =>
          if last_char /= ASCII.CR then Ada.Text_IO.New_Line (out_file); end if;
        when others =>
          Ada.Text_IO.Put (out_file, c);
      end case;
      last_char := c;
    end loop;
  end Put_Multi_Line;

  procedure Write_as_Text
    (out_file  :        Ada.Text_IO.File_Type;
     buffer    :        Byte_Buffer;
     last_char : in out Character)  --  track line-ending characters across writes
  is
    c : Character;
  begin
    for i in buffer'Range loop
      c := Character'Val (buffer (i));
      case c is
        when ASCII.CR =>
          Ada.Text_IO.New_Line (out_file);
        when ASCII.LF =>
          if last_char /= ASCII.CR then Ada.Text_IO.New_Line (out_file); end if;
        when others =>
          Ada.Text_IO.Put (out_file, c);
      end case;
      last_char := c;
    end loop;
  end Write_as_Text;

  function Hexadecimal (x : Interfaces.Unsigned_32) return String
  is
    package MIO is new Ada.Text_IO.Modular_IO (Interfaces.Unsigned_32);
    str : String (1 .. 12);
    use Ada.Strings.Fixed;
  begin
    MIO.Put (str, x, 16);
    return str (Index (str, "#") + 1 .. 11);
  end Hexadecimal;

  overriding procedure Adjust (info : in out Zip_info) is

    function Tree_Clone (p : in p_Dir_node) return p_Dir_node is
      q : p_Dir_node;
    begin
      if p = null then
        return null;
      else
        q := new Dir_node'(p.all);
        q.left  := Tree_Clone (p.left);
        q.right := Tree_Clone (p.right);
        return q;
      end if;
    end Tree_Clone;

  begin
    info.dir_binary_tree  := Tree_Clone (info.dir_binary_tree);
    info.zip_file_name    := new String'(info.zip_file_name.all);
    info.zip_file_comment := new String'(info.zip_file_comment.all);
  end Adjust;

  overriding procedure Finalize (info : in out Zip_info) is
  begin
    Delete (info);
  end Finalize;

end Zip;


Zip-Ada: Ada library for zip archive files (.zip). Ada programming.
Some news about Zip-Ada and other Ada projects on Gautier's blog.