2024-05-26 17:35:11 -05:00
|
|
|
|
;;;; eksd-unix: UNIX-style cli interface for the xxd-clone eksd.
|
|
|
|
|
|
|
|
|
|
;; Copyright © 2019–2024 Jaidyn Ann <jadedctrl@posteo.at>
|
|
|
|
|
;;
|
|
|
|
|
;; This program is free software: you can redistribute it and/or
|
|
|
|
|
;; modify it under the terms of the GNU General Public License as
|
|
|
|
|
;; published by the Free Software Foundation, either version 3 of
|
|
|
|
|
;; the License, or (at your option) any later version.
|
2019-11-18 23:38:26 -06:00
|
|
|
|
;;
|
|
|
|
|
;; This program is distributed in the hope that it will be useful,
|
|
|
|
|
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
2024-05-26 17:35:11 -05:00
|
|
|
|
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
2019-11-18 23:38:26 -06:00
|
|
|
|
;; GNU General Public License for more details.
|
2024-05-26 17:35:11 -05:00
|
|
|
|
;;
|
|
|
|
|
;; You should have received a copy of the GNU General Public License
|
|
|
|
|
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
|
2019-11-18 23:38:26 -06:00
|
|
|
|
|
|
|
|
|
(defpackage :eksd-unix
|
|
|
|
|
(:use :cl :eksd)
|
|
|
|
|
(:export :invoke
|
|
|
|
|
*text-table*))
|
|
|
|
|
|
|
|
|
|
(in-package :eksd-unix)
|
|
|
|
|
|
2024-05-26 17:35:11 -05:00
|
|
|
|
|
|
|
|
|
;;; —————————————————————————————————————
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(opts:define-opts
|
2024-05-26 17:35:11 -05:00
|
|
|
|
(:name :help
|
|
|
|
|
:description "print this help text."
|
|
|
|
|
:short #\h :long "help")
|
|
|
|
|
(:name :reverse
|
|
|
|
|
:description "reverse operation: convert hexdump into binary."
|
|
|
|
|
:short #\r :long "reverse")
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(:name :upcase
|
2024-05-26 17:35:11 -05:00
|
|
|
|
:description "print hexadecimal in uppercase."
|
|
|
|
|
:short #\u :long "upcase")
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(:name :width
|
2024-05-26 17:35:11 -05:00
|
|
|
|
:description "octets per line; 16 as default."
|
|
|
|
|
:short #\c :long "cols"
|
|
|
|
|
:arg-parser #'parse-integer
|
|
|
|
|
:meta-var "COLS")
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(:name :seek
|
2024-05-26 17:35:11 -05:00
|
|
|
|
:description "skip given amount of bytes in file."
|
|
|
|
|
:short #\s :long "seek"
|
|
|
|
|
:arg-parser #'parse-integer
|
|
|
|
|
:meta-var "OFFSET")
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(:name :group
|
2024-05-26 17:35:11 -05:00
|
|
|
|
:description "octets per group; 2 as default."
|
|
|
|
|
:short #\g :long "groupsize"
|
|
|
|
|
:arg-parser #'parse-integer
|
|
|
|
|
:meta-var "OCTETS")
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(:name :ascii
|
2024-05-26 17:35:11 -05:00
|
|
|
|
:description "use simple-ascii for previews; default is fun."
|
|
|
|
|
:short #\a :long "ascii")
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(:name :text-table
|
2024-05-26 17:35:11 -05:00
|
|
|
|
:description "specify a text-table; semi-ascii as default."
|
|
|
|
|
:short #\t :long "table"
|
|
|
|
|
:arg-parser #'probe-file
|
|
|
|
|
:meta-var "TABLE"))
|
2019-11-18 23:38:26 -06:00
|
|
|
|
|
|
|
|
|
|
2024-05-26 17:35:11 -05:00
|
|
|
|
|
|
|
|
|
;;; Macros
|
|
|
|
|
;;; —————————————————————————————————————
|
|
|
|
|
;; list symbol form
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(defmacro when-opt (opts option body)
|
|
|
|
|
`(when (getf ,opts ,option)
|
|
|
|
|
,body))
|
|
|
|
|
|
2024-05-26 17:35:11 -05:00
|
|
|
|
;; list symbol form form
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(defmacro if-opt (opts option if-form &optional else-form)
|
|
|
|
|
`(if (getf ,opts ,option)
|
2024-05-26 17:35:11 -05:00
|
|
|
|
,if-form ,else-form))
|
2019-11-18 23:38:26 -06:00
|
|
|
|
|
|
|
|
|
|
2024-05-26 17:35:11 -05:00
|
|
|
|
|
|
|
|
|
;;; Main - Invocation
|
|
|
|
|
;;; —————————————————————————————————————
|
|
|
|
|
;; nil → nil
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(defun invoke ()
|
|
|
|
|
"Actual invocation of the program. This is what you should set as :toplevel."
|
|
|
|
|
(multiple-value-bind (opts free) (opts:get-opts)
|
|
|
|
|
(when-opt opts :help (help))
|
|
|
|
|
(choose-text-table opts)
|
|
|
|
|
|
|
|
|
|
(let* ((input-stream (choose-input-stream free opts)))
|
|
|
|
|
(when (not input-stream)
|
|
|
|
|
(format *error-output* "eksd: No file provided and nothing piped.~%")
|
|
|
|
|
(help 2))
|
|
|
|
|
|
|
|
|
|
(choose-stream-position opts input-stream)
|
|
|
|
|
(reverse-or-dump opts input-stream)
|
|
|
|
|
|
|
|
|
|
(close input-stream))))
|
|
|
|
|
|
2024-05-26 17:35:11 -05:00
|
|
|
|
;; number stream → nil
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(defun help (&optional (exit-code 0) (stream *standard-output*))
|
|
|
|
|
"Prints help message and dies."
|
|
|
|
|
(unix-opts:describe :prefix "usage: eksd [-hr] [-t table-file] file"
|
|
|
|
|
:stream stream)
|
|
|
|
|
(unix-opts:exit exit-code))
|
|
|
|
|
|
2024-05-26 17:35:11 -05:00
|
|
|
|
; list stream → nil
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(defun reverse-or-dump (opts input-stream)
|
|
|
|
|
"Determine if a hexdump or reversal's necessary— and execute it."
|
|
|
|
|
(if-opt opts :reverse
|
2024-05-26 17:35:11 -05:00
|
|
|
|
(reverse-stream input-stream)
|
|
|
|
|
(apply #'print-stream (choose-pstream-args opts input-stream))))
|
2019-11-18 23:38:26 -06:00
|
|
|
|
|
|
|
|
|
|
2024-05-26 17:35:11 -05:00
|
|
|
|
|
|
|
|
|
;;; Input
|
|
|
|
|
;;; —————————————————————————————————————
|
|
|
|
|
;; list → stream
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(defun choose-input-stream (free-args opts)
|
|
|
|
|
"Select an input stream, between a file passed in free-args or stdin."
|
|
|
|
|
(let* ((input-file (car free-args))
|
|
|
|
|
(input-file-p (ignore-errors (probe-file input-file)))
|
|
|
|
|
(stdin-p (listen *standard-input*)))
|
2024-05-26 17:35:11 -05:00
|
|
|
|
(cond ((and (getf opts :reverse) input-file-p
|
|
|
|
|
(open input-file :direction :input :element-type 'character)))
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(input-file-p (open-byte input-file))
|
|
|
|
|
(stdin-p *standard-input*))))
|
|
|
|
|
|
2024-05-26 17:35:11 -05:00
|
|
|
|
;; list → nil
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(defun choose-text-table (opts)
|
|
|
|
|
"Choose the appropriate text-table— user-given or otherwise."
|
|
|
|
|
(if-opt opts :text-table
|
|
|
|
|
(setq *text-table* (parse-table-file (getf opts :text-table)))
|
|
|
|
|
(if-opt opts :ascii
|
2024-05-26 17:35:11 -05:00
|
|
|
|
(setq *text-table* eksd:*ascii-text-table*)
|
|
|
|
|
(setq *text-table* eksd:*fancy-text-table*))))
|
2019-11-18 23:38:26 -06:00
|
|
|
|
|
2024-05-26 17:35:11 -05:00
|
|
|
|
;; list stream → nil
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(defun choose-stream-position (opts stream)
|
|
|
|
|
"Choose the correct stream position— if seek arg used, etc."
|
|
|
|
|
(when (not (eq stream *standard-input*))
|
|
|
|
|
(if-opt opts :seek (file-position stream (getf opts :seek)))))
|
|
|
|
|
|
2024-05-26 17:35:11 -05:00
|
|
|
|
;; list stream → list
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(defun choose-pstream-args (opts input-stream)
|
|
|
|
|
"Take all options, and return the appropriate arguments to #'print-stream."
|
|
|
|
|
(let ((args (list input-stream)))
|
|
|
|
|
(when-opt opts :upcase (nconc args '(:upcase t)))
|
|
|
|
|
(when-opt opts :width (nconc args `(:width ,(getf opts :width))))
|
|
|
|
|
(when-opt opts :group (nconc args `(:group ,(getf opts :group))))
|
|
|
|
|
args))
|
|
|
|
|
|
2024-05-26 17:35:11 -05:00
|
|
|
|
|
|
|
|
|
;;; —————————————————
|
2019-11-18 23:38:26 -06:00
|
|
|
|
|
2024-05-26 17:35:11 -05:00
|
|
|
|
;; stream number number → list number
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(defun get-line-hex (stream index width)
|
|
|
|
|
"Return a line's worth of octets; and a new octet-index."
|
|
|
|
|
(values
|
2024-05-26 17:35:11 -05:00
|
|
|
|
(loop :while (listen stream) :for i :from 1 :to width
|
|
|
|
|
:collect (eksd:read-hex stream) :do (incf index))
|
|
|
|
|
index))
|
2019-11-18 23:38:26 -06:00
|
|
|
|
|
|
|
|
|
|
2024-05-26 17:35:11 -05:00
|
|
|
|
|
|
|
|
|
;; Output
|
2019-11-18 23:38:26 -06:00
|
|
|
|
;; —————————————————————————————————————
|
2024-05-26 17:35:11 -05:00
|
|
|
|
;; stream number number stream
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(defun print-stream (stream &key (width 16) (group 2) (upcase nil) (out 't))
|
|
|
|
|
"Print an entire stream in hex, xxd-style."
|
|
|
|
|
(let ((index 0))
|
|
|
|
|
(loop :while (listen stream)
|
|
|
|
|
:do (setq index (print-line stream :out out :index index
|
2024-05-26 17:35:11 -05:00
|
|
|
|
:group group :width width
|
|
|
|
|
:upcase upcase)))))
|
2019-11-18 23:38:26 -06:00
|
|
|
|
|
2024-05-26 17:35:11 -05:00
|
|
|
|
;; stream stream number number number → number
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(defun print-line (stream &key (out 't) (index 0) (width 16) (group 2)
|
2024-05-26 17:35:11 -05:00
|
|
|
|
(upcase nil))
|
2019-11-18 23:38:26 -06:00
|
|
|
|
"Print a given line of xxd-style output— index, bytes, preview and all.
|
|
|
|
|
Returns the new index of the stream."
|
|
|
|
|
(multiple-value-bind (hexes new-index) (get-line-hex stream index width)
|
|
|
|
|
(print-index index out)
|
|
|
|
|
(print-bytes (list-pad hexes width " ") group upcase out)
|
|
|
|
|
(print-preview hexes out)
|
|
|
|
|
(format t "~%")
|
|
|
|
|
new-index))
|
|
|
|
|
|
2024-05-26 17:35:11 -05:00
|
|
|
|
;; number stream → nil
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(defun print-index (index &optional (out 't))
|
|
|
|
|
"Print the current index, padded to 8 char-length and in hexadecimal."
|
|
|
|
|
(format out "~8,,,'0@A: " (string-downcase (eksd:integer-to-hex index))))
|
|
|
|
|
|
2024-05-26 17:35:11 -05:00
|
|
|
|
;; list-of-strings number stream → nil
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(defun print-bytes (hexes group-size upcase &optional (out 't))
|
|
|
|
|
"Print the given list of bytes on a line in specified-sized groupings."
|
|
|
|
|
(mapcar (lambda (group)
|
|
|
|
|
(format out (if upcase "~{~@:(~a~)~} " "~{~(~a~)~} ") group))
|
|
|
|
|
(pairs hexes group-size)))
|
|
|
|
|
|
2024-05-26 17:35:11 -05:00
|
|
|
|
;; list-of-strings stream → nil
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(defun print-preview (hexes &optional (out 't))
|
|
|
|
|
"Print a given list of bytes' preview, as per ASCII table."
|
|
|
|
|
(format out " ~{~A~}"
|
|
|
|
|
(mapcar (lambda (hex) (hex-to-char hex *text-table*)) hexes)))
|
|
|
|
|
|
|
|
|
|
|
2024-05-26 17:35:11 -05:00
|
|
|
|
|
|
|
|
|
;;; Reversal
|
|
|
|
|
;;; —————————————————————————————————————
|
|
|
|
|
;; stream stream → nil
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(defun reverse-stream (stream &optional (out *standard-output*))
|
|
|
|
|
"Take a stream of xxd-style/eksd-output hexcode and convert back into binary."
|
|
|
|
|
(loop :while (listen stream)
|
|
|
|
|
:do (mapcar (lambda (byte)
|
|
|
|
|
(write-byte (eksd:hex-to-integer byte) out))
|
|
|
|
|
(line-to-hexes (read-line stream)))))
|
|
|
|
|
|
2024-05-26 17:35:11 -05:00
|
|
|
|
;; string → list
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(defun line-to-hexes (line)
|
|
|
|
|
"Convert an xxd-style/eksd-output hexcode line into a list of hexcodes."
|
|
|
|
|
(mapcar (lambda (pair) (format nil "~{~A~}" pair))
|
2024-05-26 17:35:11 -05:00
|
|
|
|
(string-pairs
|
|
|
|
|
(remove #\space (car (cl-strings:split
|
|
|
|
|
(left-clip-string line ": ")" "))))))
|
2019-11-18 23:38:26 -06:00
|
|
|
|
|
|
|
|
|
|
2024-05-26 17:35:11 -05:00
|
|
|
|
|
|
|
|
|
;;; Text-tables
|
|
|
|
|
;;; —————————————————————————————————————
|
|
|
|
|
;; string → list
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(defun parse-table-line (string)
|
|
|
|
|
"Parse a text-table line into a pair of hex-code and preview character."
|
|
|
|
|
(let ((chars (char-list string)))
|
|
|
|
|
(list (format nil "~{~a~}" (list (car chars) (cadr chars)))
|
|
|
|
|
(tail chars))))
|
|
|
|
|
|
2024-05-26 17:35:11 -05:00
|
|
|
|
;; pathname → list
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(defun parse-table-file (pathname)
|
|
|
|
|
"Parse a text-table file (hexcode followed by preview character) into a list
|
|
|
|
|
of lists '(hexcode character)."
|
|
|
|
|
(with-open-file (istream pathname :direction :input :element-type 'character)
|
|
|
|
|
(loop :while (listen istream)
|
|
|
|
|
:collect (parse-table-line (read-line istream)))))
|
|
|
|
|
|
|
|
|
|
|
2024-05-26 17:35:11 -05:00
|
|
|
|
|
|
|
|
|
;;; Misc
|
|
|
|
|
;;; —————————————————————————————————————
|
|
|
|
|
;; list number varying → list
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(defun list-pad (list target-length &optional (padding nil))
|
|
|
|
|
"Pad a list out to length, by appending padding as necessary."
|
|
|
|
|
(if (not (eq target-length (length list)))
|
2024-05-26 17:35:11 -05:00
|
|
|
|
(list-pad (append list (list padding)) target-length padding)
|
|
|
|
|
list))
|
2019-11-18 23:38:26 -06:00
|
|
|
|
|
2024-05-26 17:35:11 -05:00
|
|
|
|
;; list number → list
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(defun pairs (list width &optional pairs)
|
|
|
|
|
"Split a list into pairs (sublist) of a given width."
|
|
|
|
|
(cond ((not list) pairs)
|
|
|
|
|
((or (eq width (length (tail pairs))) (not pairs))
|
|
|
|
|
(pairs (cdr list) width (nconc pairs `((,(car list))))))
|
|
|
|
|
((not (eq width (length (tail pairs))))
|
|
|
|
|
(pairs (cdr list) width
|
2024-05-26 17:35:11 -05:00
|
|
|
|
(nconc (de-tail pairs) `(,(nconc (tail pairs) `(,(car list)))))))))
|
2019-11-18 23:38:26 -06:00
|
|
|
|
|
2024-05-26 17:35:11 -05:00
|
|
|
|
;; string character → string
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(defun left-clip-string (string &optional (seperator #\space))
|
|
|
|
|
"Clip a string up to the first instance of the seperator."
|
|
|
|
|
(reduce (lambda (a b) (format nil "~A~A~A" a seperator b))
|
|
|
|
|
(cdr (cl-strings:split string seperator))))
|
|
|
|
|
|
2024-05-26 17:35:11 -05:00
|
|
|
|
;; string number → list
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(defun string-pairs (string &optional (pair-length 2))
|
|
|
|
|
"Return a list of characters from a string in pairs of given length."
|
|
|
|
|
(pairs (char-list string) pair-length))
|
|
|
|
|
|
2024-05-26 17:35:11 -05:00
|
|
|
|
;; pathname → stream
|
2019-11-18 23:38:26 -06:00
|
|
|
|
(defun open-byte (pathname)
|
|
|
|
|
"Open an input file as a byte-stream."
|
|
|
|
|
(open pathname :direction :input :element-type '(unsigned-byte 8)))
|
|
|
|
|
|
2024-05-26 17:35:11 -05:00
|
|
|
|
;; string → list
|
|
|
|
|
(defun char-list (string)
|
|
|
|
|
"Convert a string into a list of characters."
|
|
|
|
|
(loop :for char :across string :collect char))
|
|
|
|
|
|
|
|
|
|
;; list → list
|
|
|
|
|
(defun de-tail (list)
|
|
|
|
|
"Remove the last element from a list."
|
|
|
|
|
(reverse (cdr (reverse list))))
|
2019-11-18 23:38:26 -06:00
|
|
|
|
|