-
Notifications
You must be signed in to change notification settings - Fork 0
/
eksd-unix.lisp
278 lines (227 loc) · 10.2 KB
/
eksd-unix.lisp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
;; This file is free software: you can redistribute it and/or modify
;; it under the terms of version 3 of the GNU General Public License
;; as published by the Free Software Foundation.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
(defpackage :eksd-unix
(:use :cl :eksd)
(:export :invoke
*text-table*))
(in-package :eksd-unix)
;; —————————————————————————————————————
(opts:define-opts
(:name :help
:description "print this help text."
:short #\h :long "help")
(:name :reverse
:description "reverse operation: convert hexdump into binary."
:short #\r :long "reverse")
(:name :upcase
:description "print hexadecimal in uppercase."
:short #\u :long "upcase")
(:name :width
:description "octets per line; 16 as default."
:short #\c :long "cols"
:arg-parser #'parse-integer
:meta-var "COLS")
(:name :seek
:description "skip given amount of bytes in file."
:short #\s :long "seek"
:arg-parser #'parse-integer
:meta-var "OFFSET")
(:name :group
:description "octets per group; 2 as default."
:short #\g :long "groupsize"
:arg-parser #'parse-integer
:meta-var "OCTETS")
(:name :ascii
:description "use simple-ascii for previews; default is fun."
:short #\a :long "ascii")
(:name :text-table
:description "specify a text-table; semi-ascii as default."
:short #\t :long "table"
:arg-parser #'probe-file
:meta-var "TABLE"))
;; —————————————————————————————————————
;; MACROS
;; LIST SYMBOL FORM
(defmacro when-opt (opts option body)
`(when (getf ,opts ,option)
,body))
;; LIST SYMBOL FORM FORM
(defmacro if-opt (opts option if-form &optional else-form)
`(if (getf ,opts ,option)
,if-form ,else-form))
;; —————————————————————————————————————
;; MAIN - INVOCATION
;; NIL → NIL
(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))))
;; NUMBER STREAM → NIL
(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))
;; —————————————————
;; LIST STREAM → NIL
(defun reverse-or-dump (opts input-stream)
"Determine if a hexdump or reversal's necessary— and execute it."
(if-opt opts :reverse
(reverse-stream input-stream)
(apply #'print-stream (choose-pstream-args opts input-stream))))
;; —————————————————————————————————————
;; INPUT
;; LIST → STREAM
(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*)))
(cond ((and (getf opts :reverse) input-file-p)
(open input-file :direction :input :element-type 'character))
(input-file-p (open-byte input-file))
(stdin-p *standard-input*))))
;; LIST → NIL
(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
(setq *text-table* eksd:*ascii-text-table*)
(setq *text-table* eksd:*fancy-text-table*))))
;; LIST STREAM → NIL
(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)))))
;; LIST STREAM → LIST
(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))
;; —————————————————
;; STREAM NUMBER NUMBER → LIST NUMBER
(defun get-line-hex (stream index width)
"Return a line's worth of octets; and a new octet-index."
(values
(loop :while (listen stream) :for i :from 1 :to width
:collect (eksd:read-hex stream) :do (incf index))
index))
;; —————————————————————————————————————
;; OUTPUT
;; STREAM NUMBER NUMBER STREAM
(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
:group group :width width
:upcase upcase)))))
;; STREAM STREAM NUMBER NUMBER NUMBER → NUMBER
(defun print-line (stream &key (out 't) (index 0) (width 16) (group 2)
(upcase nil))
"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))
;; NUMBER STREAM → NIL
(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))))
;; LIST_OF_STRINGS NUMBER STREAM → NIL
(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)))
;; LIST_OF_STRINGS STREAM → NIL
(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)))
;; —————————————————————————————————————
;; REVERSAL
;; STREAM STREAM → NIL
(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)))))
;; STRING → LIST
(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))
(string-pairs
(remove #\space (car (cl-strings:split
(left-clip-string line ": ")" "))))))
;; —————————————————————————————————————
;; TEXT-TABLES
;; STRING → LIST
(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))))
;; PATHNAME → LIST
(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)))))
;; —————————————————————————————————————
;; MISC
;; LIST NUMBER VARYING → LIST
(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)))
(list-pad (append list (list padding)) target-length padding)
list))
;; LIST NUMBER → LIST
(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
(nconc (de-tail pairs) `(,(nconc (tail pairs) `(,(car list)))))))))
;; STRING CHARACTER → STRING
(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))))
;; STRING NUMBER → LIST
(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))
;; PATHNAME → STREAM
(defun open-byte (pathname)
"Open an input file as a byte-stream."
(open pathname :direction :input :element-type '(unsigned-byte 8)))
;; —————————————————
(defun char-list (string) (loop :for char :across string :collect char))
(defun de-tail (list) (reverse (cdr (reverse list))))
(defun tail (list) (car (last list)))