Add bounded raw file disassembly

ober

0f6f582e524d139c380454bb21756fef8c5e6a96

diff --git a/.gitignore b/.gitignore
index df31ea9..c497728 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ scheme_boot.h
 program_boot.h
 .jasm-sample.o
 .jasm-test-sample.o
+.jasm-raw.bin
diff --git a/Makefile b/Makefile
index 1c7de7f..9eb1814 100644
--- a/Makefile
+++ b/Makefile
@@ -24,14 +24,16 @@ test-binary: binary
 	file ./$(BINARY)
 	./$(BINARY) raw --arch x86-64 --syntax intel --hex '55 48 89 e5 c3'
 	./$(BINARY) raw --arch aarch64 --hex '20 00 80 d2 c0 03 5f d6'
+	$(SCHEME) --libdirs $(LIBDIRS) --script tests/write-raw-bytes.ss ./.jasm-raw.bin
+	./$(BINARY) raw --arch x86-64 --syntax intel --max-bytes 2 --file ./.jasm-raw.bin
 	$(SCHEME) --libdirs $(LIBDIRS) --script tests/write-sample-elf.ss ./.jasm-sample.o
 	./$(BINARY) headers ./.jasm-sample.o
 	./$(BINARY) sections ./.jasm-sample.o
 	./$(BINARY) symbols ./.jasm-sample.o
 	./$(BINARY) relocs ./.jasm-sample.o
 	./$(BINARY) object --section .text --syntax intel ./.jasm-sample.o
-	rm -f ./.jasm-sample.o
+	rm -f ./.jasm-sample.o ./.jasm-raw.bin
 
 clean:
-	rm -f $(BINARY) $(BINARY).wp.so $(BINARY)-main.c petite_boot.h scheme_boot.h program_boot.h .jasm-sample.o .jasm-test-sample.o
+	rm -f $(BINARY) $(BINARY).wp.so $(BINARY)-main.c petite_boot.h scheme_boot.h program_boot.h .jasm-sample.o .jasm-test-sample.o .jasm-raw.bin
 	find . \( -name '*.so' -o -name '*.wpo' \) -print0 | xargs -0 rm -f
diff --git a/docs/plan.md b/docs/plan.md
index 9ff6fe4..7a347a8 100644
--- a/docs/plan.md
+++ b/docs/plan.md
@@ -46,6 +46,8 @@ reference material and parity targets.
 - The `jasm` binary has initial ELF-oriented `object`, `headers`, and
   `sections`, `symbols`, and `relocs` commands. `object` disassembles
   executable sections or a selected section by name.
+- Raw CLI mode accepts byte input from `--hex` or `--file`. The reusable
+  bytevector API supports `max-bytes` and `stop-address` limits.
 
 ## Core Architecture
 
@@ -121,8 +123,8 @@ Options should include:
 - `syntax`
 - `features`
 - `cpu`
-- `max-bytes`
-- `stop-address`
+- `max-bytes` (implemented)
+- `stop-address` (implemented)
 - `show-bytes?`
 - `show-addresses?`
 - `unknown-policy`: emit `.byte`, error, or skip
@@ -135,7 +137,7 @@ The binary should eventually support:
 
 ```sh
 jasm raw --arch x86-64 --syntax intel --hex '55 48 89 e5'
-jasm raw --arch aarch64 --file code.bin --address 0x1000
+jasm raw --arch aarch64 --file code.bin --address #x1000
 jasm -d file.o
 jasm -D file.o
 jasm --headers file.o
diff --git a/jasm b/jasm
index f930e2b..31f1e00 100755
Binary files a/jasm and b/jasm differ
diff --git a/lib/jasm/disasm.ss b/lib/jasm/disasm.ss
index 816baed..9b91d84 100644
--- a/lib/jasm/disasm.ss
+++ b/lib/jasm/disasm.ss
@@ -571,11 +571,20 @@
     (let* ([arch (normalize-architecture arch)]
            [options (if (null? maybe-options) '() (car maybe-options))]
            [base (option-ref options 'address 0)]
-           [len (bytevector-length bv)])
+           [max-bytes (option-ref options 'max-bytes #f)]
+           [stop-address (option-ref options 'stop-address #f)]
+           [len (if max-bytes
+                    (min (bytevector-length bv) max-bytes)
+                    (bytevector-length bv))])
+      (when (and max-bytes (< max-bytes 0))
+        (error 'disassemble-bytevector "max-bytes must be non-negative" max-bytes))
       (let loop ([offset 0] [address base] [out '()])
-        (if (>= offset len)
+        (if (or (>= offset len)
+                (and stop-address (>= address stop-address)))
             (reverse out)
             (let ([insn (decode-instruction arch bv offset address options)])
+              (when (<= (instruction-size insn) 0)
+                (error 'disassemble-bytevector "decoder returned a zero-sized instruction" insn))
               (loop (+ offset (instruction-size insn))
                     (+ address (instruction-size insn))
                     (cons insn out)))))))
diff --git a/main.ss b/main.ss
index c8fe211..b671c8d 100644
--- a/main.ss
+++ b/main.ss
@@ -4,7 +4,7 @@
 
 (define (usage)
   (display "Usage:\n")
-  (display "  jasm raw --arch <x86-64|aarch64|mips|mipsel|mips64|mips64el> [--syntax att|intel] [--address n] --hex <bytes>\n")
+  (display "  jasm raw --arch <x86-64|aarch64|mips|mipsel|mips64|mips64el> [--syntax att|intel] [--address n] [--max-bytes n] [--stop-address n] (--hex <bytes>|--file path)\n")
   (display "  jasm object [--arch <arch>] [--syntax att|intel] [--section name] <file>\n")
   (display "  jasm headers <file>\n")
   (display "  jasm sections <file>\n")
@@ -20,10 +20,19 @@
   (normalize-architecture s))
 
 (define (parse-number s)
-  (let ([n (string->number s 0)])
+  (let ([n (string->number s)])
     (unless n (error 'jasm "invalid number" s))
     n))
 
+(define (read-file-bytevector path)
+  (let ([port (open-file-input-port path)])
+    (let ([bytes (get-bytevector-all port)])
+      (close-port port)
+      bytes)))
+
+(define (option-cons key value options)
+  (if value (cons (cons key value) options) options))
+
 (define (ascii-downcase c)
   (if (and (char>=? c #\A) (char<=? c #\Z))
       (integer->char (+ (char->integer #\a)
@@ -164,25 +173,41 @@
              [arch #f]
              [syntax 'att]
              [address 0]
-             [hex #f])
+             [hex #f]
+             [file #f]
+             [max-bytes #f]
+             [stop-address #f])
     (cond
       [(null? args)
        (unless arch (error 'jasm "missing --arch"))
-       (unless hex (error 'jasm "missing --hex"))
-       (let ([opts (list (cons 'syntax syntax) (cons 'address address))])
-         (display (disassemble->string arch (hex-string->bytevector hex) opts)))]
+       (when (and hex file) (error 'jasm "use either --hex or --file, not both"))
+       (unless (or hex file) (error 'jasm "missing --hex or --file"))
+       (let* ([opts (option-cons 'stop-address stop-address
+                      (option-cons 'max-bytes max-bytes
+                        (list (cons 'syntax syntax) (cons 'address address))))]
+              [bytes (if hex (hex-string->bytevector hex) (read-file-bytevector file))])
+         (display (disassemble->string arch bytes opts)))]
       [(string=? (car args) "--arch")
        (when (null? (cdr args)) (error 'jasm "--arch requires a value"))
-       (loop (cddr args) (string->arch (cadr args)) syntax address hex)]
+       (loop (cddr args) (string->arch (cadr args)) syntax address hex file max-bytes stop-address)]
       [(string=? (car args) "--syntax")
        (when (null? (cdr args)) (error 'jasm "--syntax requires a value"))
-       (loop (cddr args) arch (parse-syntax (cadr args)) address hex)]
+       (loop (cddr args) arch (parse-syntax (cadr args)) address hex file max-bytes stop-address)]
       [(string=? (car args) "--address")
        (when (null? (cdr args)) (error 'jasm "--address requires a value"))
-       (loop (cddr args) arch syntax (parse-number (cadr args)) hex)]
+       (loop (cddr args) arch syntax (parse-number (cadr args)) hex file max-bytes stop-address)]
+      [(string=? (car args) "--max-bytes")
+       (when (null? (cdr args)) (error 'jasm "--max-bytes requires a value"))
+       (loop (cddr args) arch syntax address hex file (parse-number (cadr args)) stop-address)]
+      [(string=? (car args) "--stop-address")
+       (when (null? (cdr args)) (error 'jasm "--stop-address requires a value"))
+       (loop (cddr args) arch syntax address hex file max-bytes (parse-number (cadr args)))]
       [(string=? (car args) "--hex")
        (when (null? (cdr args)) (error 'jasm "--hex requires a value"))
-       (loop (cddr args) arch syntax address (cadr args))]
+       (loop (cddr args) arch syntax address (cadr args) file max-bytes stop-address)]
+      [(string=? (car args) "--file")
+       (when (null? (cdr args)) (error 'jasm "--file requires a value"))
+       (loop (cddr args) arch syntax address hex (cadr args) max-bytes stop-address)]
       [else (error 'jasm "unknown raw option" (car args))])))
 
 (define (parse-object args)
diff --git a/tests/test-jasm.ss b/tests/test-jasm.ss
index 23f4d0f..7d9707d 100644
--- a/tests/test-jasm.ss
+++ b/tests/test-jasm.ss
@@ -199,6 +199,20 @@
           (instruction-text insn)))
   '(#t x86-64 1 "pushq\t%rbp"))
 
+(test "disassemble max bytes"
+  (map instruction-text
+       (disassemble-bytevector 'x86-64
+         (hex-string->bytevector "55 c3 90")
+         '((syntax . intel) (max-bytes . 2))))
+  '("push\trbp" "ret"))
+
+(test "disassemble stop address"
+  (map instruction-address
+       (disassemble-bytevector 'x86-64
+         (hex-string->bytevector "55 c3 90")
+         '((address . #x1000) (stop-address . #x1001))))
+  '(4096))
+
 (test "x86 push att"
   (one-text 'x86-64 "55")
   "pushq\t%rbp")
diff --git a/tests/write-raw-bytes.ss b/tests/write-raw-bytes.ss
new file mode 100644
index 0000000..0e64f17
--- /dev/null
+++ b/tests/write-raw-bytes.ss
@@ -0,0 +1,19 @@
+#!chezscheme
+
+(import (chezscheme))
+
+(define (write-bytevector-file path bv)
+  (when (file-exists? path)
+    (delete-file path))
+  (let ([port (open-file-output-port path (file-options no-fail))])
+    (put-bytevector port bv)
+    (close-port port)))
+
+(let ([args (command-line-arguments)])
+  (unless (= (length args) 1)
+    (error 'write-raw-bytes "usage: write-raw-bytes <path>"))
+  (let ([bv (make-bytevector 3 0)])
+    (bytevector-u8-set! bv 0 #x55)
+    (bytevector-u8-set! bv 1 #xc3)
+    (bytevector-u8-set! bv 2 #x90)
+    (write-bytevector-file (car args) bv)))