Created
August 17, 2024 19:03
-
-
Save renatoathaydes/54a93e91cacfafd08ad875f11615fbc2 to your computer and use it in GitHub Desktop.
Parse AR Files
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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")))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;; 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