------------------------------------------------------------------------------
--                                                                          --
--                         GNAT RUN-TIME COMPONENTS                         --
--                                                                          --
--                      A D A . D I R E C T O R I E S                       --
--                                                                          --
--                                 B o d y                                  --
--                                                                          --
--          Copyright (C) 2004-2005, Free Software Foundation, Inc.         --
--                                                                          --
-- GNAT is free software;  you can  redistribute it  and/or modify it under --
-- terms of the  GNU General Public License as published  by the Free Soft- --
-- ware  Foundation;  either version 2,  or (at your option) any later ver- --
-- sion.  GNAT is distributed in the hope that it will be useful, but WITH- --
-- OUT ANY WARRANTY;  without even the  implied warranty of MERCHANTABILITY --
-- or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License --
-- for  more details.  You should have  received  a copy of the GNU General --
-- Public License  distributed with GNAT;  see file COPYING.  If not, write --
-- to  the  Free Software Foundation,  51  Franklin  Street,  Fifth  Floor, --
-- Boston, MA 02110-1301, USA.                                              --
--                                                                          --
-- As a special exception,  if other files  instantiate  generics from this --
-- unit, or you link  this unit with other files  to produce an executable, --
-- this  unit  does not  by itself cause  the resulting  executable  to  be --
-- covered  by the  GNU  General  Public  License.  This exception does not --
-- however invalidate  any other reasons why  the executable file  might be --
-- covered by the  GNU Public License.                                      --
--                                                                          --
-- GNAT was originally developed  by the GNAT team at  New York University. --
-- Extensive contributions were provided by Ada Core Technologies Inc.      --
--                                                                          --
------------------------------------------------------------------------------

with Ada.Directories.Validity;   use Ada.Directories.Validity;
with Ada.Strings.Unbounded;      use Ada.Strings.Unbounded;
with Ada.Unchecked_Deallocation;
with Ada.Unchecked_Conversion;
with Ada.Characters.Handling;    use Ada.Characters.Handling;

with GNAT.Directory_Operations;  use GNAT.Directory_Operations;
with GNAT.OS_Lib;                use GNAT.OS_Lib;
with GNAT.Regexp;                use GNAT.Regexp;
--  ??? Ada units should not depend on GNAT units

with System;

package body Ada.Directories is

   function Duration_To_Time is new
     Ada.Unchecked_Conversion (Duration, Ada.Calendar.Time);
   function OS_Time_To_Long_Integer is new
     Ada.Unchecked_Conversion (OS_Time, Long_Integer);
   --  These two unchecked conversions are used in function Modification_Time
   --  to convert an OS_Time to a Calendar.Time.

   type Search_Data is record
      Is_Valid      : Boolean := False;
      Name          : Ada.Strings.Unbounded.Unbounded_String;
      Pattern       : Regexp;
      Filter        : Filter_Type;
      Dir           : Dir_Type;
      Entry_Fetched : Boolean := False;
      Dir_Entry     : Directory_Entry_Type;
   end record;
   --  The current state of a search

   Empty_String : constant String := (1 .. 0 => ASCII.NUL);
   --  Empty string, returned by function Extension when there is no extension

   procedure Free is new Ada.Unchecked_Deallocation (Search_Data, Search_Ptr);

   function File_Exists (Name : String) return Boolean;
   --  Returns True if the named file exists

   procedure Fetch_Next_Entry (Search : Search_Type);
   --  Get the next entry in a directory, setting Entry_Fetched if successful
   --  or resetting Is_Valid if not.

   procedure To_Lower_If_Case_Insensitive (S : in out String);
   --  Put S in lower case if file and path names are case-insensitive

   ---------------
   -- Base_Name --
   ---------------

   function Base_Name (Name : String) return String is
      Simple : String := Simple_Name (Name);
      --  Simple'First is guaranteed to be 1

   begin
      To_Lower_If_Case_Insensitive (Simple);

      --  Look for the last dot in the file name and return the part of the
      --  file name preceding this last dot. If the first dot is the first
      --  character of the file name, the base name is the empty string.

      for Pos in reverse Simple'Range loop
         if Simple (Pos) = '.' then
            return Simple (1 .. Pos - 1);
         end if;
      end loop;

      --  If there is no dot, return the complete file name

      return Simple;
   end Base_Name;

   -------------
   -- Compose --
   -------------

   function Compose
     (Containing_Directory : String := "";
      Name                 : String;
      Extension            : String := "") return String
   is
      Result : String (1 .. Containing_Directory'Length +
                              Name'Length + Extension'Length + 2);
      Last   : Natural;

   begin
      --  First, deal with the invalid cases

      if not Is_Valid_Path_Name (Containing_Directory) then
         raise Name_Error;

      elsif
        Extension'Length = 0 and then (not Is_Valid_Simple_Name (Name))
      then
         raise Name_Error;

      elsif Extension'Length /= 0 and then
        (not Is_Valid_Simple_Name (Name & '.' & Extension))
      then
         raise Name_Error;

         --  This is not an invalid case so build the path name

      else
         Last := Containing_Directory'Length;
         Result (1 .. Last) := Containing_Directory;

         --  Add a directory separator if needed

         if Result (Last) /= Dir_Separator then
            Last := Last + 1;
            Result (Last) := Dir_Separator;
         end if;

         --  Add the file name

         Result (Last + 1 .. Last + Name'Length) := Name;
         Last := Last + Name'Length;

         --  If extension was specified, add dot followed by this extension

         if Extension'Length /= 0 then
            Last := Last + 1;
            Result (Last) := '.';
            Result (Last + 1 .. Last + Extension'Length) := Extension;
            Last := Last + Extension'Length;
         end if;

         To_Lower_If_Case_Insensitive (Result (1 .. Last));
         return Result (1 .. Last);
      end if;
   end Compose;

   --------------------------
   -- Containing_Directory --
   --------------------------

   function Containing_Directory (Name : String) return String is
   begin
      --  First, the invalid case

      if not Is_Valid_Path_Name (Name) then
         raise Name_Error;

      else
         --  Get the directory name using GNAT.Directory_Operations.Dir_Name

         declare
            Value : constant String := Dir_Name (Path => Name);
            Result : String (1 .. Value'Length);
            Last : Natural := Result'Last;

         begin
            Result := Value;

            --  Remove any trailing directory separator, except as the first
            --  character.

            while Last > 1 and then Result (Last) = Dir_Separator loop
               Last := Last - 1;
            end loop;

            --  Special case of current directory, identified by "."

            if Last = 1 and then Result (1) = '.' then
               return Get_Current_Dir;

            else
               To_Lower_If_Case_Insensitive (Result (1 .. Last));
               return Result (1 .. Last);
            end if;
         end;
      end if;
   end Containing_Directory;

   ---------------
   -- Copy_File --
   ---------------

   procedure Copy_File
     (Source_Name   : String;
      Target_Name   : String;
      Form          : String := "")
   is
      pragma Unreferenced (Form);
      Success : Boolean;

   begin
      --  First, the invalid cases

      if not Is_Valid_Path_Name (Source_Name)
        or else not Is_Valid_Path_Name (Target_Name)
        or else not Is_Regular_File (Source_Name)
      then
         raise Name_Error;

      elsif Is_Directory (Target_Name) then
         raise Use_Error;

      else
         --  The implementation uses GNAT.OS_Lib.Copy_File, with parameters
         --  suitable for all platforms.

         Copy_File
           (Source_Name, Target_Name, Success, Overwrite, None);

         if not Success then
            raise Use_Error;
         end if;
      end if;
   end Copy_File;

   ----------------------
   -- Create_Directory --
   ----------------------

   procedure Create_Directory
     (New_Directory : String;
      Form          : String := "")
   is
      pragma Unreferenced (Form);

   begin
      --  First, the invalid case

      if not Is_Valid_Path_Name (New_Directory) then
         raise Name_Error;

      else
         --  The implementation uses GNAT.Directory_Operations.Make_Dir

         begin
            Make_Dir (Dir_Name => New_Directory);

         exception
            when Directory_Error =>
               raise Use_Error;
         end;
      end if;
   end Create_Directory;

   -----------------
   -- Create_Path --
   -----------------

   procedure Create_Path
     (New_Directory : String;
      Form          : String := "")
   is
      pragma Unreferenced (Form);

      New_Dir : String (1 .. New_Directory'Length + 1);
      Last    : Positive := 1;

   begin
      --  First, the invalid case

      if not Is_Valid_Path_Name (New_Directory) then
         raise Name_Error;

      else
         --  Build New_Dir with a directory separator at the end, so that the
         --  complete path will be found in the loop below.

         New_Dir (1 .. New_Directory'Length) := New_Directory;
         New_Dir (New_Dir'Last) := Directory_Separator;

         --  Create, if necessary, each directory in the path

         for J in 2 .. New_Dir'Last loop

            --  Look for the end of an intermediate directory

            if New_Dir (J) /= Dir_Separator then
               Last := J;

            --  We have found a new intermediate directory each time we find
            --  a first directory separator.

            elsif New_Dir (J - 1) /= Dir_Separator then

               --  No need to create the directory if it already exists

               if Is_Directory (New_Dir (1 .. Last)) then
                  null;

               --  It is an error if a file with such a name already exists

               elsif Is_Regular_File (New_Dir (1 .. Last)) then
                  raise Use_Error;

               else
                  --  The implementation uses
                  --  GNAT.Directory_Operations.Make_Dir.

                  begin
                     Make_Dir (Dir_Name => New_Dir (1 .. Last));

                  exception
                     when Directory_Error =>
                        raise Use_Error;
                  end;
               end if;
            end if;
         end loop;
      end if;
   end Create_Path;

   -----------------------
   -- Current_Directory --
   -----------------------

   function Current_Directory return String is

      --  The implementation uses GNAT.Directory_Operations.Get_Current_Dir

      Cur : String := Normalize_Pathname (Get_Current_Dir);

   begin
      To_Lower_If_Case_Insensitive (Cur);

      if Cur'Length > 1 and then Cur (Cur'Last) = Dir_Separator then
         return Cur (1 .. Cur'Last - 1);
      else
         return Cur;
      end if;
   end Current_Directory;

   ----------------------
   -- Delete_Directory --
   ----------------------

   procedure Delete_Directory (Directory : String) is
   begin
      --  First, the invalid cases

      if not Is_Valid_Path_Name (Directory) then
         raise Name_Error;

      elsif not Is_Directory (Directory) then
         raise Name_Error;

      else
         --  The implementation uses GNAT.Directory_Operations.Remove_Dir

         begin
            Remove_Dir (Dir_Name => Directory, Recursive => False);

         exception
            when Directory_Error =>
               raise Use_Error;
         end;
      end if;
   end Delete_Directory;

   -----------------
   -- Delete_File --
   -----------------

   procedure Delete_File (Name : String) is
      Success : Boolean;

   begin
      --  First, the invalid cases

      if not Is_Valid_Path_Name (Name) then
         raise Name_Error;

      elsif not Is_Regular_File (Name) then
         raise Name_Error;

      else
         --  The implementation uses GNAT.OS_Lib.Delete_File

         Delete_File (Name, Success);

         if not Success then
            raise Use_Error;
         end if;
      end if;
   end Delete_File;

   -----------------
   -- Delete_Tree --
   -----------------

   procedure Delete_Tree (Directory : String) is
   begin
      --  First, the invalid cases

      if not Is_Valid_Path_Name (Directory) then
         raise Name_Error;

      elsif not Is_Directory (Directory) then
         raise Name_Error;

      else
         --  The implementation uses GNAT.Directory_Operations.Remove_Dir

         begin
            Remove_Dir (Directory, Recursive => True);

         exception
            when Directory_Error =>
               raise Use_Error;
         end;
      end if;
   end Delete_Tree;

   ------------
   -- Exists --
   ------------

   function Exists (Name : String) return Boolean is
   begin
      --  First, the invalid case

      if not Is_Valid_Path_Name (Name) then
         raise Name_Error;

      else
         --  The implementation is in File_Exists

         return File_Exists (Name);
      end if;
   end Exists;

   ---------------
   -- Extension --
   ---------------

   function Extension (Name : String) return String is
   begin
      --  First, the invalid case

      if not Is_Valid_Path_Name (Name) then
         raise Name_Error;

      else
         --  Look for first dot that is not followed by a directory separator

         for Pos in reverse Name'Range loop

            --  If a directory separator is found before a dot, there
            --  is no extension.

            if Name (Pos) = Dir_Separator then
               return Empty_String;

            elsif Name (Pos) = '.' then

               --  We found a dot, build the return value with lower bound 1

               declare
                  Result : String (1 .. Name'Last - Pos);
               begin
                  Result := Name (Pos + 1 .. Name'Last);
                  return Result;
                  --  This should be done with a subtype conversion, avoiding
                  --  the unnecessary junk copy ???
               end;
            end if;
         end loop;

         --  No dot were found, there is no extension

         return Empty_String;
      end if;
   end Extension;

   ----------------------
   -- Fetch_Next_Entry --
   ----------------------

   procedure Fetch_Next_Entry (Search : Search_Type) is
      Name : String (1 .. 255);
      Last : Natural;

      Kind : File_Kind := Ordinary_File;
      --  Initialized to avoid a compilation warning

   begin
      --  Search.Value.Is_Valid is always True when Fetch_Next_Entry is called

      loop
         Read (Search.Value.Dir, Name, Last);

         --  If no matching entry is found, set Is_Valid to False

         if Last = 0 then
            Search.Value.Is_Valid := False;
            exit;
         end if;

         --  Check if the entry matches the pattern

         if Match (Name (1 .. Last), Search.Value.Pattern) then
            declare
               Full_Name : constant String :=
                             Compose
                               (To_String
                                  (Search.Value.Name), Name (1 .. Last));
               Found     : Boolean := False;

            begin
               if File_Exists (Full_Name) then

                  --  Now check if the file kind matches the filter

                  if Is_Regular_File (Full_Name) then
                     if Search.Value.Filter (Ordinary_File) then
                        Kind := Ordinary_File;
                        Found := True;
                     end if;

                  elsif Is_Directory (Full_Name) then
                     if Search.Value.Filter (Directory) then
                        Kind := Directory;
                        Found := True;
                     end if;

                  elsif Search.Value.Filter (Special_File) then
                     Kind := Special_File;
                     Found := True;
                  end if;

                  --  If it does, update Search and return

                  if Found then
                     Search.Value.Entry_Fetched := True;
                     Search.Value.Dir_Entry :=
                       (Is_Valid => True,
                        Simple   => To_Unbounded_String (Name (1 .. Last)),
                        Full     => To_Unbounded_String (Full_Name),
                        Kind     => Kind);
                     exit;
                  end if;
               end if;
            end;
         end if;
      end loop;
   end Fetch_Next_Entry;

   -----------------
   -- File_Exists --
   -----------------

   function File_Exists (Name : String) return Boolean is
      function C_File_Exists (A : System.Address) return Integer;
      pragma Import (C, C_File_Exists, "__gnat_file_exists");

      C_Name : String (1 .. Name'Length + 1);

   begin
      C_Name (1 .. Name'Length) := Name;
      C_Name (C_Name'Last) := ASCII.NUL;
      return C_File_Exists (C_Name (1)'Address) = 1;
   end File_Exists;

   --------------
   -- Finalize --
   --------------

   procedure Finalize (Search : in out Search_Type) is
   begin
      if Search.Value /= null then

         --  Close the directory, if one is open

         if Is_Open (Search.Value.Dir) then
            Close (Search.Value.Dir);
         end if;

         Free (Search.Value);
      end if;
   end Finalize;

   ---------------
   -- Full_Name --
   ---------------

   function Full_Name (Name : String) return String is
   begin
      --  First, the invalid case

      if not Is_Valid_Path_Name (Name) then
         raise Name_Error;

      else
         --  Build the return value with lower bound 1

         --  Use GNAT.OS_Lib.Normalize_Pathname

         declare
            Value : String := Normalize_Pathname (Name);
            subtype Result is String (1 .. Value'Length);
         begin
            To_Lower_If_Case_Insensitive (Value);
            return Result (Value);
         end;
      end if;
   end Full_Name;

   function Full_Name (Directory_Entry : Directory_Entry_Type) return String is
   begin
      --  First, the invalid case

      if not Directory_Entry.Is_Valid then
         raise Status_Error;

      else
         --  The value to return has already been computed

         return To_String (Directory_Entry.Full);
      end if;
   end Full_Name;

   --------------------
   -- Get_Next_Entry --
   --------------------

   procedure Get_Next_Entry
     (Search          : in out Search_Type;
      Directory_Entry : out Directory_Entry_Type)
   is
   begin
      --  First, the invalid case

      if Search.Value = null or else not Search.Value.Is_Valid then
         raise Status_Error;
      end if;

      --  Fetch the next entry, if needed

      if not Search.Value.Entry_Fetched then
         Fetch_Next_Entry (Search);
      end if;

      --  It is an error if no valid entry is found

      if not Search.Value.Is_Valid then
         raise Status_Error;

      else
         --  Reset Entry_Fatched and return the entry

         Search.Value.Entry_Fetched := False;
         Directory_Entry := Search.Value.Dir_Entry;
      end if;
   end Get_Next_Entry;

   ----------
   -- Kind --
   ----------

   function Kind (Name : String) return File_Kind is
   begin
      --  First, the invalid case

      if not File_Exists (Name) then
         raise Name_Error;

      elsif Is_Regular_File (Name) then
         return Ordinary_File;

      elsif Is_Directory (Name) then
         return Directory;

      else
         return Special_File;
      end if;
   end Kind;

   function Kind (Directory_Entry : Directory_Entry_Type) return File_Kind is
   begin
      --  First, the invalid case

      if not Directory_Entry.Is_Valid then
         raise Status_Error;

      else
         --  The value to return has already be computed

         return Directory_Entry.Kind;
      end if;
   end Kind;

   -----------------------
   -- Modification_Time --
   -----------------------

   function Modification_Time (Name : String) return Ada.Calendar.Time is
      Date   : OS_Time;
      Year   : Year_Type;
      Month  : Month_Type;
      Day    : Day_Type;
      Hour   : Hour_Type;
      Minute : Minute_Type;
      Second : Second_Type;

      Result : Ada.Calendar.Time;

   begin
      --  First, the invalid cases

      if not (Is_Regular_File (Name) or else Is_Directory (Name)) then
         raise Name_Error;

      else
         Date := File_Time_Stamp (Name);

         --  ??? This implementation should be revisited when AI 00351 has
         --  implemented.

         if OpenVMS then

            --  On OpenVMS, OS_Time is in local time

            GM_Split (Date, Year, Month, Day, Hour, Minute, Second);

            return Ada.Calendar.Time_Of
              (Year, Month, Day,
               Duration (Second + 60 * (Minute + 60 * Hour)));

         else
            --  On Unix and Windows, OS_Time is in GMT

            Result :=
              Duration_To_Time (Duration (OS_Time_To_Long_Integer (Date)));
            return Result;
         end if;
      end if;
   end Modification_Time;

   function Modification_Time
     (Directory_Entry : Directory_Entry_Type) return Ada.Calendar.Time
   is
   begin
      --  First, the invalid case

      if not Directory_Entry.Is_Valid then
         raise Status_Error;

      else
         --  The value to return has already be computed

         return Modification_Time (To_String (Directory_Entry.Full));
      end if;
   end Modification_Time;

   ------------------
   -- More_Entries --
   ------------------

   function More_Entries (Search : Search_Type) return Boolean is
   begin
      if Search.Value = null then
         return False;

      elsif Search.Value.Is_Valid then

         --  Fetch the next entry, if needed

         if not Search.Value.Entry_Fetched then
            Fetch_Next_Entry (Search);
         end if;
      end if;

      return Search.Value.Is_Valid;
   end More_Entries;

   ------------
   -- Rename --
   ------------

   procedure Rename (Old_Name, New_Name : String) is
      Success : Boolean;

   begin
      --  First, the invalid cases

      if not Is_Valid_Path_Name (Old_Name)
        or else not Is_Valid_Path_Name (New_Name)
        or else (not Is_Regular_File (Old_Name)
                   and then not Is_Directory (Old_Name))
      then
         raise Name_Error;

      elsif Is_Regular_File (New_Name) or Is_Directory (New_Name) then
         raise Use_Error;

      else
         --  The implementation uses GNAT.OS_Lib.Rename_File

         Rename_File (Old_Name, New_Name, Success);

         if not Success then
            raise Use_Error;
         end if;
      end if;
   end Rename;

   -------------------
   -- Set_Directory --
   -------------------

   procedure Set_Directory (Directory : String) is
   begin
      --  The implementation uses GNAT.Directory_Operations.Change_Dir

      Change_Dir (Dir_Name => Directory);

   exception
      when Directory_Error =>
         raise Name_Error;
   end Set_Directory;

   -----------------
   -- Simple_Name --
   -----------------

   function Simple_Name (Name : String) return String is
   begin
      --  First, the invalid case

      if not Is_Valid_Path_Name (Name) then
         raise Name_Error;

      else
         --  Build the value to return with lower bound 1

         --  The implementation uses GNAT.Directory_Operations.Base_Name

         declare
            Value  : String := GNAT.Directory_Operations.Base_Name (Name);
            subtype Result is String (1 .. Value'Length);
         begin
            To_Lower_If_Case_Insensitive (Value);
            return Result (Value);
         end;
      end if;
   end Simple_Name;

   function Simple_Name
     (Directory_Entry : Directory_Entry_Type) return String
   is
   begin
      --  First, the invalid case

      if not Directory_Entry.Is_Valid then
         raise Status_Error;

      else
         --  The value to return has already be computed

         return To_String (Directory_Entry.Simple);
      end if;
   end Simple_Name;

   ----------
   -- Size --
   ----------

   function Size (Name : String) return File_Size is
      C_Name : String (1 .. Name'Length + 1);

      function C_Size (Name : System.Address) return Long_Integer;
      pragma Import (C, C_Size, "__gnat_named_file_length");

   begin
      --  First, the invalid case

      if not Is_Regular_File (Name) then
         raise Name_Error;

      else
         C_Name (1 .. Name'Length) := Name;
         C_Name (C_Name'Last) := ASCII.NUL;
         return File_Size (C_Size (C_Name'Address));
      end if;
   end Size;

   function Size (Directory_Entry : Directory_Entry_Type) return File_Size is
   begin
      --  First, the invalid case

      if not Directory_Entry.Is_Valid then
         raise Status_Error;

      else
         --  The value to return has already be computed

         return Size (To_String (Directory_Entry.Full));
      end if;
   end Size;

   ------------------
   -- Start_Search --
   ------------------

   procedure Start_Search
     (Search    : in out Search_Type;
      Directory : String;
      Pattern   : String;
      Filter    : Filter_Type := (others => True))
   is
   begin
      --  First, the invalid case

      if not Is_Directory (Directory) then
         raise Name_Error;
      end if;

      --  If needed, finalize Search

      Finalize (Search);

      --  Allocate the default data

      Search.Value := new Search_Data;

      begin
         --  Check the pattern

         Search.Value.Pattern := Compile (Pattern, Glob => True);

      exception
         when Error_In_Regexp =>
            Free (Search.Value);
            raise Name_Error;
      end;

      --  Initialize some Search components

      Search.Value.Filter := Filter;
      Search.Value.Name := To_Unbounded_String (Full_Name (Directory));
      Open (Search.Value.Dir, Directory);
      Search.Value.Is_Valid := True;
   end Start_Search;

   ----------------------------------
   -- To_Lower_If_Case_Insensitive --
   ----------------------------------

   procedure To_Lower_If_Case_Insensitive (S : in out String) is
   begin
      if not Is_Path_Name_Case_Sensitive then
         for J in S'Range loop
            S (J) := To_Lower (S (J));
         end loop;
      end if;
   end To_Lower_If_Case_Insensitive;

end Ada.Directories;
