Put tape utilities under tape/ with .gitignore for the data, update search path
authorNick Downing <nick@ndcode.org>
Sat, 12 Oct 2019 22:50:01 +0000 (09:50 +1100)
committerNick Downing <nick@ndcode.org>
Sat, 12 Oct 2019 23:16:40 +0000 (10:16 +1100)
.gitignore
multics_sim.c
tape/od.sh [new file with mode: 0755]
tape/tape.py [new file with mode: 0755]
tape/unarchive.py [new file with mode: 0755]
tape/unarchive_all.sh [new file with mode: 0755]

index 60da34e..a3a29dc 100644 (file)
@@ -1,2 +1,5 @@
 *.o
 /multics_sim
+/tape/*.tap
+/tape/char
+/tape/word
index c10633e..ae69d7a 100644 (file)
@@ -13,7 +13,7 @@
 
 #define N_PATHS 1
 char *paths[] = {
-  "../tape/word/system_library_standard/"
+  "tape/word/system_library_standard/"
 };
 
 int main(int argc, char **argv) {
diff --git a/tape/od.sh b/tape/od.sh
new file mode 100755 (executable)
index 0000000..b201626
--- /dev/null
@@ -0,0 +1,2 @@
+#!/bin/sh
+od -t o8 -w32 $@ |sed -e 's/^\([0-7]*\)0/\1:/; s/ 0000000000/ /g'
diff --git a/tape/tape.py b/tape/tape.py
new file mode 100755 (executable)
index 0000000..26d01a2
--- /dev/null
@@ -0,0 +1,181 @@
+#!/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))
diff --git a/tape/unarchive.py b/tape/unarchive.py
new file mode 100755 (executable)
index 0000000..8183d59
--- /dev/null
@@ -0,0 +1,33 @@
+#!/usr/bin/env python3
+
+import sys
+
+dir = sys.argv[2] + '/' if len(sys.argv) >= 3 else ''
+with open(sys.argv[1], 'rb') as fin:
+  for line in fin:
+    fields = (
+      line[:-1].
+        replace(b'\0', b'').
+        replace(b'\n', b'').
+        replace(b'\f', b'').
+        replace(b'\x0f', b'').
+        split()
+    )
+    if len(fields):
+      assert len(fields) >= 5
+      file = str(fields[0], 'ascii')
+      print('file', file)
+      bitcount = int(fields[-1])
+      print('bitcount', bitcount)
+      assert bitcount % 9 == 0
+      size = bitcount // 9
+
+      assert fin.readline() == b'\n'
+      assert fin.readline() == b'\n'
+      assert fin.readline() == b'\n'
+      data = fin.read(size)
+      assert len(data) == size
+   
+      file = file.replace('/', '_') 
+      with open(dir + file, 'wb') as fout:
+        fout.write(data)
diff --git a/tape/unarchive_all.sh b/tape/unarchive_all.sh
new file mode 100755 (executable)
index 0000000..a953124
--- /dev/null
@@ -0,0 +1,11 @@
+#!/bin/sh
+for i in `find char -name '*.archive' -print`
+do
+  echo $i
+  mkdir ${i}__temp__
+  if ./unarchive.py $i ${i}__temp__
+  then
+    rm $i
+    mv ${i}__temp__ $i
+  fi
+done