--- /dev/null
+#!/usr/bin/env python3
+
+import os
+import sys
+
+do_od = False
+if len(sys.argv) >= 2 and sys.argv[1] == '--od':
+ do_od = True
+ del sys.argv[1]
+
+index = 0
+def read_block(fin):
+ global index
+
+ header = fin.read(4)
+ while len(header):
+ assert len(header) == 4
+ size = int.from_bytes(header, 'little')
+ if size == 0:
+ print('mark')
+ elif size == 0xffffffff:
+ print('eof')
+ else:
+ print('block', hex(size))
+ block = fin.read(size)
+ assert len(block) == size
+ n_lines = (len(block) + 17) // 18
+ words = []
+ for i in range(0, len(block), 9):
+ if len(block) >= i + 5:
+ words.append(
+ int.from_bytes(block[i:i + 5], 'big') >> 4
+ )
+ if len(block) >= i + 9:
+ words.append(
+ int.from_bytes(block[i + 4:i + 9], 'big') & 0xfffffffff
+ )
+ footer = fin.read(4)
+ assert len(footer) == 4
+ assert int.from_bytes(footer, 'little') == size
+
+ assert len(words) == 0o2020
+ assert words[0] == 0o670314355245
+ if words[3] != index:
+ index = (index & 0o777777) + 1
+ assert words[3] == index
+ index += 0o1000000
+ assert words[7] == 0o512556146073
+ assert words[0o2010] == 0o107463422532
+ assert words[0o2017] == 0o265221631704
+
+ return words, bytes(
+ [
+ (i >> j) & 0xff
+ for i in words
+ for j in [27, 18, 9, 0]
+ ]
+ )
+ header = fin.read(4)
+ return None, None
+
+with open(sys.argv[1], 'rb') as fin:
+ if do_od:
+ words, chars = read_block(fin)
+ while words is not None:
+ for i in range(0, len(words), 4):
+ print(
+ format(i, '06o') + ':',
+ ' '.join(
+ [
+ format(words[j], '012o') if j < len(words) else ' '
+ for j in range(i, i + 4)
+ ]
+ ),
+ ''.join(
+ [
+ (
+ (
+ chr(chars[j])
+ if chars[j] >= 0x20 and chars[j] < 0x7e else
+ '.'
+ )
+ if j < len(chars) else
+ ''
+ )
+ for j in range(i * 4, i * 4 + 16)
+ ]
+ )
+ )
+ words, chars = read_block(fin)
+ sys.exit(0)
+
+ words, chars = read_block(fin)
+ assert chars[0o10 * 4:0o20 * 4] == b'Installation and location '
+
+ index = 1 # block, file
+ words, chars = read_block(fin)
+ while words is not None:
+ assert (
+ chars[0o20 * 4:0o36 * 4] ==
+ b'This is the beginning of a backup logical record. '
+ )
+ size = words[0o47]
+ print('size', oct(size))
+ dir = str(chars[0o51 * 4:0o51 * 4 + words[0o50]], 'ascii')
+ print('dir', dir)
+ file = str(chars[0o124 * 4:0o124 * 4 + words[0o123]], 'ascii')
+ print('file', file)
+ bitcount = words[0o134]
+ print('bitcount', oct(bitcount))
+
+ # record consists of header then unknown size of ACLs and similar, then
+ # the data which appears to be on a 0x400 word boundary, then padding of
+ # all -1's until the next block boundary (EOF or start of a new record),
+ # can't calculate size of ACLs so we punt on resynchronizing to the next
+ # record, removing trailing blocks of 0x400 -1's and working back by size
+ # note: end of tape record is a block of -1's so it naturally disappears
+ data = words[0o410:0o2010]
+ words, chars = read_block(fin)
+ while (
+ words is not None and
+ chars[0o20 * 4:0o36 * 4] !=
+ b'This is the beginning of a backup logical record. '
+ ):
+ data.extend(words[0o10:0o2010])
+ words, chars = read_block(fin)
+ print('done')
+
+ # do not write zero length files, as they might be directories
+ if size:
+ while all([i == 0o777777777777 for i in data[-0o400:]]):
+ data = data[:-0o400]
+ if len(data) < size:
+ print('short file', oct(len(data)), 'should be', size)
+ if words is None:
+ break
+ assert False
+ data = data[-size:]
+
+ unix_dir = dir.replace('/', '_').replace('>', '/')
+ unix_file = file.replace('/', '_')
+
+ # write word-oriented copy of the file (exact representation)
+ path_word = 'word' + unix_dir
+ os.makedirs(path_word, exist_ok = True)
+ with open(path_word + '/' + unix_file, 'wb') as fout:
+ fout.write(b''.join([i.to_bytes(8, 'little') for i in data]))
+
+ # write bitcount to an index in the directory containing the file
+ path_word_dir = path_word + '/.dir'
+ path_word_dir_temp = path_word + '/.dir.temp'
+ files = {}
+ try:
+ with open(path_word_dir, 'r') as fdir:
+ for line in fdir:
+ fields = line.split()
+ files[fields[0]] = fields[1]
+ except IOError:
+ pass
+ files[unix_file] = str(bitcount)
+ with open(path_word_dir_temp, 'w') as fdir:
+ for i in sorted(files.items()):
+ fdir.write(' '.join(i) + '\n')
+ try:
+ os.unlink(path_word_dir)
+ except IOError:
+ pass
+ os.rename(path_word_dir_temp, path_word_dir)
+
+ # write char-oriented copy of the file (approximate representation)
+ if bitcount % 9 == 0 and bitcount <= size * 36:
+ text = [
+ (i >> j) & 0x1ff
+ for i in data
+ for j in [27, 18, 9, 0]
+ ][:bitcount // 9]
+ if all([i < 0x100 for i in text]):
+ path_char = 'char' + unix_dir
+ os.makedirs(path_char, exist_ok = True)
+ with open(path_char + '/' + unix_file, 'wb') as fout:
+ fout.write(bytes(text))