Fuzion Logo
fuzion-lang.dev — The Fuzion Language Portal
JavaScript seems to be disabled. Functionality is limited.

path.fz


# This file is part of the Fuzion language implementation.
#
# The Fuzion language implementation is free software: you can redistribute it
# and/or modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation, version 3 of the License.
#
# The Fuzion language implementation is distributed in the hope that it will be
# useful, but WITHOUT 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 along with The
# Fuzion language implementation.  If not, see <https://www.gnu.org/licenses/>.


# -----------------------------------------------------------------------
#
#  Tokiwa Software GmbH, Germany
#
#  Source code of Fuzion standard library feature path
#
# -----------------------------------------------------------------------


# NYI: CLEANUP: where to move this os/io?


# a feature denoting a path/file in the filesystem
#
# instantiate via path.of, e.g.
#
#     path.of "/some/path"
# or
#     path.of ["some", "path"] true
#
private:public path(public names array String, public is_absolute bool) : property.orderable, property.hashable
  pre debug: !names.contains "."
      # allow ".." only at start in relative paths
      safety: is_absolute ? !names.contains ".." : !(names.drop_while ="..").contains ".."
is

  # is this a relative path
  #
  public is_relative bool => !is_absolute

  # is this the root
  #
  public fixed is_root    bool => path.this = path.root

  # is this a relative path representing "current directory"
  #
  public fixed is_current bool => path.this = path.current_dir

  # is this a relative path representing "parent directory"
  #
  public fixed is_parent  bool => path.this = path.parent_dir

  # is this a path with just one segment, i.e. not containing a path separator when normalized
  #
  public is_single_segment bool => is_relative && names.count = 1

  # the number of segments in this path
  #
  public segments_count i32 => names.count

  # does this path end with other
  #
  public fixed ends_with(other path) bool =>
    (other.segments_count < segments_count && other.is_relative && names.reverse.starts_with other.names.reverse) ||
      path.this = other

  # does this path contain other
  #
  public contains(other path) bool =>
    other.is_relative && (names.find other.names ? i32 => true | * => false) || starts_with other


  # resolve o in this path
  #
  # e.g.
  #
  #     path.of "/tmp/folder" .resolve "file"
  #
  # effectively returns
  #
  #     path.of "/tmp/folder/file"
  #
  # resolving an absolute path returns that path unmodified
  #
  public resolve(o path|String) path =>
    p := o ? p1 path => p1 | s String => path.of s

    p.is_absolute ? p : path.of names++p.names is_absolute


  # get subpath starting from (inclusive)
  #
  # e.g.
  #     path.of "/tmp/folder/file" .subpath 1 = path.of "folder/file"
  #
  public subpath(from i32) path =>
    subpath from segments_count


  # get subpath from (inclusive) to (exclusive)
  #
  # e.g.
  #     path.of "/tmp/folder/file" .subpath 0 2 = path.of "/tmp/folder"
  #
  public subpath(from, to i32) path =>
    sp := names
      .slice from to
    path.of sp (is_absolute && from=0)


  # Full file name or name of the last directory
  #
  public base_name String
    pre debug: !names.is_empty
  =>
    names.last.get

  # consider this to represent a file
  # and return just name of the file
  # without its extension and path.
  #
  public stem String =>
    match base_name.find_last "."
      i i32 => 0 < i < base_name.byte_length-1 ? base_name.substring 0 i : base_name
      nil   => base_name

  # consider this to represent a file
  # and return the suffix of the file_name
  #
  public suffix String =>
    match base_name.find_last "."
      i i32 => 0 < i < base_name.byte_length-1 ? base_name.substring i+1 : ""
      nil   => ""


  # get the parent of this path
  #
  # if folder is root folder or .
  # nil is returned
  #
  public parent option path => parent 1

  # go up n levels, i.e. call parent n times
  #
  public parent(n i32) option path
    pre debug: n >= 0
  =>
    if names.is_empty && is_absolute && n>0
      nil
    else
      path.of names++[".."]*n is_absolute


  # relativize path `p` based on this path.
  #
  # e.g. "/tmp/folder".relative "/tmp/folder/file" will return "file"
  #
  public relativize(p path) path
    pre debug: p.starts_with (path.of names is_absolute)
  =>
    path.of (p.names.drop names.count) false


  # checks if this path starts with path `p`
  #
  # both this and path p must either be absolute or relative
  # for this to return true.
  #
  # e.g. (path.of "/tmp/file").starts_with (path.of "/tmp") = true
  #
  public starts_with(p path) bool =>
    is_absolute = p.is_absolute &&
      p.names.count <= names.count &&
      (names.take p.names.count) = p.names


  # String representation of this path
  #
  # e.g. "/folder/file" or "relative_folder/file"
  #
  public redef as_string String =>
    "{if is_absolute then "/" else ""}{if names.is_empty && !is_absolute then "./" else String.join names "/"}"


  # define order for paths
  #
  public redef fixed type.lteq(a, b path) bool =>
    if a.is_absolute != b.is_absolute
      b.is_absolute
    else
      a.names <= b.names


  # create hash code for this instance
  #
  # This should satisfy the following condition:
  #
  #   (T.equality a b) : (T.hash_code a = T.hash_code b)
  #
  public redef fixed type.hash_code(a path) u64 =>
    xxh_next
      (hash a.is_absolute)
      ((array String).type.hash_code a.names)


  # instantiates a path via a given String
  # if paths starts with / it is considered to be
  # absolute rather than relative.
  #
  public type.of(str String) path
    pre debug: !str.starts_with "/../"
        debug: str != "/.."
  =>
    t := str.trim
    path.of (t.split "/") (t.starts_with "/")


  # the path separator that is used
  # on the current platform
  #
  public type.separator codepoint =>
    # NYI: UNDER DEVELOPMENT: cache this globally
    (cache separator_cache ()->
      separator_cache (os.platform.get ? os.posix => "/" | os.windows => "\\")).val


  # instantiates a path
  #
  # e.g.
  #
  #     path.of ["tmp", "some_file"] true
  #
  public type.of(seq Sequence String, is_absolute bool) path
    pre debug: seq ∀ s->
          !(s.contains "/" || s.contains "\0")
        debug: !is_absolute || (seq.first "") != ".."
  =>
    dotdot, names := seq
      .reverse
      .filter !="."
      .filter !=""
      .reduce (0, Sequence String .empty) (r,t)->
        if t = ".."
          # we have double dots, ignore them and increase counter
          (r.0+1, r.1)
        else if r.0 > 0
          # we previously encountered double dots.
          # ignore this element too and decrease the counter.
          (r.0-1, r.1)
        else
          # no double dots encountered previously,
          # we need to include this element in the final path
          (r.0, [t]++r.1)
    path ([".."]*dotdot ++ names).as_array is_absolute


  # the root path "/"
  #
  public type.root path => path [] true

  # the current directory path "."
  #
  public type.current_dir path => path [] false

  # the parent directory path ".."
  #
  public type.parent_dir path => path.of ".."


# used in path.separator
#
separator_cache(val codepoint) is

last changed: 2026-02-23