path-handling.scm 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. (define-module (path-handling)
  2. #:export (absolute-path
  3. absolute-path?
  4. path-join
  5. path-split
  6. file-extension
  7. subpath?
  8. complex-path?
  9. get-current-directory))
  10. (use-modules
  11. ;; for fold, last
  12. (srfi srfi-1)
  13. (ice-9 exceptions)
  14. ;; custom modules
  15. (string-utils)
  16. (list-utils)
  17. ((logging) #:prefix log:)
  18. (file-system))
  19. (define path-join
  20. (λ (path1 . other-path-parts)
  21. "Join paths using the system preferred separator."
  22. (let ([dir-sep (car (string->list file-name-separator-string))])
  23. (fold
  24. (λ (current-elem accumulated)
  25. (cond
  26. ;; If a later path is an absolute path, then it is
  27. ;; used as the new accumulated value. Basically a
  28. ;; later absolute path overrides the already
  29. ;; accumulated path, because it cannot be joined in
  30. ;; a useful way.
  31. [(absolute-path? current-elem) current-elem]
  32. ;; We know, that the current-elem is not an absolute
  33. ;; path and so it can be usefully joined with the
  34. ;; already accumulated path.
  35. [else
  36. ;; If the first element is the empty string, then
  37. ;; we should make an absolute path. We know the
  38. ;; first element by looking at what is already
  39. ;; accumulated. If the accumulated path is also
  40. ;; still empty, then we are at the beginning of
  41. ;; path parts.
  42. (cond
  43. ;; Are we at the beginning?
  44. [(string-null? accumulated)
  45. (cond
  46. ;; Is the first element the empty string? Then
  47. ;; make an absolute path.
  48. ;; NOTE: WARNING: This is not OS independent!
  49. ;; Absolute paths do not have to start with
  50. ;; the directory separator on all OS.
  51. [(string-null? current-elem) (char->string dir-sep)]
  52. ;; Otherwise use the first element as
  53. ;; accumulated path and go on with the rest.
  54. [else current-elem])]
  55. ;; If we are not at the beginning, then the path
  56. ;; cannot become absolute any longer.
  57. [else
  58. (string-append
  59. ;; Remove any trailing separators to make sure
  60. ;; there is only one separator, when the paths
  61. ;; are concattenated.
  62. (string-trim-right accumulated
  63. (λ (char)
  64. (char=? char dir-sep)))
  65. ;; Concat the paths with the separator in the
  66. ;; middle.
  67. (char->string dir-sep)
  68. ;; We already know current-elem is not an
  69. ;; absolute path.
  70. current-elem)])]))
  71. ""
  72. (cons path1 other-path-parts)))))
  73. (define path-split
  74. (λ (path)
  75. "Split a path by the preferred separator of the system."
  76. (string-split path (string->char file-name-separator-string))))
  77. (define absolute-path
  78. (lambda* (path
  79. ;; We give the working directory as a keyword
  80. ;; argument, so that this procedure does not
  81. ;; need to make the decision on its own and the
  82. ;; resulting absolute paths for non-absolute
  83. ;; paths do not necessarily depend on where
  84. ;; exactly this module is located in the file
  85. ;; system.
  86. #:key
  87. (working-directory (get-current-directory)))
  88. "Return the absolute path of a given absolute or non-absolute path."
  89. (cond
  90. ;; If the path is already an absolute path, simply
  91. ;; return that.
  92. [(absolute-path? path) path]
  93. [else
  94. ;; In case the path is not absolute already, we look
  95. ;; for it in the current directory.
  96. (let next-parent ([path-parts
  97. (path-split
  98. (path-join working-directory path))])
  99. (cond
  100. ;; WARNING: This part is not OS independent. An
  101. ;; absolute path does not have to start with the
  102. ;; separator string in all OS.
  103. [(null? path-parts) file-name-separator-string]
  104. [else
  105. (let* ([path-str (apply path-join path-parts)]
  106. [canon-abs-path (false-if-exception (canonicalize-path path-str))])
  107. (cond
  108. [(not canon-abs-path)
  109. (apply path-join
  110. (list (next-parent (drop-right path-parts 1))
  111. (last path-parts)))]
  112. [else
  113. canon-abs-path]))]))])))
  114. (define absolute-path?
  115. (λ (path)
  116. "Check, whether the given path is an absolute path."
  117. ;; Guile already offers a function for this, but it is a
  118. ;; little bit strangely named, as it can be used for
  119. ;; files and directories, not only for files. We only
  120. ;; give it an alias.
  121. (absolute-file-name? path)))
  122. (define file-extension
  123. (λ (path)
  124. "Get the file extension of the given path or #f if there
  125. is no file extension."
  126. (cond
  127. ;; An empty string is given, there can be no file
  128. ;; extension.
  129. [(string-null? path) #f]
  130. [else
  131. (let ([path-last-part (basename path)]
  132. [file-extension-separator #\.])
  133. (let ([last-part-split (string-split path-last-part file-extension-separator)])
  134. (cond
  135. ;; If the split did not produce more than one
  136. ;; part, then the split character was not found
  137. ;; and so the path does not have a file
  138. ;; extension.
  139. [(= (length last-part-split) 1) #f]
  140. [else
  141. (let ([perhaps-file-extension (last last-part-split)])
  142. ;; A file name could end with a "." and that
  143. ;; would produce an empty string as file
  144. ;; extension. This procedure does not consider
  145. ;; the empty string to be a file extension.
  146. (if (string-null? perhaps-file-extension)
  147. #f
  148. perhaps-file-extension))])))])))
  149. (define subpath?
  150. (λ (path parent-path)
  151. "Check, whether a path is a sub path of a given parent
  152. path."
  153. (cond
  154. ;; We want to avoid complicated paths for now and
  155. ;; simply claim, that upwards navigating paths are not
  156. ;; in any parent path for security reasons.
  157. [(complex-path? path) #f]
  158. [else
  159. (let ([path-parts (path-split path)]
  160. [parent-path-parts (path-split parent-path)])
  161. (list-prefix? (path-split path)
  162. (path-split parent-path)))])))
  163. (define complex-path?
  164. (λ (path)
  165. "Check, whether the given path contains anything, which
  166. could be used to navigate upwards in the file system tree or
  167. is in any way complex.
  168. This is useful, when trying to make sure, that a path does
  169. not point to resources, which the context shall have no
  170. access to."
  171. (cond
  172. ;; contains sub shell
  173. [(string-contains path "`") #t]
  174. ;; contains upwards navigation
  175. [(string-contains path "/../") #t]
  176. ;; ends with 2 or more dots (a file could be named 3 or more dots)
  177. [(>= (string-suffix-length path "...") 3) #t]
  178. ;; contains tilde
  179. [(string-contains path "~") #t]
  180. ;; contains variables
  181. [(string-contains path "$") #t]
  182. ;; otherwise seems to be safe
  183. [else #f])))
  184. (define get-current-directory
  185. (λ ()
  186. (dirname
  187. (or (current-filename)
  188. (canonicalize-path ".")))))