utils.lisp 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. ;; This software is Copyright (c) Leslie P. Polzer, 2011.
  2. ;; Leslie P. Polzer grants you the rights to distribute
  3. ;; and use this software as governed by the terms
  4. ;; of the Lisp Lesser GNU Public License
  5. ;; (http://opensource.franz.com/preamble.html),
  6. ;; known as the LLGPL
  7. (in-package :cl-i18n)
  8. (defparameter *debug* nil)
  9. (defparameter *directory-sep-regexp*
  10. #+windows "\\"
  11. #-windows "\\/")
  12. (defparameter *directory-sep*
  13. #+windows "\\"
  14. #-windows "/")
  15. (defparameter *valid-dir-mofile-repo* "(?i)^(([a-z][a-z])|([a-z][a-z]_[a-z][a-z]))$")
  16. (defparameter *valid-dir-rate-mofile-repo* 0.7)
  17. (defparameter *valid-dir-average-mofile-repo* 40)
  18. (defparameter *mofile-repo-exclude-path* '("^\\/proc" "^\\/sys" "^\\/dev" "^\\/run"))
  19. (defparameter *well-known-mofile-path* '("/usr/share/locale/" "/usr/local/share/locale/"))
  20. (alexandria:define-constant +utx-ext+ "utx$" :test 'string=)
  21. (alexandria:define-constant +pofile-ext+ "po$" :test 'string=)
  22. (alexandria:define-constant +lisp-table-ext+ "lisp$" :test 'string=)
  23. (alexandria:define-constant +mo-file-magic-number+ '(#x95 #x04 #x12 #xde) :test #'equalp)
  24. (defmacro when-debug (&body body)
  25. `(when (not (null *debug*))
  26. ,@body))
  27. (defun slurp-file (filename &key (convert-to-string t))
  28. "A simple way to slurp a file."
  29. (with-open-file (stream filename :direction :input :element-type '(unsigned-byte 8))
  30. (let ((seq (make-array (file-length stream) :element-type '(unsigned-byte 8))))
  31. (read-sequence seq stream)
  32. (if convert-to-string
  33. (babel:octets-to-string seq)
  34. seq))))
  35. (defun create-brand-new-file (file)
  36. (open file :direction :probe :if-does-not-exist :create))
  37. (defun uchar-length (leading-byte)
  38. (let ((ones (do* ((ct 7 (1- ct))
  39. (bit (ldb (byte 1 ct) leading-byte)
  40. (ldb (byte 1 ct) leading-byte))
  41. (ones-ct 0))
  42. ((= bit 0) ones-ct)
  43. (incf ones-ct))))
  44. (cond
  45. ((= ones 0)
  46. 1)
  47. ((= ones 1)
  48. 0)
  49. (t
  50. ones))))
  51. (defun utf8-encoded-p (file)
  52. (with-open-file (stream file :direction :input
  53. :if-does-not-exist :error
  54. ::element-type '(unsigned-byte 8))
  55. (let* ((leading-byte (read-byte stream))
  56. (leading-byte-length (uchar-length leading-byte)))
  57. (cond
  58. ((= leading-byte-length 0)
  59. nil)
  60. ((> leading-byte-length 6)
  61. nil)
  62. (t
  63. (loop for i from 0 below (1- leading-byte-length) do
  64. (let* ((ch (read-byte stream))
  65. (ll (uchar-length ch)))
  66. (when (> ll 0)
  67. (return-from utf8-encoded-p nil))))
  68. t)))))
  69. (defun pathname->string (path)
  70. (uiop/filesystem:native-namestring path))
  71. (defun directoryp (d)
  72. (uiop/filesystem:directory-exists-p d))
  73. (defun list-directory-entries (d)
  74. "Does not resolve symlinks to avoid loop"
  75. (and (directoryp d)
  76. (nconc
  77. (uiop/filesystem:subdirectories d)
  78. (uiop/filesystem:directory-files d))))
  79. (defun file-exists-p (f)
  80. (uiop:file-exists-p f))
  81. (defun file-does-not-exists-p (filename)
  82. (not (file-exists-p filename)))
  83. (defun file-size (filename)
  84. (when (uiop/filesystem:file-exists-p filename)
  85. (with-open-file (stream filename :direction :input :element-type '(unsigned-byte 8))
  86. (file-length stream))))
  87. (defun remove-regular-files (entries)
  88. (remove-if #'(lambda (a) (not (directoryp a))) entries))
  89. (defun file-has-mo-magic-number-p (file)
  90. (when (file-exists-p file)
  91. (when (> (file-size file) (length +mo-file-magic-number+))
  92. (let ((seq (make-list (length +mo-file-magic-number+) :initial-element 0)))
  93. (with-open-file (stream file :direction :input :element-type '(unsigned-byte 8))
  94. (read-sequence seq stream)
  95. (cl-i18n:mo-magic-number-p seq))))))
  96. (defun is-mo-file-p (path &key (ext "\\.mo") (test-magic-number nil))
  97. (and (uiop/filesystem:file-exists-p path)
  98. (cl-ppcre:scan (concatenate 'string ext "$")
  99. (pathname->string path))
  100. (or (not test-magic-number)
  101. (file-has-mo-magic-number-p path))))
  102. (defun cat-parent-dir (parent direntry)
  103. (format nil "~a~a~a" parent *directory-sep* direntry))
  104. (defmacro do-directory ((var) root &body body)
  105. `(and (directoryp ,root)
  106. (loop for ,var in (list-directory-entries root) do ,@body)))
  107. (defun excluded-path-p (dir)
  108. (loop for i in *mofile-repo-exclude-path* do
  109. (if (cl-ppcre:scan i (pathname->string dir))
  110. (return-from excluded-path-p t))))
  111. (defun count-mo-files-direct-children (root)
  112. (let ((count 0))
  113. (do-directory (file) root
  114. (when (is-mo-file-p file)
  115. (incf count)))
  116. count))
  117. (defun mo-repository-p (db)
  118. (if db
  119. (let* ((dircount (/ (length db) 2))
  120. (mofile-count (loop for i from 1 below (length db) by 2 collect (nth i db)))
  121. (mofile-dir (loop for i from 0 below (length db) by 2 collect (symbol-name (nth i db))))
  122. (mofile-last-dir (mapcar
  123. #'(lambda (dir) (car (last (cl-ppcre:split *directory-sep-regexp* dir))))
  124. mofile-dir))
  125. (mofile-valid-dir (remove-if #'null
  126. (mapcar #'(lambda (dir)
  127. (cl-ppcre:scan-to-strings
  128. *valid-dir-mofile-repo* dir))
  129. mofile-last-dir)))
  130. (average-mofile-count (/ (reduce #'+ mofile-count :initial-value 0) dircount)))
  131. (and (> (/ (length mofile-valid-dir) dircount) *valid-dir-rate-mofile-repo*)
  132. (> average-mofile-count *valid-dir-average-mofile-repo*)))
  133. nil))
  134. (defun scan-mo-files (root &optional (db '()) (max-depth 10))
  135. (let ((dirs (list-directory-entries root))
  136. (direct-child-count 0))
  137. (if (and dirs
  138. (> max-depth 0))
  139. (progn
  140. (incf direct-child-count (count-mo-files-direct-children root))
  141. (loop for ent in dirs do
  142. (when (directoryp ent)
  143. (let ((children-count (scan-mo-files ent db (1- max-depth))))
  144. (setf (getf db (alexandria:make-keyword (pathname->string ent)))
  145. children-count)
  146. (incf direct-child-count children-count))))
  147. (values direct-child-count db))
  148. (values 0 db))))
  149. (defun equals-path-fn ()
  150. #'(lambda (a b) (string= (pathname->string a) (pathname->string b))))
  151. (defun count-mo-files (root)
  152. (let ((seen nil))
  153. (do ((stack (list root))
  154. (count 0))
  155. ((null stack) count)
  156. (let* ((dirname (uiop/filesystem:truenamize (pop stack)))
  157. (dirs (remove-regular-files (list-directory-entries dirname))))
  158. (incf count (count-mo-files-direct-children dirname))
  159. (loop for ent in dirs do
  160. (when (not (find ent seen :test (equals-path-fn)))
  161. (push ent seen)
  162. (when (and (directoryp ent)
  163. (not (excluded-path-p ent)))
  164. (push ent stack))))))))
  165. (defun search-mo-repository (root &key (max-path-depth 10))
  166. (let ((seen nil))
  167. (labels ((get-max-count-dir (root)
  168. (let ((max-count (list "" 0)))
  169. (do-directory (dir) root
  170. (when (and (directoryp dir)
  171. (not (excluded-path-p dir))
  172. (not (find dir seen :test (equals-path-fn))))
  173. (push dir seen)
  174. (let ((count (count-mo-files dir)))
  175. (when (> count (second max-count))
  176. (setf max-count (list dir count))))))
  177. max-count)))
  178. (let ((catalog (find-if #'(lambda (p)
  179. (mo-repository-p
  180. (second
  181. (multiple-value-list (scan-mo-files p
  182. '()
  183. max-path-depth)))))
  184. *well-known-mofile-path*)))
  185. (if (not catalog)
  186. (do ((dir (first (get-max-count-dir root)) (first (get-max-count-dir dir))))
  187. ((mo-repository-p (second (multiple-value-list (scan-mo-files dir
  188. '()
  189. max-path-depth))))
  190. dir))
  191. catalog)))))