------------------------------------------------------------------------------
--                                                                          --
--                         GNAT RUN-TIME COMPONENTS                         --
--                                                                          --
--                  ADA.DIRECTORIES.HIERARCHICAL_FILE_NAMES                 --
--                                                                          --
--                                 B o d y                                  --
--                                                                          --
--          Copyright (C) 2004-2025, 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 3,  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.                                     --
--                                                                          --
-- As a special exception under Section 7 of GPL version 3, you are granted --
-- additional permissions described in the GCC Runtime Library Exception,   --
-- version 3.1, as published by the Free Software Foundation.               --
--                                                                          --
-- In particular,  you can freely  distribute your programs  built with the --
-- GNAT Pro compiler, including any required library run-time units,  using --
-- any licensing terms  of your choosing.  See the AdaCore Software License --
-- for full details.                                                        --
--                                                                          --
-- GNAT was originally developed  by the GNAT team at  New York University. --
-- Extensive contributions were provided by Ada Core Technologies Inc.      --
--                                                                          --
------------------------------------------------------------------------------

with Ada.Characters.Handling;  use Ada.Characters.Handling;
with Ada.Directories.Validity; use Ada.Directories.Validity;
with Ada.Strings.Fixed;        use Ada.Strings.Fixed;
with System;                   use System;

package body Ada.Directories.Hierarchical_File_Names is

   Dir_Separator : constant Character;
   pragma Import (C, Dir_Separator, "__gnat_dir_separator");
   --  Running system default directory separator

   -----------------
   -- Subprograms --
   -----------------

   function Equivalent_File_Names
     (Left  : String;
      Right : String)
      return Boolean;
   --  Perform an OS-independent comparison between two file paths

   function Is_Absolute_Path (Name : String) return Boolean;
   --  Returns True if Name is an absolute path name, i.e. it designates a
   --  file or directory absolutely rather than relative to another directory.

   ---------------------------
   -- Equivalent_File_Names --
   ---------------------------

   function Equivalent_File_Names
     (Left  : String;
      Right : String)
      return Boolean
   is
   begin
      --  Check the validity of the input paths

      if not Is_Valid_Path_Name (Left)
        or else not Is_Valid_Path_Name (Right)
      then
         return False;
      end if;

      --  Normalize the paths by removing any trailing directory separators and
      --  perform the comparison.

      declare
         Normal_Left  : constant String :=
           (if Index (Left, Dir_Separator & "", Strings.Backward) = Left'Last
              and then not Is_Root_Directory_Name (Left)
            then
               Left (Left'First .. Left'Last - 1)
            else
               Left);

         Normal_Right : constant String :=
           (if Index (Right, Dir_Separator & "", Strings.Backward) = Right'Last
              and then not Is_Root_Directory_Name (Right)
            then
               Right (Right'First .. Right'Last - 1)
            else
               Right);
      begin
         --  Within Windows we assume case insensitivity

         if not Windows then
            return Normal_Left = Normal_Right;
         end if;

         --  Otherwise do a straight comparison

         return To_Lower (Normal_Left) = To_Lower (Normal_Right);
      end;
   end Equivalent_File_Names;

   ----------------------
   -- Is_Absolute_Path --
   ----------------------

   function Is_Absolute_Path (Name : String) return Boolean is
      function Is_Absolute_Path
        (Name   : Address;
         Length : Integer) return Integer;
      pragma Import (C, Is_Absolute_Path, "__gnat_is_absolute_path");
   begin
      return Is_Absolute_Path (Name'Address, Name'Length) /= 0;
   end Is_Absolute_Path;

   --------------------
   -- Is_Simple_Name --
   --------------------

   function Is_Simple_Name (Name : String) return Boolean is
   begin
      --  Verify the file path name is valid and that it is not a root

      if not Is_Valid_Path_Name (Name)
        or else Is_Root_Directory_Name (Name)
      then
         return False;
      end if;

      --  Check for the special paths "." and "..", which are considered simple

      if Is_Parent_Directory_Name (Name)
        or else Is_Current_Directory_Name (Name)
      then
         return True;
      end if;

      --  Perform a comparison with the calculated simple path name

      return Equivalent_File_Names (Simple_Name (Name), Name);
   end Is_Simple_Name;

   ----------------------------
   -- Is_Root_Directory_Name --
   ----------------------------

   function Is_Root_Directory_Name (Name : String) return Boolean is
   begin
      --  Check if the path name is a root directory by looking for a slash in
      --  the general case, and a drive letter in the case of Windows.

      return Name = "/"
               or else
                 (Windows
                   and then
                     (Name = "\"
                       or else
                         (Name'Length = 3
                           and then Name (Name'Last - 1) = ':'
                           and then Name (Name'Last) in '/' | '\'
                           and then (Name (Name'First) in 'a' .. 'z'
                                      or else
                                        Name (Name'First) in 'A' .. 'Z'))
                       or else
                         (Name'Length = 2
                           and then Name (Name'Last) = ':'
                           and then (Name (Name'First) in 'a' .. 'z'
                                      or else
                                        Name (Name'First) in 'A' .. 'Z'))));
   end Is_Root_Directory_Name;

   ------------------------------
   -- Is_Parent_Directory_Name --
   ------------------------------

   function Is_Parent_Directory_Name (Name : String) return Boolean is
   begin
      return Name = "..";
   end Is_Parent_Directory_Name;

   -------------------------------
   -- Is_Current_Directory_Name --
   -------------------------------

   function Is_Current_Directory_Name (Name : String) return Boolean is
   begin
      return Name = ".";
   end Is_Current_Directory_Name;

   ------------------
   -- Is_Full_Name --
   ------------------

   function Is_Full_Name (Name : String) return Boolean is
   begin
      return Equivalent_File_Names (Full_Name (Name), Name);
   end Is_Full_Name;

   ----------------------
   -- Is_Relative_Name --
   ----------------------

   function Is_Relative_Name (Name : String) return Boolean is
   begin
      return not Is_Absolute_Path (Name)
               and then Is_Valid_Path_Name (Name);
   end Is_Relative_Name;

   -----------------------
   -- Initial_Directory --
   -----------------------

   function Initial_Directory (Name : String) return String is
      Start : constant Integer := Index (Name, Dir_Separator & "");
   begin
      --  Verify path name

      if not Is_Valid_Path_Name (Name) then
         raise Name_Error with "invalid path name """ & Name & '"';
      end if;

      --  When there is no starting directory separator or the path name is a
      --  root directory then the path name is already simple - so return it.

      if Is_Root_Directory_Name (Name) or else Start = 0 then
         return Name;
      end if;

      --  When the initial directory of the path name is a root directory then
      --  the starting directory separator is part of the result so we must
      --  return it in the slice.

      if Is_Root_Directory_Name (Name (Name'First .. Start)) then
         return Name (Name'First .. Start);
      end if;

      --  Otherwise we grab a slice up to the starting directory separator

      return Name (Name'First .. Start - 1);
   end Initial_Directory;

   -------------------
   -- Relative_Name --
   -------------------

   function Relative_Name (Name : String) return String is
   begin
      --  We cannot derive a relative name if Name does not exist

      if not Is_Relative_Name (Name)
        and then not Is_Valid_Path_Name (Name)
      then
         raise Name_Error with "invalid relative path name """ & Name & '"';
      end if;

      --  Name only has a single part and thus cannot be made relative

      if Is_Simple_Name (Name)
        or else Is_Root_Directory_Name (Name)
      then
         raise Name_Error with
           "relative path name """ & Name & """ is composed of a single part";
      end if;

      --  Trim the input according to the initial directory and maintain proper
      --  directory separation due to the fact that root directories may
      --  contain separators.

      declare
         Init_Dir : constant String := Initial_Directory (Name);
      begin
         if Init_Dir (Init_Dir'Last) = Dir_Separator then
            return Name (Name'First + Init_Dir'Length .. Name'Last);
         end if;

         return Name (Name'First + Init_Dir'Length + 1 .. Name'Last);
      end;
   end Relative_Name;

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

   function Compose
     (Directory     : String := "";
      Relative_Name : String;
      Extension     : String := "") return String
   is
      --  Append a directory separator if none is present

      Separated_Dir : constant String :=
        (if    Directory = "" then ""
         elsif Directory (Directory'Last) = Dir_Separator then Directory
         else  Directory & Dir_Separator);
   begin
      --  Check that relative name is valid

      if not Is_Relative_Name (Relative_Name) then
         raise Name_Error with
           "invalid relative path name """ & Relative_Name & '"';
      end if;

      --  Check that directory is valid

      if Separated_Dir /= ""
        and then not Is_Valid_Path_Name (Separated_Dir & Relative_Name)
      then
         raise Name_Error with
           "invalid path composition """ & Separated_Dir & Relative_Name & '"';
      end if;

      --  Check that the extension is valid

      if Extension /= ""
        and then not Is_Valid_Path_Name
                       (Separated_Dir & Relative_Name & Extension)
      then
         raise Name_Error with
           "invalid path composition """
             & Separated_Dir & Relative_Name & Extension & '"';
      end if;

      --  Concatenate the result

      return Separated_Dir & Relative_Name & Extension;
   end Compose;

end Ada.Directories.Hierarchical_File_Names;
