Mastering absolute/relative file path conversions in Pascal in a cross-platform way

In this post, I'll discuss how to handle file paths in Pascal in a cross-platform manner. I have newly written the following functions, because I often encountered problems with the Delphi and FreePascal own appropriate functions in terms of cross-platform difficulties. So I reinvented the wheel out of necessity, but for the cross-platform special case, where each build variant must also support the each other path convention. Specifically, I'll focus on converting between relative and absolute paths, expanding relative paths, including handling UNC paths and so on. I'll also provide an in-depth analysis of each function, detailing the logic and edge cases they handle. So, let's get started.

IsPathSeparator

The IsPathSeparator function checks if a character is a path separator. It considers '/' and '\' as path separators. This fundamental function forms the basis for parsing the paths in the following functions, and is crucial for ensuring cross-platform compatibility. It's implemented as a function rather than a check like aChar in ['/','\'] due to some code generator bugs in recent trunk versions of FreePascal when handling sets in conditions.


function IsPathSeparator(const aChar:AnsiChar):Boolean; inline;
begin
 case aChar of
  '/','\':begin
   result:=true;
  end;
  else begin
   result:=false;
  end;
 end;
end;

ExpandRelativePath

The ExpandRelativePath function converts a relative path to an absolute path, accounting for different path styles used across various platforms. It accepts a relative path and an optional base path. The function begins by checking if the relative path is already an absolute path. It recognizes both Unix-style and Windows-style absolute paths, including UNC paths. It then appends the relative path to the base path. The function also handles edge cases like multiple consecutive path separators and trailing separators.


function ExpandRelativePath(const aRelativePath:RawByteString;const aBasePath:RawByteString=''):RawByteString;
var InputIndex,OutputIndex:Int32;
    InputPath:RawByteString;
    PathSeparator:AnsiChar;
begin
 if (length(aRelativePath)>0) and
    (IsPathSeparator(aRelativePath[1]) or
     ((length(aRelativePath)>1) and
      (aRelativePath[1] in ['a'..'z','A'..'Z']) and
      (aRelativePath[2]=':'))) then begin
  InputPath:=aRelativePath;
 end else begin
  if (length(aBasePath)>1) and not IsPathSeparator(aBasePath[length(aBasePath)]) then begin
   PathSeparator:=#0;
   for InputIndex:=1 to length(aBasePath) do begin
    if IsPathSeparator(aBasePath[InputIndex]) then begin
     PathSeparator:=aBasePath[InputIndex];
     break;
    end;
   end;
   if PathSeparator=#0 then begin
    for InputIndex:=1 to length(aRelativePath) do begin
     if IsPathSeparator(aRelativePath[InputIndex]) then begin
      PathSeparator:=aRelativePath[InputIndex];
      break;
     end;
    end;
    if PathSeparator=#0 then begin
     PathSeparator:='/';
    end;
   end;
   InputPath:=aBasePath+PathSeparator;
  end else begin
   InputPath:=aBasePath;
  end;
  InputPath:=InputPath+aRelativePath;
 end;
 result:=InputPath;
 InputIndex:=1;
 OutputIndex:=1;
 while InputIndex<=length(InputPath) do begin
  if (((InputIndex+1)<=length(InputPath)) and (InputPath[InputIndex]='.') and IsPathSeparator(InputPath[InputIndex+1])) or
     ((InputIndex=length(InputPath)) and (InputPath[InputIndex]='.')) then begin
   inc(InputIndex,2);
   if OutputIndex=1 then begin
    inc(OutputIndex,2);
   end;
  end else if (((InputIndex+1)<=length(InputPath)) and (InputPath[InputIndex]='.') and (InputPath[InputIndex+1]='.')) and
              ((((InputIndex+2)<=length(InputPath)) and IsPathSeparator(InputPath[InputIndex+2])) or
               ((InputIndex+1)=length(InputPath))) then begin
   inc(InputIndex,3);
   if OutputIndex=1 then begin
    inc(OutputIndex,3);
   end else if OutputIndex>1 then begin
    dec(OutputIndex,2);
    while (OutputIndex>1) and not IsPathSeparator(result[OutputIndex]) do begin
     dec(OutputIndex);
    end;
    inc(OutputIndex);
   end;
  end else if IsPathSeparator(InputPath[InputIndex]) then begin
   if (InputIndex=1) and
      ((InputIndex+1)<=length(InputPath)) and
      IsPathSeparator(InputPath[InputIndex+1]) and
      ((length(InputPath)=2) or
       (((InputIndex+2)<=Length(InputPath)) and not IsPathSeparator(InputPath[InputIndex+2]))) then begin
    result[OutputIndex]:=InputPath[InputIndex];
    result[OutputIndex+1]:=InputPath[InputIndex+1];
    inc(InputIndex,2);
    inc(OutputIndex,2);
   end else begin
    if (OutputIndex=1) or ((OutputIndex>1) and not IsPathSeparator(result[OutputIndex-1])) then begin
     result[OutputIndex]:=InputPath[InputIndex];
     inc(OutputIndex);
    end;
    inc(InputIndex);
   end;
  end else begin
   while (InputIndex<=length(InputPath)) and not IsPathSeparator(InputPath[InputIndex]) do begin
    result[OutputIndex]:=InputPath[InputIndex];
    inc(InputIndex);
    inc(OutputIndex);
   end;
   if InputIndex<=length(InputPath) then begin
    result[OutputIndex]:=InputPath[InputIndex];
    inc(InputIndex);
    inc(OutputIndex);
   end;
  end;
 end;
 SetLength(result,OutputIndex-1);
end;

ConvertPathToRelative

The ConvertPathToRelative function converts an absolute path (including UNC paths) to a relative path with respect to a base path. This function is more complex as it needs to find the common prefix of the absolute path and the base path, and then compute the relative path. The function handles edge cases like trailing separators in the base path and different path separators in the input paths, ensuring cross-platform compatibility.


function ConvertPathToRelative(aAbsolutePath,aBasePath:RawByteString):RawByteString;
var AbsolutePathIndex,BasePathIndex:Int32;
    PathSeparator:AnsiChar;
begin
 if length(aBasePath)=0 then begin
  result:=aAbsolutePath;
 end else begin
  aAbsolutePath:=ExpandRelativePath(aAbsolutePath);
  aBasePath:=ExpandRelativePath(aBasePath);
  PathSeparator:=#0;
  for BasePathIndex:=1 to length(aBasePath) do begin
   if IsPathSeparator(aBasePath[BasePathIndex]) then begin
    PathSeparator:=aBasePath[BasePathIndex];
    break;
   end;
  end;
  if PathSeparator=#0 then begin
   for AbsolutePathIndex:=1 to length(aAbsolutePath) do begin
    if IsPathSeparator(aAbsolutePath[AbsolutePathIndex]) then begin
     PathSeparator:=aAbsolutePath[AbsolutePathIndex];
     break;
    end;
   end;
   if PathSeparator=#0 then begin
    PathSeparator:='/';
   end;
  end;
  if length(aBasePath)>1 then begin
   if IsPathSeparator(aBasePath[length(aBasePath)]) then begin
    if (length(aAbsolutePath)>1) and IsPathSeparator(aAbsolutePath[length(aAbsolutePath)]) then begin
     if (aAbsolutePath=aBasePath) and (aAbsolutePath[1]<>'.') and (aBasePath[1]<>'.') then begin
      result:='.'+PathSeparator;
      exit;
     end;
    end;
   end else begin
    aBasePath:=aBasePath+PathSeparator;
   end;
  end;
  AbsolutePathIndex:=1;
  BasePathIndex:=1;
  while (BasePathIndex<=Length(aBasePath)) and
        (AbsolutePathIndex<=Length(aAbsolutePath)) and
        ((aBasePath[BasePathIndex]=aAbsolutePath[AbsolutePathIndex]) or
         (IsPathSeparator(aBasePath[BasePathIndex]) and IsPathSeparator(aAbsolutePath[AbsolutePathIndex]))) do begin
   inc(AbsolutePathIndex);
   inc(BasePathIndex);
  end;
  if ((BasePathIndex<=length(aBasePath)) and not IsPathSeparator(aBasePath[BasePathIndex])) or
     ((AbsolutePathIndex<=length(aAbsolutePath)) and not IsPathSeparator(aAbsolutePath[AbsolutePathIndex])) then begin
   while (BasePathIndex>1) and not IsPathSeparator(aBasePath[BasePathIndex-1]) do begin
    dec(AbsolutePathIndex);
    dec(BasePathIndex);
   end;
  end;
  if BasePathIndex<=Length(aBasePath) then begin
   result:='';
   while BasePathIndex<=Length(aBasePath) do begin
    if IsPathSeparator(aBasePath[BasePathIndex]) then begin
     result:=result+'..'+PathSeparator;
    end;
    inc(BasePathIndex);
   end;
  end else begin
   result:='.'+PathSeparator;
  end;
  if AbsolutePathIndex<=length(aAbsolutePath) then begin
   result:=result+copy(aAbsolutePath,AbsolutePathIndex,(length(aAbsolutePath)-AbsolutePathIndex)+1);
  end;
 end;
end;

Analysis

Let's delve deeper into the analysis of each function:

The IsPathSeparator function is straightforward. It checks whether a given character is a path separator, either '/' or '\'. This utility function plays a crucial role in other functions where parsing of the paths is needed.

The ExpandRelativePath function handles a variety of scenarios:

  • It can process relative paths, absolute paths, and UNC paths, which is crucial for cross-platform compatibility.
  • It handles both Unix-style ('/') and Windows-style ('\') path separators, supporting different file systems.
  • It can deal with path components like . (current directory) and .. (parent directory).
  • It correctly adds a path separator if the base path doesn't have a trailing separator.
  • It handles case sensitivity properly. For simplicity and cross-platform compatibility, case sensitivity is ignored as well as the fact that under *nix '\' can be a valid part of a filename.

The ConvertPathToRelative function also handles various edge cases:

  • It starts by expanding the absolute path and the base path, ensuring they are in a consistent format.
  • Then, it finds the common prefix of the two paths. If the characters at the current position are not equal, it moves back to the last path separator.
  • It constructs the relative path by appending '..' for each remaining segment in the base path and the remaining part of the absolute path.
  • It handles case sensitivity properly. For simplicity and cross-platform compatibility, case sensitivity is ignored as well as the fact that under *nix '\' can be a valid part of a filename.

Overall, these functions are robust and handle a variety of common and edge cases. They can handle both Unix-style and Windows-style paths, including UNC paths. They do not interact with the file system, i.e., they don't check whether the paths exist, nor do they handle file permissions or other file system-specific issues. Their focus is purely on manipulating path strings in a cross-platform manner.

That's a wrap on cross-platform file paths in Pascal. I hope this post illuminated new insights for you. Happy coding!