2023-01-11 07:51:40 +00:00
# Functions for working with paths, see ./path.md
{ lib }:
let
inherit ( builtins )
isString
2023-02-09 11:40:11 +00:00
isPath
2023-01-11 07:51:40 +00:00
split
match
2023-07-15 17:15:38 +00:00
typeOf
2023-01-11 07:51:40 +00:00
;
inherit ( lib . lists )
length
head
last
genList
elemAt
2023-03-15 16:39:30 +00:00
all
concatMap
foldl'
2023-07-15 17:15:38 +00:00
take
2023-08-04 22:07:22 +00:00
drop
2023-01-11 07:51:40 +00:00
;
inherit ( lib . strings )
concatStringsSep
substring
;
inherit ( lib . asserts )
assertMsg
;
2023-02-09 11:40:11 +00:00
inherit ( lib . path . subpath )
isValid
;
2023-01-11 07:51:40 +00:00
# Return the reason why a subpath is invalid, or `null` if it's valid
subpathInvalidReason = value :
if ! isString value then
" T h e g i v e n v a l u e i s o f t y p e ${ builtins . typeOf value } , b u t a s t r i n g w a s e x p e c t e d "
else if value == " " then
" T h e g i v e n s t r i n g i s e m p t y "
else if substring 0 1 value == " / " then
" T h e g i v e n s t r i n g \" ${ value } \" s t a r t s w i t h a ` / ` , r e p r e s e n t i n g a n a b s o l u t e p a t h "
# We don't support ".." components, see ./path.md#parent-directory
else if match " ( . * / ) ? \\ . \\ . ( / . * ) ? " value != null then
" T h e g i v e n s t r i n g \" ${ value } \" c o n t a i n s a ` . . ` c o m p o n e n t , w h i c h i s n o t a l l o w e d i n s u b p a t h s "
else null ;
# Split and normalise a relative path string into its components.
# Error for ".." components and doesn't include "." components
splitRelPath = path :
let
# Split the string into its parts using regex for efficiency. This regex
# matches patterns like "/", "/./", "/././", with arbitrarily many "/"s
# together. These are the main special cases:
# - Leading "./" gets split into a leading "." part
# - Trailing "/." or "/" get split into a trailing "." or ""
# part respectively
#
# These are the only cases where "." and "" parts can occur
parts = split " / + ( \\ . / + ) * " path ;
# `split` creates a list of 2 * k + 1 elements, containing the k +
# 1 parts, interleaved with k matches where k is the number of
# (non-overlapping) matches. This calculation here gets the number of parts
# back from the list length
# floor( (2 * k + 1) / 2 ) + 1 == floor( k + 1/2 ) + 1 == k + 1
partCount = length parts / 2 + 1 ;
# To assemble the final list of components we want to:
# - Skip a potential leading ".", normalising "./foo" to "foo"
# - Skip a potential trailing "." or "", normalising "foo/" and "foo/." to
# "foo". See ./path.md#trailing-slashes
skipStart = if head parts == " . " then 1 else 0 ;
skipEnd = if last parts == " . " || last parts == " " then 1 else 0 ;
# We can now know the length of the result by removing the number of
# skipped parts from the total number
componentCount = partCount - skipEnd - skipStart ;
in
# Special case of a single "." path component. Such a case leaves a
# componentCount of -1 due to the skipStart/skipEnd not verifying that
# they don't refer to the same character
if path == " . " then [ ]
# Generate the result list directly. This is more efficient than a
# combination of `filter`, `init` and `tail`, because here we don't
# allocate any intermediate lists
else genList ( index :
# To get to the element we need to add the number of parts we skip and
# multiply by two due to the interleaved layout of `parts`
elemAt parts ( ( skipStart + index ) * 2 )
) componentCount ;
# Join relative path components together
joinRelPath = components :
# Always return relative paths with `./` as a prefix (./path.md#leading-dots-for-relative-paths)
" . / " +
# An empty string is not a valid relative path, so we need to return a `.` when we have no components
( if components == [ ] then " . " else concatStringsSep " / " components ) ;
2023-07-15 17:15:38 +00:00
# Type: Path -> { root :: Path, components :: [ String ] }
#
# Deconstruct a path value type into:
# - root: The filesystem root of the path, generally `/`
# - components: All the path's components
#
# This is similar to `splitString "/" (toString path)` but safer
# because it can distinguish different filesystem roots
deconstructPath =
let
recurse = components : base :
# If the parent of a path is the path itself, then it's a filesystem root
if base == dirOf base then { root = base ; inherit components ; }
else recurse ( [ ( baseNameOf base ) ] ++ components ) ( dirOf base ) ;
in recurse [ ] ;
2023-01-11 07:51:40 +00:00
in /* N o r e c ! A d d d e p e n d e n c i e s o n t h i s f i l e a t t h e t o p . */ {
2023-02-09 11:40:11 +00:00
/* A p p e n d a s u b p a t h s t r i n g t o a p a t h .
Like ` path + ( " / " + string ) ` but safer , because it errors instead of returning potentially surprising results .
More specifically , it checks that the first argument is a [ path value type ] ( https://nixos.org/manual/nix/stable/language/values.html #type-path"),
and that the second argument is a valid subpath string ( see ` lib . path . subpath . isValid ` ) .
2023-07-15 17:15:38 +00:00
Laws :
- Not influenced by subpath normalisation
append p s == append p ( subpath . normalise s )
2023-02-09 11:40:11 +00:00
Type :
append : : Path -> String -> Path
Example :
append /foo " b a r / b a z "
= > /foo/bar/baz
# subpaths don't need to be normalised
append /foo " . / b a r / / b a z / . / "
= > /foo/bar/baz
# can append to root directory
append /. " f o o / b a r "
= > /foo/bar
# first argument needs to be a path value type
append " / f o o " " b a r "
= > <error>
# second argument needs to be a valid subpath string
append /foo /bar
= > <error>
append /foo " "
= > <error>
append /foo " / b a r "
= > <error>
append /foo " . . / b a r "
= > <error>
* /
append =
# The absolute path to append to
path :
# The subpath string to append
subpath :
assert assertMsg ( isPath path ) ''
lib . path . append : The first argument is of type $ { builtins . typeOf path } , but a path was expected'' ;
assert assertMsg ( isValid subpath ) ''
lib . path . append : Second argument is not a valid subpath string :
$ { subpathInvalidReason subpath } '' ;
path + ( " / " + subpath ) ;
2023-01-11 07:51:40 +00:00
2023-07-15 17:15:38 +00:00
/*
Whether the first path is a component-wise prefix of the second path .
Laws :
- ` hasPrefix p q ` is only true if ` q == append p s ` for some subpath ` s ` .
- ` hasPrefix ` is a [ non-strict partial order ] ( https://en.wikipedia.org/wiki/Partially_ordered_set #Non-strict_partial_order) over the set of all path values
Type :
hasPrefix : : Path -> Path -> Bool
Example :
hasPrefix /foo /foo/bar
= > true
hasPrefix /foo /foo
= > true
hasPrefix /foo/bar /foo
= > false
hasPrefix /. /foo
= > true
* /
hasPrefix =
path1 :
assert assertMsg
( isPath path1 )
" l i b . p a t h . h a s P r e f i x : F i r s t a r g u m e n t i s o f t y p e ${ typeOf path1 } , b u t a p a t h w a s e x p e c t e d " ;
let
path1Deconstructed = deconstructPath path1 ;
in
path2 :
assert assertMsg
( isPath path2 )
" l i b . p a t h . h a s P r e f i x : S e c o n d a r g u m e n t i s o f t y p e ${ typeOf path2 } , b u t a p a t h w a s e x p e c t e d " ;
let
path2Deconstructed = deconstructPath path2 ;
in
assert assertMsg
( path1Deconstructed . root == path2Deconstructed . root ) ''
lib . path . hasPrefix : Filesystem roots must be the same for both paths , but paths with different roots were given :
first argument : " ${ toString path1 } " with root " ${ toString path1Deconstructed . root } "
second argument : " ${ toString path2 } " with root " ${ toString path2Deconstructed . root } " '' ;
take ( length path1Deconstructed . components ) path2Deconstructed . components == path1Deconstructed . components ;
2023-08-04 22:07:22 +00:00
/*
Remove the first path as a component-wise prefix from the second path .
The result is a normalised subpath string , see ` lib . path . subpath . normalise ` .
Laws :
- Inverts ` append ` for normalised subpaths :
removePrefix p ( append p s ) == subpath . normalise s
Type :
removePrefix : : Path -> Path -> String
Example :
removePrefix /foo /foo/bar/baz
= > " . / b a r / b a z "
removePrefix /foo /foo
= > " . / . "
removePrefix /foo/bar /foo
= > <error>
removePrefix /. /foo
= > " . / f o o "
* /
removePrefix =
path1 :
assert assertMsg
( isPath path1 )
" l i b . p a t h . r e m o v e P r e f i x : F i r s t a r g u m e n t i s o f t y p e ${ typeOf path1 } , b u t a p a t h w a s e x p e c t e d . " ;
let
path1Deconstructed = deconstructPath path1 ;
path1Length = length path1Deconstructed . components ;
in
path2 :
assert assertMsg
( isPath path2 )
" l i b . p a t h . r e m o v e P r e f i x : S e c o n d a r g u m e n t i s o f t y p e ${ typeOf path2 } , b u t a p a t h w a s e x p e c t e d . " ;
let
path2Deconstructed = deconstructPath path2 ;
success = take path1Length path2Deconstructed . components == path1Deconstructed . components ;
components =
if success then
drop path1Length path2Deconstructed . components
else
throw ''
lib . path . removePrefix : The first path argument " ${ toString path1 } " is not a component-wise prefix of the second path argument " ${ toString path2 } " . '' ;
in
assert assertMsg
( path1Deconstructed . root == path2Deconstructed . root ) ''
lib . path . removePrefix : Filesystem roots must be the same for both paths , but paths with different roots were given :
first argument : " ${ toString path1 } " with root " ${ toString path1Deconstructed . root } "
second argument : " ${ toString path2 } " with root " ${ toString path2Deconstructed . root } " '' ;
joinRelPath components ;
/*
Split the filesystem root from a [ path ] ( https://nixos.org/manual/nix/stable/language/values.html #type-path).
The result is an attribute set with these attributes :
- ` root ` : The filesystem root of the path , meaning that this directory has no parent directory .
- ` subpath ` : The [ normalised subpath string ] ( #function-library-lib.path.subpath.normalise) that when [appended](#function-library-lib.path.append) to `root` returns the original path.
Laws :
- [ Appending ] ( #function-library-lib.path.append) the `root` and `subpath` gives the original path:
p ==
append
( splitRoot p ) . root
( splitRoot p ) . subpath
- Trying to get the parent directory of ` root ` using [ ` readDir ` ] ( https://nixos.org/manual/nix/stable/language/builtins.html #builtins-readDir) returns `root` itself:
dirOf ( splitRoot p ) . root == ( splitRoot p ) . root
Type :
splitRoot : : Path -> { root : : Path , subpath : : String }
Example :
splitRoot /foo/bar
= > { root = /. ; subpath = " . / f o o / b a r " ; }
splitRoot /.
= > { root = /. ; subpath = " . / . " ; }
# Nix neutralises `..` path components for all path values automatically
splitRoot /foo/../bar
= > { root = /. ; subpath = " . / b a r " ; }
splitRoot " / f o o / b a r "
= > <error>
* /
splitRoot = path :
assert assertMsg
( isPath path )
" l i b . p a t h . s p l i t R o o t : A r g u m e n t i s o f t y p e ${ typeOf path } , b u t a p a t h w a s e x p e c t e d " ;
let
deconstructed = deconstructPath path ;
in {
root = deconstructed . root ;
subpath = joinRelPath deconstructed . components ;
} ;
2023-07-15 17:15:38 +00:00
2023-01-11 07:51:40 +00:00
/* W h e t h e r a v a l u e i s a v a l i d s u b p a t h s t r i n g .
2023-08-04 22:07:22 +00:00
A subpath string points to a specific file or directory within an absolute base directory .
It is a stricter form of a relative path that excludes ` . . ` components , since those could escape the base directory .
2023-01-11 07:51:40 +00:00
- The value is a string
- The string is not empty
- The string doesn't start with a ` / `
- The string doesn't contain any ` . . ` path components
Type :
subpath . isValid : : String -> Bool
Example :
# Not a string
subpath . isValid null
= > false
# Empty string
subpath . isValid " "
= > false
# Absolute path
subpath . isValid " / f o o "
= > false
# Contains a `..` path component
subpath . isValid " . . / f o o "
= > false
# Valid subpath
subpath . isValid " f o o / b a r "
= > true
# Doesn't need to be normalised
subpath . isValid " . / f o o / / b a r / "
= > true
* /
2023-02-09 11:40:11 +00:00
subpath . isValid =
# The value to check
value :
2023-01-11 07:51:40 +00:00
subpathInvalidReason value == null ;
2023-03-15 16:39:30 +00:00
/* J o i n s u b p a t h s t r i n g s t o g e t h e r u s i n g ` / ` , r e t u r n i n g a n o r m a l i s e d s u b p a t h s t r i n g .
Like ` concatStringsSep " / " ` but safer , specifically :
- All elements must be valid subpath strings , see ` lib . path . subpath . isValid `
- The result gets normalised , see ` lib . path . subpath . normalise `
- The edge case of an empty list gets properly handled by returning the neutral subpath ` " . / . " `
Laws :
- Associativity :
subpath . join [ x ( subpath . join [ y z ] ) ] == subpath . join [ ( subpath . join [ x y ] ) z ]
- Identity - ` " . / . " ` is the neutral element for normalised paths :
subpath . join [ ] == " . / . "
subpath . join [ ( subpath . normalise p ) " . / . " ] == subpath . normalise p
subpath . join [ " . / . " ( subpath . normalise p ) ] == subpath . normalise p
- Normalisation - the result is normalised according to ` lib . path . subpath . normalise ` :
subpath . join ps == subpath . normalise ( subpath . join ps )
- For non-empty lists , the implementation is equivalent to normalising the result of ` concatStringsSep " / " ` .
Note that the above laws can be derived from this one .
ps != [ ] -> subpath . join ps == subpath . normalise ( concatStringsSep " / " ps )
Type :
subpath . join : : [ String ] -> String
Example :
subpath . join [ " f o o " " b a r / b a z " ]
= > " . / f o o / b a r / b a z "
# normalise the result
subpath . join [ " . / f o o " " . " " b a r / / . / b a z / " ]
= > " . / f o o / b a r / b a z "
# passing an empty list results in the current directory
subpath . join [ ]
= > " . / . "
# elements must be valid subpath strings
subpath . join [ /foo ]
= > <error>
subpath . join [ " " ]
= > <error>
subpath . join [ " / f o o " ]
= > <error>
subpath . join [ " . . / f o o " ]
= > <error>
* /
subpath . join =
# The list of subpaths to join together
subpaths :
# Fast in case all paths are valid
if all isValid subpaths
then joinRelPath ( concatMap splitRelPath subpaths )
else
# Otherwise we take our time to gather more info for a better error message
# Strictly go through each path, throwing on the first invalid one
# Tracks the list index in the fold accumulator
foldl' ( i : path :
if isValid path
then i + 1
else throw ''
lib . path . subpath . join : Element at index $ { toString i } is not a valid subpath string :
$ { subpathInvalidReason path } ''
) 0 subpaths ;
2023-08-04 22:07:22 +00:00
/*
Split [ a subpath ] ( #function-library-lib.path.subpath.isValid) into its path component strings.
Throw an error if the subpath isn't valid .
Note that the returned path components are also valid subpath strings , though they are intentionally not [ normalised ] ( #function-library-lib.path.subpath.normalise).
Laws :
- Splitting a subpath into components and [ joining ] ( #function-library-lib.path.subpath.join) the components gives the same subpath but [normalised](#function-library-lib.path.subpath.normalise):
subpath . join ( subpath . components s ) == subpath . normalise s
Type :
subpath . components : : String -> [ String ]
Example :
subpath . components " . "
= > [ ]
subpath . components " . / f o o / / b a r / . / b a z / "
= > [ " f o o " " b a r " " b a z " ]
subpath . components " / f o o "
= > <error>
* /
subpath . components =
subpath :
assert assertMsg ( isValid subpath ) ''
lib . path . subpath . components : Argument is not a valid subpath string :
$ { subpathInvalidReason subpath } '' ;
splitRelPath subpath ;
2023-01-11 07:51:40 +00:00
/* N o r m a l i s e a s u b p a t h . T h r o w a n e r r o r i f t h e s u b p a t h i s n ' t v a l i d , s e e
` lib . path . subpath . isValid `
- Limit repeating ` / ` to a single one
- Remove redundant ` . ` components
- Remove trailing ` / ` and ` /. `
- Add leading ` . / `
Laws :
2023-02-09 11:40:11 +00:00
- Idempotency - normalising multiple times gives the same result :
2023-01-11 07:51:40 +00:00
subpath . normalise ( subpath . normalise p ) == subpath . normalise p
2023-02-09 11:40:11 +00:00
- Uniqueness - there's only a single normalisation for the paths that lead to the same file system node :
2023-01-11 07:51:40 +00:00
subpath . normalise p != subpath . normalise q -> $ ( realpath $ { p } ) != $ ( realpath $ { q } )
- Don't change the result when appended to a Nix path value :
base + ( " / " + p ) == base + ( " / " + subpath . normalise p )
- Don't change the path according to ` realpath ` :
$ ( realpath $ { p } ) == $ ( realpath $ { subpath . normalise p } )
- Only error on invalid subpaths :
builtins . tryEval ( subpath . normalise p ) ) . success == subpath . isValid p
Type :
subpath . normalise : : String -> String
Example :
# limit repeating `/` to a single one
subpath . normalise " f o o / / b a r "
= > " . / f o o / b a r "
# remove redundant `.` components
subpath . normalise " f o o / . / b a r "
= > " . / f o o / b a r "
# add leading `./`
subpath . normalise " f o o / b a r "
= > " . / f o o / b a r "
# remove trailing `/`
subpath . normalise " f o o / b a r / "
= > " . / f o o / b a r "
# remove trailing `/.`
subpath . normalise " f o o / b a r / . "
= > " . / f o o / b a r "
# Return the current directory as `./.`
subpath . normalise " . "
= > " . / . "
# error on `..` path components
subpath . normalise " f o o / . . / b a r "
= > <error>
# error on empty string
subpath . normalise " "
= > <error>
# error on absolute path
subpath . normalise " / f o o "
= > <error>
* /
2023-02-09 11:40:11 +00:00
subpath . normalise =
# The subpath string to normalise
subpath :
assert assertMsg ( isValid subpath ) ''
lib . path . subpath . normalise : Argument is not a valid subpath string :
$ { subpathInvalidReason subpath } '' ;
joinRelPath ( splitRelPath subpath ) ;
2023-01-11 07:51:40 +00:00
}