fslib.scm 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. ;;; NOTE: This code makes use of an invented term "fsing",
  2. ;;; which is a combination of "fs" and "thing", in lack of a
  3. ;;; better name for something that a string like "/a/b/c"
  4. ;;; describes. The word "path" is in some contexts
  5. ;;; understood to be a string containing multiple
  6. ;;; directories, usually delimited by a separator like ":",
  7. ;;; as for example in "/a/bin:/b/c/lib/bin:/bla". Using the
  8. ;;; word "path" can be confusing for people from those
  9. ;;; contexts. Using the word "file" instead can be confusing
  10. ;;; for other people, who expect things to be really a file
  11. ;;; and not a directory, when seeing the word "file". So
  12. ;;; neither "file" nor "path" are safe to use and a new term
  13. ;;; is come up with.
  14. ;;; An "fsing" is a string describing, where to find a
  15. ;;; "thing" inside the file system. Examples:
  16. ;;; "/a/b/c"
  17. ;;; "../a/b/c"
  18. ;;; "./a/b/c"
  19. ;;; "/a/../b/c"
  20. ;;; "/a/./b/c"
  21. ;;; "/a/b/c.txt"
  22. ;;; "../a/b/c.txt"
  23. ;;; "./a/b/c.txt"
  24. ;;; "/a/../b/c.txt"
  25. ;;; "/a/./b/c.txt"
  26. ;;; "a/b/c"
  27. ;;; "a/../b/c"
  28. ;;; "a/./b/c"
  29. ;;; "a/b/c.txt"
  30. ;;; "a/../b/c.txt"
  31. ;;; "a/./b/c.txt"
  32. (library (fslib (0 1 0))
  33. (export absolute-fsing
  34. absolute-fsing?
  35. fsing-join
  36. fsing-split
  37. file-extension
  38. file-name
  39. dir-name
  40. sub-fsing?
  41. complex-fsing?
  42. get-current-directory
  43. parent-fsing)
  44. (import
  45. (except (rnrs base) let-values)
  46. (only (guile)
  47. ;; lambda forms
  48. lambda* λ
  49. ;; file system stuff
  50. dirname
  51. basename
  52. file-name-separator-string
  53. canonicalize-path
  54. absolute-file-name?
  55. current-filename
  56. current-output-port
  57. ;; string stuff
  58. string-null?
  59. string-trim-right
  60. string-split
  61. string-join
  62. string-contains
  63. string-suffix-length
  64. ;; exception stuff
  65. false-if-exception
  66. ;; debugging
  67. pk
  68. simple-format)
  69. ;; Guile modules
  70. ;; alist->hash-table
  71. ;; (prefix (ice-9 hash-table) ice9-hash-table:)
  72. ;; Guile exception handling
  73. (ice-9 exceptions)
  74. ;; (ice-9 session)
  75. ;; for bytevector operations
  76. (ice-9 binary-ports)
  77. ;; SRFIs
  78. ;; list functions
  79. ;; (prefix (srfi srfi-1) srfi-1:)
  80. ;; hash tables
  81. ;; (prefix (srfi srfi-69) srfi-69:)
  82. ;; receive form
  83. ;; (prefix (srfi srfi-8) srfi-8:)
  84. ;; let-values
  85. ;; (prefix (srfi srfi-11) srfi-11:)
  86. ;; list utils
  87. (prefix (srfi srfi-1) srfi-1:)
  88. ;; web server, concurrent
  89. (string-utils)
  90. (list-utils)
  91. (file-system)
  92. (prefix (logging) log:)))
  93. (define fsing-sep file-name-separator-string)
  94. (define fsing-join
  95. (λ (fsing1 . other-fsing-parts)
  96. "Join fsings using the system preferred separator."
  97. (let ([all-parts (cons fsing1 other-fsing-parts)])
  98. (let ([dir-sep (car (string->list fsing-sep))])
  99. (string-join
  100. ;; Remove one suffix separator. One will be added
  101. ;; by joining again. This prevents joining from
  102. ;; changing the parts.
  103. (map-to-all-except-last
  104. (λ (part)
  105. ;; (log:debug "removing" dir-sep "from suffix of" part)
  106. (remove-suffix part (char->string dir-sep)))
  107. ;; TODO: FUTURE: Perhaps use a vector instead, to
  108. ;; avoid having to reverse a list, which is O(n)
  109. ;; for the number of parts. Although perhaps not,
  110. ;; because usually not so many parts are joined
  111. ;; and the list could even be faster for small n.
  112. (reverse
  113. (let next ([accumulated-parts '()]
  114. [remaining-parts
  115. (srfi-1:filter (λ (part)
  116. (not (string-null? part)))
  117. all-parts)])
  118. (cond
  119. [(null? remaining-parts) accumulated-parts]
  120. ;; If a later fsing is an absolute fsing, then
  121. ;; it is used as the new accumulated
  122. ;; value. Basically a later absolute fsing
  123. ;; overrides the already accumulated fsing,
  124. ;; because it cannot be joined in a useful
  125. ;; way.
  126. [else
  127. (let ([current-part (car remaining-parts)])
  128. (cond
  129. [(absolute-fsing? current-part)
  130. ;; Recur with only the current element as
  131. ;; accumulate parts.
  132. (next (list current-part)
  133. (cdr remaining-parts))]
  134. ;; We know, that the current-elem is not
  135. ;; an absolute fsing and so it can be
  136. ;; usefully joined with the already
  137. ;; accumulated fsing.
  138. [else
  139. ;; Accumulate in reversed order, so that
  140. ;; we do not need to use append.
  141. (next (cons current-part accumulated-parts)
  142. (cdr remaining-parts))]))]))))
  143. ;; Join with the separator as string.
  144. (char->string dir-sep))))))
  145. (define fsing-split
  146. (λ (fsing)
  147. "Split a fsing by the preferred separator of the
  148. system."
  149. (string-split fsing (string->char fsing-sep))))
  150. (define fsing-empty?
  151. (λ (str)
  152. (string-null? str)))
  153. (define absolute-fsing
  154. (lambda* (fsing #:key
  155. (working-directory (get-current-directory))
  156. (canonicalize #f))
  157. "Return the absolute fsing of a given absolute or
  158. non-absolute fsing.
  159. We give the working directory as a keyword argument, so that
  160. this procedure does not need to make the decision on its own
  161. and the resulting absolute fsings for non-absolute fsings do
  162. not necessarily depend on where exactly this module is
  163. located in the file system."
  164. (cond
  165. ;; An empty fsing means current directory.
  166. [(fsing-empty? fsing) (absolute-fsing working-directory)]
  167. ;; If the fsing is already an absolute fsing, simply
  168. ;; return that, but only if it does not need to be
  169. ;; canonicalized.
  170. [(and (absolute-fsing? fsing) (not canonicalize)) fsing]
  171. [else
  172. ;; In case the fsing is not absolute already, we look
  173. ;; for it in the current directory.
  174. (let next ([fsing-parts
  175. ;; Splitting the fsing to work with its
  176. ;; parts means, that the list of parts
  177. ;; will contain the empty string, if the
  178. ;; fsing starts with the separator, which
  179. ;; usually implies an absolute fsing.
  180. (fsing-split (fsing-join working-directory fsing))]
  181. [accumulated-parts '()])
  182. (cond
  183. ;; WARNING: This part is not OS independent. An
  184. ;; absolute fsing does not have to start with the
  185. ;; separator string in all OS.
  186. ;; If there are no more parts, return the
  187. ;; accumulated parts.
  188. [(null? fsing-parts)
  189. ;; An empty first string in accumulated-parts
  190. ;; implies an absolute fsing. However, joining
  191. ;; would not translate it into an absolute fsing,
  192. ;; so we need to change that first string into a
  193. ;; separator.
  194. (apply fsing-join
  195. (let ([rev-acc-parts (reverse accumulated-parts)])
  196. (cond
  197. [(string-null? (car rev-acc-parts))
  198. (cons fsing-sep (cdr rev-acc-parts))]
  199. [else
  200. rev-acc-parts])))]
  201. ;; if canonicalize, then check for ".." and for "."
  202. ;; and act accordingly
  203. [canonicalize
  204. (cond
  205. ;; ignore "." parts
  206. [(string=? (car fsing-parts) ".")
  207. (next (cdr fsing-parts) accumulated-parts)]
  208. ;; ".." reduces accumulated-parts by 1 part
  209. [(string=? (car fsing-parts) "..")
  210. (next (cdr fsing-parts)
  211. (cdr accumulated-parts))]
  212. [else
  213. (next (cdr fsing-parts)
  214. (cons (car fsing-parts)
  215. accumulated-parts))])]
  216. ;; progress without checking for ".." and "."
  217. [else
  218. (next (cdr fsing-parts)
  219. (cons (car fsing-parts)
  220. accumulated-parts))]))])))
  221. (define absolute-fsing?
  222. (λ (fsing)
  223. "Check, whether the given fsing is an absolute fsing."
  224. ;; Guile already offers a function for this, but it is a
  225. ;; little bit strangely named, as it can be used for
  226. ;; files and directories, not only for files. We only
  227. ;; give it an alias.
  228. (absolute-file-name? fsing)))
  229. (define file-extension
  230. (λ (fsing)
  231. "Get the file extension of the given fsing or #f if
  232. there is no file extension."
  233. (cond
  234. ;; An empty string is given, there can be no file
  235. ;; extension.
  236. [(string-null? fsing) #f]
  237. [else
  238. (let ([fsing-last-part (basename fsing)]
  239. [file-extension-separator #\.])
  240. (let ([last-part-split (string-split fsing-last-part file-extension-separator)])
  241. (cond
  242. ;; If the split did not produce more than one
  243. ;; part, then the split character was not found
  244. ;; and so the fsing does not have a file
  245. ;; extension.
  246. [(= (length last-part-split) 1) #f]
  247. [else
  248. (let ([perhaps-file-extension (srfi-1:last last-part-split)])
  249. ;; A file name could end with a "." and that
  250. ;; would produce an empty string as file
  251. ;; extension. This procedure does not consider
  252. ;; the empty string to be a file extension.
  253. (if (string-null? perhaps-file-extension)
  254. #f
  255. perhaps-file-extension))])))])))
  256. (define file-name
  257. (lambda* (fsing #:key (file-extension-separator #\.))
  258. "Return the name part of a filename (the filename without
  259. the file extension)."
  260. (let ([base (basename fsing)])
  261. (let ([parts (string-split base file-extension-separator)])
  262. (cond
  263. [(null? parts) #f]
  264. [(= (length parts) 1) base]
  265. [(and (= (length parts) 2)
  266. (string-null? (srfi-1:first parts)))
  267. #f]
  268. [else
  269. (string-join (srfi-1:drop-right parts 1)
  270. (char->string file-extension-separator))])))))
  271. (define sub-fsing?
  272. (λ (fsing parent-fsing)
  273. "Check, whether a fsing is a sub fsing of a given parent
  274. fsing."
  275. (cond
  276. ;; We want to avoid complicated fsings for now and
  277. ;; simply claim, that complex fsings are not in any
  278. ;; parent fsing for security reasons.
  279. [(complex-fsing? fsing) #f]
  280. [else
  281. (let ([canon-abs-fsing (absolute-fsing fsing #:canonicalize #t)]
  282. [canon-abs-parent-fsing (absolute-fsing parent-fsing #:canonicalize #t)])
  283. (let ([fsing-parts (fsing-split canon-abs-fsing)]
  284. [parent-fsing-parts
  285. (fsing-split (remove-multiple-suffix canon-abs-parent-fsing fsing-sep))])
  286. (list-prefix? fsing-parts parent-fsing-parts)))])))
  287. (define complex-fsing?
  288. (λ (fsing)
  289. "Check, whether the given fsing contains anything, which
  290. could be used to navigate upwards in the file system tree or
  291. is in any way complex.
  292. This is useful, when trying to make sure, that a fsing does
  293. not point to resources, which the context shall have no
  294. access to."
  295. (cond
  296. ;; contains sub shell
  297. [(string-contains fsing "`") #t]
  298. ;; contains upwards navigation
  299. ;; [(string-contains fsing "/../") #t]
  300. ;; contains 2 dots
  301. ;; [(string-contains fsing "..") #t]
  302. ;; contains tilde
  303. [(string-contains fsing "~") #t]
  304. ;; contains variables
  305. [(string-contains fsing "$") #t]
  306. ;; otherwise seems to be safe
  307. [else #f])))
  308. (define get-current-directory
  309. (λ ()
  310. (dirname
  311. (or (current-filename)
  312. (canonicalize-path ".")))))
  313. (define parent-fsing
  314. (λ (fsing)
  315. ;; simply using already in GNU Guile defined procedure dirname
  316. (dirname fsing)))