Skip to content

Instantly share code, notes, and snippets.

@renatoathaydes
Created August 17, 2024 19:03
Show Gist options
  • Save renatoathaydes/54a93e91cacfafd08ad875f11615fbc2 to your computer and use it in GitHub Desktop.
Save renatoathaydes/54a93e91cacfafd08ad875f11615fbc2 to your computer and use it in GitHub Desktop.
Parse AR Files
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
const arHeaderLength = 60;
class ArHeader {
/// File name.
final String file;
/// File modification timestamp (in seconds).
final int mod;
/// Owner ID.
final int owner;
/// Group ID.
final int group;
/// File mode (type and permission).
final Uint8List mode;
/// File size in bytes.
final int size;
/// In some cases, the real file name is included in the data section.
/// If that's the case, this value will be greater than 0 and indicates where the actual data starts.
final int dataStart;
const ArHeader({
required this.file,
required this.mod,
required this.owner,
required this.group,
required this.mode,
required this.size,
required this.dataStart,
});
@override
String toString() {
return 'ArHeader{file: $file, mod: $mod, owner: $owner, group: $group, mode: $mode, size: $size, dataStart: $dataStart}';
}
}
class ArException implements Exception {
final String reason;
const ArException(this.reason);
@override
String toString() {
return 'ArException{reason: $reason}';
}
}
Stream<ArHeader> parseAr(File file) async* {
final handle = await file.open();
try {
final magic = await handle.read(8);
if (magic.length != 8 || utf8.decode(magic) != '!<arch>\n') {
throw ArException('Not ar archive (does not start with magic header)');
}
final len = await handle.length();
while (await handle.position() < len) {
final header = await _parseHeader(await handle.read(arHeaderLength));
yield header;
await handle.skipFileEntry(header);
}
} finally {
await handle.close();
}
}
Future<ArHeader> _parseHeader(Uint8List bytes) async {
assert(bytes.length == arHeaderLength,
'Header length should be $arHeaderLength, got ${bytes.length}');
if (bytes.sublist(arHeaderLength - 2).isNotArHeaderEnding) {
throw ArException(
'Cannot recognize AR header (wrong ending chars): $bytes');
}
return ArHeader(
file: _str(bytes, 0, 16),
mod: int.parse(_str(bytes, 16, 28)),
owner: int.parse(_str(bytes, 28, 34)),
group: int.parse(_str(bytes, 34, 40)),
mode: bytes.sublist(40, 48),
size: int.parse(_str(bytes, 48, 58)),
dataStart: 0);
}
String _str(Uint8List bytes, int start, int end, {bool trim = true}) {
final value = utf8.decode(bytes.sublist(start, end));
if (trim) {
return value.trim();
}
return value;
}
extension on RandomAccessFile {
Future<void> skipFileEntry(ArHeader header) async {
final currentPos = await position();
await setPosition(currentPos + header.size);
}
}
extension on Uint8List {
bool get isNotArHeaderEnding =>
length != 2 || this[0] != 0x60 || this[1] != 0x0A;
}
(defpackage #:ar
(:use :cl)
(:export #:ar-header #:parse-ar-file))
(in-package #:ar)
(defstruct ar-header
name mod owner group mode size)
(defparameter +magic-ar-header+
(concatenate 'string "!<arch>" '(#\linefeed)))
(defun parse-ar-file (file handle-header)
(with-open-file (st file :element-type '(unsigned-byte 8))
(let* ((bytes (read-n-bytes 8 st))
(file-len (file-length st))
(fst-8-bytes (to-str bytes)))
(if (string= fst-8-bytes +magic-ar-header+)
(for-each-ar-header st file-len handle-header)
(error "not AR file")))))
(defun for-each-ar-header (stream file-len handle-header)
(loop
while (< (file-position stream) file-len) do
(let ((header-bytes (read-n-bytes 60 stream)))
(case (length header-bytes)
(1 ;; EOF for some reason still emits last byte
(format t "done with 1 byte! ~%"))
(60 ;; ok
(let ((header (parse-header header-bytes)))
(funcall handle-header header)
(skip-file stream header)))
(t (error (format nil "AR file is invalid: ~a" header-bytes)))))))
(defun parse-header (bytes)
(assert (equalp (subseq bytes 58 60) #(#x60 #x0A)) nil "unexpected AR header ending")
(make-ar-header
:name (string-trim '(#\space) (to-str (subseq bytes 0 16)))
:mod (to-long (subseq bytes 16 28))
:owner (to-long (subseq bytes 28 34))
:group (to-long (subseq bytes 34 40))
:mode (subseq bytes 40 48)
:size (to-long (subseq bytes 48 58))))
(defun skip-file (stream header)
(let ((curr-pos (file-position stream))
(entry-size (ar-header-size header)))
(file-position stream (+ curr-pos entry-size))))
(defun to-str (bytes)
(map 'string #'code-char bytes))
(defun to-long (bytes)
(parse-integer (to-str bytes)))
(defun read-n-bytes (n stream)
(let* ((arr (make-array
n
:element-type '(unsigned-byte 8)))
(count (read-sequence arr stream)))
(subseq arr 0 count)))
(defpackage #:ar-main
(:use :cl :ar)
(:export #:main))
(in-package #:ar-main)
(defun handle-header (header)
(format t "~a~%" header))
(defun main ()
(let ((args (cdr sb-ext:*posix-argv*)))
(if (eq (length args) 1)
(ar:parse-ar-file (first args) #'handle-header)
(error "expected a single argument"))))
;; to compile the ar.lisp file using SBCL,
;; run this file like this:
;; sbcl --script build.lisp
(load "ar.lisp")
(sb-ext:save-lisp-and-die
#P"lisp-ar"
:toplevel #'ar-main:main
:executable t
:compression t)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment