A Lisp implemented in AWK
;; SPDX-License-Identifier: BSD-2-Clause

;; Herein, we shall generally use "file-path" to mean a path as
;; written in a rule, e.g. /etc/passwd; and the shorter "fp" to mean
;; that path as accessible during the build process, e.g.
;; /home/jaredj/buildin-ma-jails/jail326/root/etc/passwd.

(defun sed (script file-path)
  (let ((new (make-temp-filename-from file-path))
        (fp (*rooted* file-path))
        (nfp (*rooted* new)))
    (system-or-error "sed" "-e" script fp :gt nfp)
    (copy-ogm file-path new)
    (system-or-error "mv" nfp fp)))

;; e.g.
;; (defun do-a-thing-with-a-file (file-path)
;;   (with-existing-rooted-plain-file-a-rule
;;         (file-path fp)
;;         ("we are doing a thing with file %s" file-path)
;;     (do-thing-with fp))))
;;
;; fp in the above example is *introduced* inside the first argument
;; of w-e-r-p-f-a-r.
(macro with-existing-rooted-plain-file-a-rule
  (lambda (args)
    (let ((file-path-name (caar args))
          (fpname (cadar args))
          (rule-log-args (cadr args))
          (body (cddr args)))
      `(with-log-rule ,rule-log-args
         (let ((,fpname (*rooted* ,file-path-name)))
           (if (-f ,fpname)
               (progn ,@body)
             (log-warning "plain file %s not found. no changes made" ,fpname)))))))

(defun replace-line-in-file (file-path detect-regex replace-line)
  (with-existing-rooted-plain-file-a-rule (file-path fp)
      ("replace-line-in-file: in %s, regex %s -> line %s" file-path detect-regex replace-line)
    (let ((script (sprintf "/%s/c \\\n%s\n" detect-regex replace-line)))
      (if (probe-sed-would-change script file-path)
          (sed script file-path)
        (log-info "no change necessary")))))

;; this only adds to the _end_ of the file, at present
(defun add-line-to-file (file-path line detect-regex)
  (with-existing-rooted-plain-file-a-rule (file-path fp)
      ("add-line-to-file: in %s, add %s" file-path line)
    (let* ((grep-command (if detect-regex
                             (list "grep" detect-regex fp)
                           (list "grep" "-xF" line fp)))
           (output (apply probe-output grep-command))
           (script (sprintf "$a\\\n%s\n" line)))
      (if (null output)
          (sed script file-path)
        (log-info "no change necessary")))))

(defun delete-lines-matching (file-path regex)
  (with-existing-rooted-plain-file-a-rule (file-path fp)
      ("delete-lines-matching: in %s, regex %s" file-path regex)
    (let* ((script (sprintf "/%s/d" regex)))
      (if (probe-sed-would-change script file-path)
          (sed script file-path)
        (log-info "no change necessary")))))

(defun file-exists (file-path owner group mode)
  (with-log-rule ("file-exists: %s, owned by %d:%d, mode %s"
                  file-path owner group mode)
    (let ((fp (*rooted* file-path)))
      (cond
       ((-f fp)
        (if (equal (get-ogm file-path) (list owner group mode))
              (log-info "no change necessary")
            (set-ogm file-path owner group mode)))
       ((-e fp)
        (error "%s exists but is not a plain file" fp))
       (true 
        (system-or-error "touch" fp)
        (set-ogm file-path owner group mode))))))

(defun file-does-not-exist (file-path)
  (with-log-rule ("file-does-not-exist: %s" file-path)
    (let ((fp (*rooted* file-path)))
      (cond
       ((-f fp)
        (system-or-error "rm" fp))
       (true
        (log-info "no change necessary"))))))

(defun dir-exists (dir-path owner group mode)
  (with-log-rule ("dir-exists: %s" dir-path)
    (let ((dp (*rooted* dir-path)))
      (cond
       ((-d dp)
        (if (equal (get-ogm dp) (list owner group mode))
            (log-info "no change necessary")
          (set-ogm dp owner group mode)))
       ((-e dp)
        (error "%s exists but is not a directory" dp))
       (true
        (system-or-error "mkdir" dp)
        (set-ogm dp owner n-group mode))))))

(defun editing-nonexistent-file-error (file-path)
  (error "editing nonexistent file %s. ensure file-exists first" file-path))

(defun file-has-immediate-contents (file-path contents)
  (with-log-rule ("file-has-immediate-contents: %s (contents not shown)"
                  file-path)
    (let ((new (make-temp-filename-from file-path))
          (fp (*rooted* file-path))
          (new-fp (*rooted* new)))
      (if (-f fp)
          (progn
            (with-output-to ">" new-fp (printf "%s" contents))
            (if (equal (crypto-digest fp)
                       (crypto-digest new-fp))
                (progn
                  (log-info "no change necessary")
                  (system "rm" "-f" new-fp))
              (copy-ogm file-path new)
              (system-or-error "mv" new-fp fp)))
        (editing-nonexistent-file-error file-path)))))

(defun file-text-copied-from (dest-file-path src-file-path)
  ;; We're not going to assume the src-file-path has the right perms.
  ;; Also the src-file-path is expected to be outside any chroot.
  (when (not (-f src-file-path))
    (error "source file %s does not exist" src-file-path))
  (with-existing-rooted-plain-file-a-rule (dest-file-path dfp)
      ("file-contents-copied-from: %s <- %s" dest-file-path src-file-path)
    (if (equal (crypto-digest dfp) (crypto-digest src-file-path))
        (log-info "no change necessary")
      (let ((tmp  (make-temp-filename-from dest-file-path))
            (dfp-tmp (*rooted* tmp)))
        (with-input-from "<" src-file-path
          (with-output-to ">" dfp-tmp
            (dolines (lambda (l) (printf "%s\n" l)))))
        (copy-ogm dest-file-path tmp)
        (system-or-error "mv" dfp-tmp dfp)))))

(defun file-contents-from-m4-template (dest tmpl vars)
  (when (not (-f tmpl))
    (error "nonexistent template %s" tmpl))
  (with-existing-rooted-plain-file-a-rule (dest dfp)
      (label
       ((make-dash-D
               (lambda (kv)
                 (let ((k (car kv)) (v (cadr kv)))
                   (sprintf "-D%s:%s" k v)))))
       (let ((ft (make-temp-filename-from "fcfm4t-XXXXXXXX"))
             (rft (*rooted* ft))
             (dash-Ds (mapcar make-dash-D vars)))
         (apply system-or-error `("m4" ,@dash-Ds :gt ,rft))
         (if (equal (crypto-digest rft)
                    (crypto-digest dfp))
             (log-info "no change necessary")
           (copy-ogm dest ft)
           (system-or-error "mv" rft dfp))))))

(defun file-exists-with-entire-contents (file-path o g m desc contents)
  (file-exists file-path o g m)
  (file-has-immediate-contents file-path contents))

(defun local-user-exists (user-name additional-groups)
  (let ((alt-root-addendum
         (if *fact-alt-root* (list "-R" *fact-alt-root*) nil))
        (groups-addendum
         (if additional-groups
             (list "-G" (apply string-join "," additional-groups))
           nil)))
    (if (probe-success "grep" "-q" (sprintf "^%s:" user-name)
                       (*rooted* "/etc/passwd"))
        (log-info "no change necessary")
      (apply system-or-error `("pw" ,@alt-root-addendum
                               "useradd" "-n" ,user-name "-m"
                               ,@group-addendum)))))

(defun package-installed (name pkg-opts)
  (let* ((pkg-opts (if *fact-alt-root*
                       (append (list "-r" *fact-alt-root*) pkg-opts)
                     pkg-opts))
         (installed (apply probe-success `("pkg" ,@pkg-opts
                                           "info" "-e" name))))
    (if installed
        (log-info "no change necessary")
      ;; "If pkg is already installed, nothing is done."
      (apply system-or-error `("pkg" ,@pkg-opts "bootstrap"))
      (apply system-or-error `("pkg" ,@pkg-opts "install" "-y" ,name)))))

;; if we have a sysrc variable whose name is "varname", whose existing
;; value is "value1 value2", and we run "sysrc varname+=value3", then
;; sysrc says "varname: value1 value2 -> value1 value2 value3".
;; between the colon and the arrow is the old value; between the arrow
;; and the end of the line is the new value.
(defun parse-sysrc-output (output)
  (let* ((just-old-and-new (trim (sub "^[^:]+:" "" res)))
         (oan (split just-old-and-new " -> "))
         (o (car oan))
         (n (cadr oan)))
    (list o n)))
    
;; op is :set, :add, or :remove
(defun sysrc (op name value)
  (let ((alt-root
         (if *fact-alt-root* (list "-R" *fact-alt-root*) nil)))
    (cond
     ((eq op :set)
      (let ((setting (sprintf "%s=%s" name value)))
        (if (apply probe-success `("sysrc" "-c" ,@alt-root ,setting))
            (log-info "no change necessary")
          (apply system-or-error `("sysrc" ,@alt-root ,setting)))))
     ((eq op :add)
      ;; You can't sysrc -c ...+=... But if you try to add a value
      ;; onto a variable when it's already in there, nothing happens:
      ;; the += operation is idempotent. But we don't want to just
      ;; always say we changed it: that'll drown real changes in
      ;; chaff.
      ;;
      ;;
      ;; If we are trying to add value3 and it wasn't there to begin
      ;; with, the old value will be different from the new value. but
      ;; if it was already in there, the two complete values will be
      ;; the same.
      (let* ((add-s (sprintf "%s+=$s" name value))
             (sub-s (sprintf "%s-=%s" name value))
             (res (apply output-of `("sysrc" ,@alt-root ,add-s)))
             (old-and-new (parse-sysrc-output res))
             (o (car old-and-new))
             (n (cadr old-and-new)))
        (if (equal o n)
            ;; The value was unchanged by our snooping. That means the
            ;; thing we were trying to add is already in there, so our
            ;; check succeeds and our change will not be done (nor
            ;; superfluously reported).
            (log-info "no change necessary")
          ;; We changed it while trying to check. The added value will
          ;; be on the end, so if we remove it, everything will be
          ;; back how we found it, so we can then change it. :)
          ;;
          ;; -- except here in lacrum, we aren't separating checks and
          ;; changes yet, nor worrying about how to undo a change, so
          ;; we shall just be happy. we did the change!
          true)))
     ((eq op :remove)
      ;; Same as above: you can't check (-c) with -=.
      (let* ((sub-s (sprintf "%s-=%s" name value))
             (res (apply output-of `("sysrc" ,@alt-root ,sub-s)))
             (old-and-new (parse-sysrc-output res))
             (o (car old-and-new))
             (n (cadr old-and-new)))
        (if (equal o n)
            (log-info "no change necessary")
          ;; while checking it we removed it. be happy
          true)))
     (true
      (error "unknown sysrc op %s; use :set, :add, or :remove" op)))))