firejail-mode.el 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863
  1. ;;; firejail-mode --- Major mode for editing firejail profiles -*- lexical-binding: t -*-
  2. ;;; Commentary:
  3. ;;; Code:
  4. (require 'find-file)
  5. (require 'custom)
  6. (require 'thingatpt)
  7. (require 'man)
  8. (eval-when-compile
  9. (require 'rx))
  10. (defgroup firejail-mode ()
  11. "Major mode for editing Firejail profiles."
  12. :group 'programming
  13. :prefix "firejail-")
  14. (defcustom firejail-executable "firejail"
  15. "Executable to use when calling firejail."
  16. :tag "Executable"
  17. :group 'firejail-mode
  18. :type 'string)
  19. (defcustom firejail-include-search-directories
  20. '("./" "~/.config/firejail/" "/etc/firejail/" "/usr/local/etc/firejail/")
  21. "List of directories to search for include files."
  22. :tag "Include Search Directories"
  23. :group 'firejail-mode
  24. :type '(repeat string))
  25. (defcustom firejail-include-search-suffixes
  26. '("inc" "local" "profile")
  27. "List of file suffixes to use when searching for include files.
  28. These should _NOT_ have a leading period."
  29. :tag "Include Search Suffixes"
  30. :group 'firejail-mode
  31. :type '(repeat string))
  32. (defcustom firejail-include-ignored-files
  33. '(".git/")
  34. "List of file names that should be ignored when searching for include files.
  35. These should end with a slash (/) if their are a directory."
  36. :tag "Include Ignored Files"
  37. :group 'firejail-mode
  38. :type '(repeat string))
  39. (defface firejail-error-face
  40. '((t :background "red"))
  41. "Face for reporting Firejail syntax errors."
  42. :tag "Error Face"
  43. :group 'firejail-mode)
  44. (defun firejail--debug-output-to-list (&rest args)
  45. "Convert the output from one of Firejail's --debug-* commands to a list.
  46. ARGS are passed uncaged to Firejail and should include the proper debug command."
  47. (ignore-error file-missing
  48. (mapcan (lambda (line)
  49. (when (string-match (rx "- " (group (+ any)) eol) line)
  50. (list (match-string 1 line))))
  51. (apply 'process-lines firejail-executable args))))
  52. (defconst firejail--known-caps
  53. (firejail--debug-output-to-list "--debug-caps")
  54. "A list of known Linux capabilities.
  55. This will probably be empty on anything but Linux.")
  56. (defconst firejail--known-syscalls64
  57. (firejail--debug-output-to-list "--debug-syscalls")
  58. "A list of known 64 bit system calls.
  59. This will probably be empty on anything by Linux.")
  60. (defconst firejail--known-syscalls32
  61. (firejail--debug-output-to-list "--debug-syscalls32")
  62. "A list of known system 32 bit calls.
  63. This will probably be empty on anything by Linux.")
  64. (defconst firejail--known-errnos
  65. (firejail--debug-output-to-list "--debug-errnos")
  66. "A list of known system 32 bit calls.
  67. This will probably be empty on anything by Linux.")
  68. (defconst firejail--known-conditionals
  69. '("HAS_APPIMAGE" "HAS_NET" "HAS_NODBUS" "HAS_NOSOUND" "HAS_PRIVATE"
  70. "HAS_X11" "ALLOW_TRAY" "BROWSER_DISABLE_U2F" "BROWSER_ALLOW_DRM")
  71. "List of conditionals known to Firejail.")
  72. (defun firejail--list-dbus-services (bus)
  73. "List all DBus services on BUS.
  74. BUS is one of `:system' or `:session'."
  75. (ignore-errors
  76. (require 'dbus nil t)
  77. (when (fboundp 'dbus-call-method) ;; silence byte compiler
  78. (dbus-call-method bus "org.freedesktop.DBus" "/org/freedesktop/DBus"
  79. "org.freedesktop.DBus" "ListNames"))))
  80. (defun firejail--insert-entire-special-file (file)
  81. "Insert all of FILE (e.g. /proc/cpuinfo), even if it's special."
  82. (while (>= (cl-second (insert-file-contents file nil (1- (point))
  83. (+ (point) 9999)))
  84. 10000)
  85. (goto-char (point-max))))
  86. (defvar-local firejail--num-cpus-cache nil
  87. "The number of CPUs the current system has.
  88. This might be nil on platforms other than Linux.")
  89. (defun firejail--get-num-cpus ()
  90. "Return the number of CPUs the current system has."
  91. (if (local-variable-p 'firejail--num-cpus-cache)
  92. firejail--num-cpus-cache
  93. (ignore-error file-missing
  94. (with-temp-buffer
  95. (firejail--insert-entire-special-file "/proc/cpuinfo")
  96. (goto-char (point-max))
  97. (when (re-search-backward (rx bol "processor" blank ":" blank
  98. (group (+ digit)) eol))
  99. (setq firejail--num-cpus-cache
  100. (string-to-number (match-string-no-properties 1))))))))
  101. (defun firejail--find-next-glob-char (limit)
  102. "Find the next glob char between point and LIMIT."
  103. (let ((max-lisp-eval-depth 10000))
  104. (when (search-forward "*" limit t)
  105. (backward-char)
  106. (if (not (eq t (nth 5 (syntax-ppss))))
  107. (progn
  108. (looking-at (regexp-quote "*"))
  109. (forward-char)
  110. t)
  111. (forward-char)
  112. (firejail--find-next-glob-char limit)))))
  113. (defun firejail--generate-documentation-table ()
  114. "Parse the firejail-profile(5) man page to get a documentation table."
  115. (ignore-error file-missing
  116. (let ((path (car (process-lines-handling-status
  117. manual-program (lambda (status)
  118. (when (not (zerop status))
  119. (signal 'file-missing "")))
  120. "-w" "firejail-profile")))
  121. (ht (make-hash-table)))
  122. (with-temp-buffer
  123. ;; Emacs will auto unzip this if needed
  124. (insert-file-contents path)
  125. (when (re-search-forward (rx bol ".TP\n"
  126. bol "\\fBinclude other.profile" eol)
  127. nil t)
  128. (forward-line -1)
  129. (while (and (not (looking-at-p (rx bol ".SH FILES" eol)))
  130. (re-search-forward (rx bol ".TP\n" bol
  131. "\\fB" (group
  132. (+ (not (any "\n" blank)))))
  133. nil t))
  134. (let ((name (intern (match-string-no-properties 1)))
  135. (start (+ 3 (pos-bol))))
  136. (when (re-search-forward (rx bol ".TP" eol) nil t)
  137. (forward-line -1)
  138. (when (looking-at-p (rx bol eol))
  139. (forward-line -1))
  140. (let* ((raw-doc (buffer-substring-no-properties
  141. start (pos-eol)))
  142. (new-doc (replace-regexp-in-string (rx bol ".br" eol)
  143. "\n" raw-doc))
  144. (cur-doc (gethash name ht)))
  145. (puthash name (concat cur-doc
  146. (when cur-doc "\n\n")
  147. new-doc)
  148. ht)))))))
  149. ;; some manual fixing
  150. (cl-macrolet ((summary (dir text)
  151. `(let ((old-val (gethash ',dir ht)))
  152. (puthash ',dir (concat (symbol-name ',dir) "\n"
  153. ,text (when old-val "\n\n")
  154. old-val)
  155. ht))))
  156. (summary net "Enable a new network namespace.")
  157. (summary bind "Mount bind directories or files."))
  158. ht)))
  159. (defvar-local firejail--documentation-table nil
  160. "Table mapping Firejail directives to their documentation.")
  161. (defun firejail--documentation-for (dir)
  162. "Lookup the documentation for DIR."
  163. (unless firejail--documentation-table
  164. (setq firejail--documentation-table
  165. (firejail--generate-documentation-table)))
  166. (gethash (intern-soft dir) firejail--documentation-table))
  167. (defconst firejail-profile-font-lock-keywords
  168. (let* ((cond-rx (rx (* space) "?" (group (* (any alnum "_"))) (? ":")))
  169. (ignore-rx (rx (group (+ (* space) bow "ignore"))))
  170. (prefix-rx (rx bol (? (regexp cond-rx)) (? (regexp ignore-rx))
  171. (* space)))
  172. kwds)
  173. (cl-flet ((add (dirs &optional opts (face 'font-lock-keyword-face))
  174. (push (list
  175. (rx (regexp prefix-rx)
  176. bow (regexp (regexp-opt (ensure-list dirs) t)) eow
  177. (* space)
  178. (? (regexp (regexp-opt (ensure-list opts) t)) eow))
  179. '(1 font-lock-builtin-face nil t)
  180. '(2 font-lock-keyword-face nil t)
  181. '(3 font-lock-keyword-face)
  182. `(4 ,face nil t))
  183. kwds))
  184. (add-many (dirs opts &optional (face 'font-lock-keyword-face))
  185. (push (list
  186. (rx (regexp prefix-rx)
  187. bow (regexp (regexp-opt (ensure-list dirs) t)) eow)
  188. '(1 font-lock-builtin-face nil t)
  189. '(2 font-lock-keyword-face nil t)
  190. '(3 font-lock-keyword-face)
  191. `(,(rx bow (regexp (regexp-opt opts t)) eow)
  192. nil nil (0 ,face)))
  193. kwds)))
  194. ;; NOTE the order below matters
  195. ;; glob asterisk
  196. (push '("*" 0 'bold append) kwds)
  197. ;; invalid characters
  198. (push `(,(rx (or "\"" "\\")) 0 'firejail-error-face t) kwds)
  199. ;; variables
  200. (push (list (rx "${" (+ (any alnum "_")) "}") 0
  201. 'font-lock-variable-name-face t)
  202. kwds)
  203. ;; ignore
  204. (push (list (rx bol (? (regexp cond-rx)) (regexp ignore-rx) eow)
  205. 2 'font-lock-keyword-face)
  206. kwds)
  207. ;; conditional
  208. (push (list (rx bol (regexp cond-rx) eow) 1 'font-lock-builtin-face) kwds)
  209. ;; can't have a conditional include or quiet
  210. (push (list (rx bol (? (regexp ignore-rx)) (* space)
  211. bow (group (or "include" "quiet")) eow)
  212. 2 'font-lock-keyword-face)
  213. kwds)
  214. ;; directives
  215. (add '("noblacklist" "nowhitelist" "blacklist" "blacklist-nolog" "bind"
  216. "disable-mnt" "keep-config-pulse" "keep-dev-shm" "keep-var-tmp"
  217. "mkdir" "mkfile" "noexec" "private" "private-bin" "private-cache"
  218. "private-cwd" "private-dev" "private-etc" "private-home"
  219. "private-lib" "private-opt" "private-srv" "private-tmp" "read-only"
  220. "read-write" "tmpfs" "tracelog" "whitelist" "whitelist-ro"
  221. "writable-etc" "writable-run-user" "writable-var"
  222. "writable-var-log" "allow-debuggers" "apparmor" "caps" "caps.keep"
  223. "caps.drop" "memory-deny-write-execute" "nonewprivs" "noprinters"
  224. "noroot" "restrict-namespaces" "seccomp" "seccomp.32"
  225. "seccomp.drop" "seccomp.32.drop" "seccomp.keep" "seccomp.32.keep"
  226. "seccomp.block-secondary" "protocol" "xephyr-screen"
  227. "dbus-system.own" "dbus-system.talk" "dbus-system.see"
  228. "dbus-system.call" "dbus-system.broadcast" "dbus-user.own"
  229. "dbus-user.talk" "dbus-user.see" "dbus-user.call"
  230. "dbus-user.broadcast" "nodbus" "cpu" "nice" "rlimit-as"
  231. "rlimit-cpu" "rlimit-fsize" "rlimit-nproc" "rlimit-nofile"
  232. "rlimit-sigpending" "timeout" "allusers" "env" "ipc-namespace"
  233. "keep-fd" "name" "no3d" "noautopulse" "nodvd" "nogroups" "noinput"
  234. "nosound" "notv" "nou2f" "novideo" "machine-id" "defaultgw" "dns"
  235. "hostname" "hosts-file" "x11" "dbus-system" "dbus-user" "ip" "ip6"
  236. "iprange" "mac" "mtu" "net" "netfilter" "netfilter" "netlock"
  237. "netmask" "netns" "veth-name" "deterministic-exit-code"
  238. "deterministic-shutdown" "join-or-start"))
  239. (add "caps.drop" "all")
  240. (add '("net" "shell") "none")
  241. (add '("dbus-system" "dbus-user") '("none" "filter"))
  242. (add '("ip" "ip6") '("none" "dhcp"))
  243. (add "x11" '("none" "xephyr" "xorg" "xpra" "xvfb"))
  244. (add-many "restrict-namespaces" '("cgroup" "ipc" "net" "mnt"
  245. "time" "user" "uts"))
  246. (add-many "protocol" '("unix" "inet" "inet6" "netlink"
  247. "packet" "bluetooth"))
  248. (add-many '("caps.drop" "caps.keep")
  249. firejail--known-caps 'font-lock-builtin-face)
  250. (add-many '("seccomp" "seccomp.drop" "seccomp.keep")
  251. firejail--known-syscalls64 'font-lock-builtin-face)
  252. (add-many '("seccomp.32" "seccomp.32.drop" "seccomp.32.keep")
  253. firejail--known-syscalls32 'font-lock-builtin-face)
  254. (add "seccomp-error-action" '("kill" "log"))
  255. (add "seccomp-error-action" firejail--known-errnos
  256. 'font-lock-builtin-face)
  257. kwds))
  258. "Highlight keywords for `firejail-profile-mode'.")
  259. (defvar firejail-profile-syntax-table
  260. (let ((syn-table (make-syntax-table)))
  261. (modify-syntax-entry ?# "<" syn-table)
  262. (modify-syntax-entry ?\n ">" syn-table)
  263. (modify-syntax-entry ?\" "." syn-table)
  264. (modify-syntax-entry ?\( "." syn-table)
  265. (modify-syntax-entry ?\) "." syn-table)
  266. (modify-syntax-entry ?\[ "." syn-table)
  267. (modify-syntax-entry ?\] "." syn-table)
  268. syn-table)
  269. "Syntax table for `firejail-profile-mode'.")
  270. (defconst firejail-profile--keyword-list
  271. '("ignore" "include" "noblacklist" "nowhitelist" "blacklist" "blacklist-nolog"
  272. "bind" "disable-mnt" "keep-config-pulse" "keep-dev-shm" "keep-var-tmp"
  273. "mkdir" "mkfile" "noexec" "private" "private-bin" "private-cache"
  274. "private-cwd" "private-dev" "private-etc" "private-home" "private-lib"
  275. "private-opt" "private-srv" "private-tmp" "read-only" "read-write" "tmpfs"
  276. "tracelog" "whitelist" "whitelist-ro" "writable-etc" "writable-run-user"
  277. "writable-var" "writable-var-log" "allow-debuggers" "apparmor" "caps"
  278. "caps.keep" "caps.drop" "memory-deny-write-execute" "nonewprivs"
  279. "noprinters" "noroot" "restrict-namespaces" "seccomp" "seccomp.32"
  280. "seccomp.drop" "seccomp.32.drop" "seccomp.keep" "seccomp.32.keep"
  281. "seccomp.block-secondary" "seccomp-error-action" "protocol" "xephyr-screen"
  282. "dbus-system.own" "dbus-system.talk" "dbus-system.see" "dbus-system.call"
  283. "dbus-system.broadcast" "dbus-user.own" "dbus-user.talk" "dbus-user.see"
  284. "dbus-user.call" "dbus-user.broadcast" "nodbus" "cpu" "nice" "rlimit-as"
  285. "rlimit-cpu" "rlimit-fsize" "rlimit-nproc" "rlimit-nofile"
  286. "rlimit-sigpending" "timeout" "allusers" "env" "ipc-namespace" "keep-fd"
  287. "name" "no3d" "noautopulse" "nodvd" "nogroups" "noinput" "nosound" "notv"
  288. "nou2f" "novideo" "machine-id" "defaultgw" "dns" "hostname" "hosts-file"
  289. "x11" "dbus-system" "dbus-user" "ip" "ip6" "iprange" "mac" "mtu" "net"
  290. "netfilter" "netfilter" "netlock" "netmask" "netns" "veth-name"
  291. "deterministic-exit-code" "deterministic-shutdown" "join-or-start" "net"
  292. "shell" "protocol")
  293. "List of keywords used for `firejail-profile-capf'.")
  294. (defun firejail--symlink-directory-p (symlink)
  295. "Return non-nil if SYMLINK has a directory at the end of its chain."
  296. (file-directory-p (file-truename symlink)))
  297. (defun firejail--collect-includes (&optional relative-to)
  298. "Return a list of files that the user is likely to want to include.
  299. With RELATIVE-TO, return a list of files relative to each directory in it."
  300. (let ((pat (concat "\\." (regexp-opt firejail-include-search-suffixes) "\\'"))
  301. (buffer-file (file-name-nondirectory
  302. (directory-file-name (buffer-file-name)))))
  303. (seq-difference
  304. (mapcan (lambda (dir)
  305. (ignore-error file-missing
  306. (cl-loop for (name type) in (directory-files-and-attributes dir)
  307. when (or (and (eq t type)
  308. (not (member name (list "." ".."))))
  309. (and (stringp type)
  310. (firejail--symlink-directory-p type)))
  311. collect (concat name "/")
  312. when (and (string-match-p pat name)
  313. (not (equal name buffer-file))
  314. (not (auto-save-file-name-p name))
  315. (not (backup-file-name-p name)))
  316. collect name)))
  317. (or (ensure-list relative-to) firejail-include-search-directories))
  318. firejail-include-ignored-files)))
  319. (defun firejail--include-completion-table (current-input)
  320. "Return completion table for file name based on CURRENT-INPUT.
  321. The completion table contains just the last component. Therefore, the capf
  322. should specify the START position of this table to be the first character after
  323. the last slash (/) on the line. If none of that made sense, see the
  324. documentation for `completion-at-point-functions'."
  325. (if-let ((last-slash (cl-position ?/ current-input :from-end t))
  326. (base (file-truename
  327. (substring current-input 0 (1+ last-slash)))))
  328. (let ((default-directory base))
  329. (firejail--collect-includes default-directory))
  330. (firejail--collect-includes)))
  331. (defun firejail--guess-system-cfg-directory ()
  332. "Guess the system config directory.
  333. The return value will have a trailing slash."
  334. (or (cl-find-if 'file-directory-p
  335. '("/etc/firejail/" "/usr/local/etc/firejail/"))
  336. "/etc/firejail/"))
  337. (defun firejail--exec-path ()
  338. "Parse the PATH environment variable.
  339. Return a list of files."
  340. (cl-loop for (dir . rest) = exec-path then rest
  341. while rest ;; ignore last element
  342. collect (file-name-as-directory dir)))
  343. (defun firejail--parse-file-argument (arg)
  344. "Parse ARG by resolving variables.
  345. This will return a list. This is because the PATH variable has many directories
  346. in it."
  347. (if (string-match (rx "${" (group (or "HOME" "CFG" "PATH"
  348. "RUNUSER")) "}" (? "/")) arg)
  349. (let ((var (match-string 1 arg))
  350. (rest (substring arg (match-end 0))))
  351. (cond
  352. ((equal var "HOME")
  353. (list (concat (expand-file-name "~/") rest)))
  354. ((equal var "CFG")
  355. (list (concat (firejail--guess-system-cfg-directory) rest)))
  356. ((equal var "RUNUSER")
  357. (list (concat (file-name-as-directory (getenv "XDG_RUNTIME_DIR"))
  358. rest)))
  359. ((equal var "PATH")
  360. (mapcar (lambda (elt)
  361. (concat elt rest))
  362. (firejail--exec-path)))))
  363. (list arg)))
  364. (defun firejail--file-completion-table (current-input &optional dir-only)
  365. "Generate a completion table for files.
  366. CURRENT-INPUT is the current text of the argument to complete. With DIR-ONLY,
  367. only report directory completions."
  368. (ignore-error file-missing
  369. (let ((dir (if-let ((last-idx (cl-position ?/ current-input
  370. :from-end t)))
  371. (substring current-input 0 (1+ last-idx))
  372. current-input)))
  373. (cl-loop for (name type) in (directory-files-and-attributes dir)
  374. when (or (and (eq t type)
  375. (not (member name '("." ".."))))
  376. (and (stringp type)
  377. (firejail--symlink-directory-p type)))
  378. collect (concat name "/")
  379. unless (or type dir-only)
  380. collect name))))
  381. (defun firejail--move-over-string-chars (count)
  382. "Move over COUNT characters, assuming the point is inside a string.
  383. This may move over more than COUNT characters if the string contains escapes."
  384. (cl-loop repeat count
  385. do (cl-loop with read-buf = (string (char-after))
  386. for read-val = (condition-case nil
  387. (read (concat "\"" read-buf "\""))
  388. (end-of-file))
  389. until read-val
  390. do (forward-char) and
  391. do (setq read-buf (concat read-buf (string
  392. (char-after))))
  393. finally (forward-char)
  394. finally return read-val)))
  395. (defun firejail--complete-file-from-table (table-fn index args)
  396. "Complete INDEX of ARGS using TABLE-FN.
  397. TABLE-FN should be a function of one argument that takes the current arg and
  398. returns a completion table for it."
  399. (cl-destructuring-bind (start _end text) (nth index args)
  400. (let* ((base (or (file-name-directory text) ""))
  401. (table (funcall table-fn base)))
  402. (list (+ start (length base)) (+ start (length text)) table))))
  403. (defun firejail--complete-include (index args _directive)
  404. "Complete an include directive's arg numbered INDEX of ARGS."
  405. (firejail--complete-file-from-table #'firejail--include-completion-table
  406. index args))
  407. (defun firejail--complete-file (index args _directive)
  408. "Complete file taking directive's arg numbered INDEX of ARGS."
  409. (firejail--complete-file-from-table #'firejail--file-completion-table
  410. index args))
  411. (defun firejail--complete-directory (index args _directive)
  412. "Complete directory taking directive's arg numbered INDEX of ARGS."
  413. (firejail--complete-file-from-table #'(lambda (base)
  414. (firejail--file-completion-table
  415. base 'dironly))
  416. index args))
  417. (defvar-local firejail--relative-to-cache nil
  418. "Cache for `firejail--complete-relative-to'.")
  419. (defmacro firejail--complete-relative-to (dirs &optional no-absolute)
  420. "Return a function that completes relative to DIRS.
  421. With NO-ABSOLUTE, don't complete absolute file names."
  422. (let ((index (make-symbol "index"))
  423. (args (make-symbol "args"))
  424. (directive (make-symbol "directive"))
  425. (out (make-symbol "out"))
  426. (idirs (make-symbol "dirs"))
  427. (dir (make-symbol "dir"))
  428. (adirname (make-symbol "adirname"))
  429. (evaled-dirs (eval dirs t)))
  430. `(lambda (,index ,args ,directive)
  431. (unless firejail--relative-to-cache
  432. (setq firejail--relative-to-cache (make-hash-table :test 'equal)))
  433. (let ((,idirs (cl-remove-if-not #'file-directory-p
  434. (ensure-list ',evaled-dirs)))
  435. (,adirname (file-name-directory (cl-third (nth ,index ,args)))))
  436. (if-let ((cache (gethash (cons ,adirname ,dirs)
  437. firejail--relative-to-cache)))
  438. cache
  439. (let (,out)
  440. (dolist (,dir ,idirs)
  441. ,(let ((stmt
  442. `(let ((default-directory ,dir))
  443. (push (firejail--complete-file ,index ,args
  444. ,directive)
  445. ,out))))
  446. (if no-absolute
  447. `(unless (file-name-absolute-p
  448. (cl-third (nth ,index ,args)))
  449. ,stmt)
  450. stmt)))
  451. (puthash (cons ,adirname ,idirs)
  452. (append (seq-take (car ,out) 2)
  453. (list (seq-uniq (mapcan 'cl-third ,out))))
  454. firejail--relative-to-cache)))))))
  455. (defmacro firejail--complete-many-from-set (vals)
  456. "Return a function to complete a multi-arg directive from VALS."
  457. (let ((index (make-symbol "index"))
  458. (args (make-symbol "args"))
  459. (directive (make-symbol "directive"))
  460. (i (make-symbol "i"))
  461. (arg (make-symbol "arg"))
  462. (present (make-symbol "present"))
  463. (evaled-vals (eval vals t)))
  464. `(lambda (,index ,args ,directive)
  465. (let ((,present (cl-loop for ,i upfrom 0
  466. for ,arg in ,args
  467. unless (= ,i ,index)
  468. collect (cl-third ,arg))))
  469. (append (seq-take (nth ,index ,args) 2)
  470. (list (seq-difference ,evaled-vals ,present)))))))
  471. (defun firejail--get-all-env-keys ()
  472. "Return the name of every current environment variable."
  473. (mapcar (lambda (elt)
  474. (if-let ((sep (cl-position ?= elt)))
  475. (substring elt 0 sep)
  476. elt))
  477. process-environment))
  478. (defun firejail--complete-env (index args _directive)
  479. "Complete the arg numbered INDEX in ARGS for an \"env\" directive."
  480. (cl-destructuring-bind (start _end text) (nth index args)
  481. (let ((sep-pos (or (cl-position ?= text) (length text))))
  482. (when (<= (point) (+ start sep-pos))
  483. (list start (+ start sep-pos) (firejail--get-all-env-keys))))))
  484. (defconst firejail-profile--keyword-handlers
  485. (let ((ht (make-hash-table :test 'equal)))
  486. (cl-flet* ((complete (args fun dirs)
  487. (dolist (arg (ensure-list (or args (list nil))))
  488. (dolist (dir (ensure-list dirs))
  489. (puthash (cons dir arg) fun ht))))
  490. (complete-all (fun dirs)
  491. (complete nil fun dirs)))
  492. (complete 1 #'firejail--complete-include "include")
  493. (complete 1 #'firejail--complete-file
  494. '("whitelist" "nowhitelist" "blacklist" "noblacklist"
  495. "blacklist-nolog" "noexec" "read-only" "read-write"
  496. "whitelist-ro" "hosts-file"))
  497. (complete 1 #'firejail--complete-directory
  498. '("mkdir" "mkfile" "private" "private-cwd" "tmpfs"))
  499. (complete '(1 2) #'firejail--complete-file "bind")
  500. (complete-all (firejail--complete-relative-to
  501. '("/bin" "/sbin" "/usr/bin" "/usr/sbin" "/usr/local/bin")
  502. t)
  503. "private-bin")
  504. (complete-all (firejail--complete-relative-to '(getenv "HOME") t)
  505. "private-home")
  506. (complete-all (firejail--complete-relative-to "/lib" t)
  507. "private-lib")
  508. (complete-all (firejail--complete-relative-to "/etc" t)
  509. "private-etc")
  510. (complete-all (firejail--complete-relative-to "/opt" t)
  511. "private-opt")
  512. (complete-all (firejail--complete-relative-to "/srv" t)
  513. "private-srv")
  514. (complete-all (firejail--complete-many-from-set
  515. ;; evaluate at runtime
  516. 'firejail--known-caps)
  517. "caps.keep")
  518. (complete-all (firejail--complete-many-from-set
  519. ;; evaluate at runtime
  520. '(cons "all" firejail--known-caps))
  521. "caps.drop")
  522. (complete-all (firejail--complete-many-from-set
  523. ''("unix" "inet" "inet6" "netlink" "packet" "bluetooth"))
  524. "protocol")
  525. (complete-all (firejail--complete-many-from-set
  526. ''("cgroup" "ipc" "mnt" "pid" "time" "user" "uts"))
  527. "restrict-namespaces")
  528. (complete-all (firejail--complete-many-from-set
  529. 'firejail--known-syscalls64)
  530. '("seccomp" "seccomp.drop" "seccomp.keep" ))
  531. (complete-all (firejail--complete-many-from-set
  532. 'firejail--known-syscalls32)
  533. '("seccomp.32" "seccomp.32.drop" "seccomp.32.keep"))
  534. (complete 1 (firejail--complete-many-from-set
  535. '(firejail--list-dbus-services :system))
  536. '("dbus-system" "dbus-system.own" "dbus-system.talk"
  537. "dbus-system.see"))
  538. (complete 1 (firejail--complete-many-from-set
  539. '(firejail--list-dbus-services :session))
  540. '("dbus-user" "dbus-user.own" "dbus-user.talk" "dbus-user.see"))
  541. (complete 1 (firejail--complete-many-from-set
  542. '(append '("kill" "log") firejail--known-errnos))
  543. "seccomp-error-action")
  544. (complete 1 (firejail--complete-many-from-set
  545. ''("none" "xephyr" "xorg" "xpra" "xvfb"))
  546. "x11")
  547. (complete 1 (firejail--complete-many-from-set
  548. ''("none" "filter"))
  549. '("dbus-system" "dbus-user"))
  550. (complete 1 (firejail--complete-many-from-set
  551. ''("none" "dhcp"))
  552. '("ip" "ip6"))
  553. (complete 1 (firejail--complete-many-from-set
  554. ''("none"))
  555. '("net" "shell"))
  556. (complete-all (firejail--complete-many-from-set
  557. '(mapcar 'number-to-string
  558. (number-sequence 0 (firejail--get-num-cpus))))
  559. "cpu")
  560. (complete 1 #'firejail--complete-env "env"))
  561. ht)
  562. "Hash table mapping firejail profile directives to their handler.
  563. Each handler is a function of three arguments. The first is the index of the
  564. current argument, the second is a list of the arguments, the third the
  565. directive. These functions mustn't move the point. The point will be on the
  566. first character of the argument. The keys of this table are a cons of a
  567. directive and its argument number. The values are the completion functions.")
  568. (defun firejail--quiet-allowed-p ()
  569. "Return non-nil if the \"quiet\" directive is allowed on line under point."
  570. (save-excursion
  571. (let ((orig-line (line-number-at-pos)))
  572. (goto-char (point-min))
  573. (while (forward-comment 1))
  574. (>= (line-number-at-pos) orig-line))))
  575. (defun firejail--ignored-line-p ()
  576. "Return non-nil if the line under point is an \"ignore\" directive.
  577. Actually, return the position of the first character of the \"real\" directive."
  578. (save-excursion
  579. (beginning-of-line)
  580. (when (looking-at (rx bol (* space) (? "?" (* (any alnum "_")) (? ":"))
  581. (+ (* space) "ignore" eow) (or eol (+ space))
  582. (group (* nonl)) eol))
  583. (match-beginning 1))))
  584. (defun firejail--read-next-arg ()
  585. "Return the bounds of the next argument from the buffer starting at point.
  586. This returns a list of four things, the first two are the start and end of the
  587. current argument. The third is the text of the argument."
  588. (skip-syntax-forward "-")
  589. (looking-at (rx (group (* (not (any "\n" "," "#"))))
  590. (* space) (or eol "," "#")))
  591. (goto-char (match-end 0))
  592. (when (eql ?# (char-before))
  593. (backward-char))
  594. (list (match-beginning 1) (match-end 1)
  595. (match-string-no-properties 1)))
  596. (defun firejail--has-more-args-p ()
  597. "Return non-nil if there are probably more args beyond point on this line."
  598. (save-excursion
  599. (skip-syntax-forward "-")
  600. (not (or (eobp) (memql (char-after) '(?\n ?#))))))
  601. (defun firejail--multi-arg-directive-p (name)
  602. "Return non-nil if NAME is a multi-argument directive."
  603. (member name '("bind" "private-bin" "private-etc" "private-home"
  604. "private-lib" "private-opt" "private-srv" "caps.drop"
  605. "caps.keep" "protocol" "restrict-namespaces"
  606. "seccomp" "seccomp.32" "seccomp.drop" "seccomp.32.drop"
  607. "seccomp.keep" "seccomp.32.keep" "cpu" "iprange")))
  608. (defun firejail--current-args (dir arg-start)
  609. "Return a list of the text of each argument in the directive DIR under point.
  610. ARG-START is the first character of the list of arguments."
  611. (if (firejail--multi-arg-directive-p dir)
  612. (append (save-excursion
  613. (goto-char arg-start)
  614. (cl-loop while (firejail--has-more-args-p)
  615. collect (firejail--read-next-arg)))
  616. (list (list (point) (point) "")))
  617. (save-excursion
  618. (goto-char arg-start)
  619. (skip-syntax-forward "-")
  620. (let ((eol (pos-eol)))
  621. (list (list (point) eol
  622. (buffer-substring-no-properties
  623. (point) eol)))))))
  624. (defun firejail--count-args (start end)
  625. "Return the number of arguments between START and END."
  626. (1+ (how-many "," start end)))
  627. (defun firejail--complete-arguments (directive arg-start)
  628. "Generate completions for the argument that the point is currently in.
  629. DIRECTIVE is the directive to generate completions for. ARG-START is the first
  630. argument character on the current line."
  631. (let* ((cur-arg (if (firejail--multi-arg-directive-p directive)
  632. (firejail--count-args arg-start (point))
  633. 1)))
  634. (when-let ((handler (or (gethash (cons directive nil)
  635. firejail-profile--keyword-handlers)
  636. (gethash (cons directive cur-arg)
  637. firejail-profile--keyword-handlers))))
  638. (funcall handler (1- cur-arg)
  639. (firejail--current-args directive arg-start)
  640. directive))))
  641. (defun firejail--line-conditional-p ()
  642. "Return non-nil if the line under point begins with a conditional.
  643. Actually, return a list of its bounds and the bounds of its name."
  644. (save-excursion
  645. (beginning-of-line)
  646. (skip-syntax-forward "-")
  647. (when (looking-at (rx (group "?" (group (* (any "_" alnum))) (? ":"))
  648. (or eol (+ space) "#")))
  649. (list (match-beginning 1) (match-end 1) (match-beginning 2)
  650. (match-end 2)))))
  651. (defun firejail--complete-conditional (start end)
  652. "Complete the conditional around point.
  653. START and END are the bounds of the name of the conditional."
  654. (list start end '()))
  655. (defun firejail-profile-capf ()
  656. "Complete the Firejail profile directive at point."
  657. (save-excursion
  658. ;; don't complete comments
  659. (unless (nth 4 (syntax-ppss (point)))
  660. (let ((start-pos (point)))
  661. (back-to-indentation)
  662. (let ((condition (firejail--line-conditional-p))
  663. (ignored (firejail--ignored-line-p)))
  664. (if (and condition (>= start-pos (cl-first condition))
  665. (<= start-pos (cl-second condition)))
  666. (list (cl-third condition) (cl-fourth condition)
  667. ;; is there already a '?'
  668. (if (= (cl-second condition) (cl-fourth condition))
  669. (mapcar (lambda (elt)
  670. (concat elt ":"))
  671. firejail--known-conditionals)
  672. firejail--known-conditionals))
  673. (cond
  674. (ignored (goto-char ignored))
  675. (condition
  676. (goto-char (1+ (cl-second condition)))
  677. (skip-syntax-forward "-")))
  678. ;; read the directive name
  679. (looking-at (rx (group (* (not (any space "#" "\n"))))
  680. (? (group space))))
  681. (let ((directive-start (match-beginning 1))
  682. (directive-end (match-end 1))
  683. (arg-start (match-end 2)))
  684. (if (and arg-start (>= start-pos arg-start))
  685. (progn
  686. (goto-char start-pos)
  687. (firejail--complete-arguments
  688. (buffer-substring-no-properties directive-start
  689. directive-end)
  690. arg-start))
  691. (cond
  692. ((= directive-start directive-end)
  693. (setq directive-start start-pos
  694. directive-end start-pos))
  695. ((and (< start-pos directive-start)
  696. (eql 2 (syntax-class (syntax-after (1- start-pos)))))
  697. (save-excursion
  698. (goto-char start-pos)
  699. (forward-word -1)
  700. (setq directive-start (point)
  701. directive-end start-pos)))
  702. ((< start-pos directive-start)
  703. (setq directive-start start-pos
  704. directive-end start-pos)))
  705. (list
  706. directive-start directive-end
  707. (append (when (and (not condition) (not ignored)
  708. (firejail--quiet-allowed-p))
  709. '("quiet"))
  710. firejail-profile--keyword-list))))))))))
  711. (defun firejail--directive-at-point ()
  712. "Return the name of the directive at point."
  713. (save-excursion
  714. (beginning-of-line)
  715. (when (looking-at (rx bol (* space)
  716. (? "?" (* (any alnum "_")) (? ":")
  717. (+ space))
  718. (* "ignore" (+ space))
  719. (group (+ (not (any space "\n" "#"))))))
  720. (let ((name (match-string-no-properties 1)))
  721. (unless (or (equal name "ignore")
  722. (string-prefix-p "?" name)
  723. (string-suffix-p ":" name))
  724. name)))))
  725. (defun firejail--read-next-sentence ()
  726. "Return from point up to the next sentance end."
  727. (let ((start (point))
  728. (end (or (re-search-forward (rx eow "." (or " " eol))
  729. nil t)
  730. (point-max))))
  731. (when (eql (char-before end) ? )
  732. (cl-decf end)
  733. (backward-char))
  734. (cl-substitute ? ?\n (buffer-substring-no-properties
  735. start end))))
  736. (defun firejail--format-doc-string-and-get-summary (dir doc)
  737. "Format DOC and get a summary for DIR.
  738. Return a list of the formatted doc and a summary."
  739. (with-temp-buffer
  740. (insert doc)
  741. (goto-char (point-min))
  742. (forward-line)
  743. (let ((summary (save-excursion
  744. (firejail--read-next-sentence))))
  745. (cl-loop for start = (point)
  746. until (eobp) do
  747. (forward-paragraph)
  748. (fill-region-as-paragraph start (point))
  749. (forward-line)
  750. when (looking-at-p (rx bol (literal dir) (or eol " ")))
  751. do (forward-line))
  752. (goto-char (point-min))
  753. (replace-regexp-in-region (rx (>= 3 "\n")) "\n\n")
  754. (replace-regexp-in-region (rx eow "." (+ blank)) ". ")
  755. (while (re-search-forward (rx ":" eol) nil t)
  756. (forward-line)
  757. (while (and (not (eobp))
  758. (not (char-uppercase-p (char-after))))
  759. (if (= (pos-bol) (pos-eol))
  760. (delete-char 1)
  761. (insert " ")
  762. (forward-line)))
  763. (unless (eobp)
  764. (insert "\n")))
  765. (list (buffer-string) summary))))
  766. (defun firejail-eldoc-documentation-function (callback &rest _args)
  767. "Call CALLBACK with the documentation of the directive under point."
  768. (save-excursion
  769. (when-let ((name (firejail--directive-at-point))
  770. (doc (firejail--documentation-for name)))
  771. (cl-destructuring-bind (clean-doc summary)
  772. (firejail--format-doc-string-and-get-summary name doc)
  773. (funcall callback clean-doc `(:thing ,name
  774. :echo ,summary))))))
  775. (defvar-keymap firejail-profile-mode-map
  776. :doc "Keymap for `firejail-profile-mode'."
  777. :parent prog-mode-map
  778. "C-c C-o" #'ff-find-other-file)
  779. (define-derived-mode firejail-profile-mode prog-mode "Firejail-Profile"
  780. "Major mode for editing firejail profiles."
  781. :group 'firejail-mode
  782. :syntax-table firejail-profile-syntax-table
  783. (add-hook 'completion-at-point-functions #'firejail-profile-capf nil t)
  784. (setq-local font-lock-defaults '(firejail-profile-font-lock-keywords)
  785. comment-start "#"
  786. comment-end ""
  787. electric-pair-pairs '((?{ . ?}))
  788. ff-search-directories firejail-include-search-directories
  789. ff-other-file-alist '(("\\.local\\'" (".profile"))
  790. ("\\.profile\\'" (".local")))
  791. eldoc-documentation-functions
  792. '(firejail-eldoc-documentation-function
  793. t)))
  794. (add-to-list 'auto-mode-alist
  795. '("\\.\\(firejail\\|profile\\|local\\|inc\\)\\'" . firejail-profile-mode))
  796. (provide 'firejail-mode)
  797. ;;; firejail-mode.el ends here
  798. ;; Local Variables:
  799. ;; jinx-local-words: "Firejail Firejail's"
  800. ;; End: