NOS Disk I/O (nosio)

Module for NOS device I/O

Copyright (c) 2021-2024 Steve Zoppi

This is licensed software a copy of the license should have been included in the source tree for this module. You may find a copy of the license here: https://www.gnu.org/licenses/gpl-3.0.en.html

This program operates on PLATO packs and master files. Packs are accessed as DtCyber disk images. Master files (CDC style virtual packs) can be accessed directly (if you have a master file image copy available) or as NOS files contained in a NOS disk image.

<Documentation_reference>

When accessing PLATO file structures, the host pack object needs to be instantiated and then the PlatoFile class may be used to access the underlying structure.

class nosio.BasicFileDevice(fn, mode='rb')[source]

Bases: object

Base class for NOS devices.

This class encapsulates a host-based file and presents it as a NOS-type device for additional decoration by classes deriving from this one.

Parameters:
  • fn (string) – File Name

  • mode (string) – IO Mode, permitted values ‘rb’ ‘r+b’

f

Container File Object.

Type:

object

fs

File Status Structure Object of Container file upon opening.

Type:

object

mode

The device’s IO mode as requested upon instantiation

Type:

str

close() None[source]
seek(n) None[source]
class nosio.ControlBlock(cb_format: tuple, dblock: list)[source]

Bases: object

Parse Any Control Block into Named Properties

The decomposition of the control block enables bidirectional (get/set) manipulation of properties of a control block by name. In effect, this enables a dynamic “class-like” implementation without having to write explicit getters and setters for each property.

Warning

Control Block definitions required that ALL property names be unique. This is checked during class construction.

Structure:

The primary structure is a tuple composed of
an integer and a dictionary object.

tuple:
( min_length, { structure_descriptor, ... } )

Structure Descriptor:

Structure Descriptors are one of three formats

Level One:  (Whole-word descriptor)

    "property_name": word_offset(,)
or
    "property_name": word_offset, word_count(,)

Level Two:  (Bit-segment descriptor)

    "property_name": word_offset, {
        <bit_dictionary>
        })(,)

<bit_dictionary> :=
    "property_name": bit_count_of_property(,)

Note

bit_count_of_property counts bits from the left.

dblock

Data Block

Type:

list

min_words

Minimum Number of words in Data Block list.

Type:

int

prop_name_maxlength

Maximum character count of property name.

Type:

int

properties

Dictionary of descriptors.

Type:

dict

proplist

List of all properties in this control block.

Type:

list

title

Default Title String for this control block.

Type:

str

Parameters
cb_format (tuple):

Format specification for the data words supplied in dblock

dblock (list):

List of words that are described by cb_format

dump_propertybag(file=None) None[source]
get_propertybag(itemlist=None) dict[source]

Manipulates the property bag of the stored control block.

If itemlist is not specified, all properties are returned in a dictionary object with a key and value of the following types:

If the value of the item is a whole word or more than one whole
``cmword``, the value returned is a list object containing one or more
``cmwords``.

If the value of the item to be returned is a ``bit`` value, a
single integer value is returned as the key's value.

If ``itemlist`` is specified, ONLY the items named in the ``itemlist`` are
returned in the key/value dictionary.
set_propertybag(valuedict: dict) None[source]

Modifies the contents of a control block by property name.

Parameters:

valuedict (dict) – Dictionary of “key/value” pairs.

The control block’s keys are searched using the mapping provided in the resident control block. Once a key is found, the transform is completed in the locally cached image of the control block.

class nosio.DiskDevice(fn, mode='rb')[source]

Bases: BasicFileDevice

Basic disk.

cw1

Control Word (12-bit value)

Type:

int

cw2

Control Word (12-bit value)

Type:

int

wordstyle
Type:

int

ddevtype

2-character identifier of device type (detected)

Type:

str

ddevmodel

Descriptor of the detected device model

Type:

str

GEOM_BY_SIZE = {95956992: ['di', 0, 'dd844-2(1) packed/new', 512, 64], 120695904: ['di', 1, 'dd844-2(1) unpacked/classic', 644, 64], 192147456: ['dj', 0, 'dd844-4(1/4) packed/new', 512, 64], 241685472: ['dj', 1, 'dd844-4(1/4) unpacked/classic', 644, 64], 552468480: ['dm', 0, 'dd885-(11/12) packed/new (dm or dq)', 512, 64], 554626560: ['db', 2, 'dd885-42 unpacked (db)', 2056, 256], 694901760: ['dm', 1, 'dd885-(11/12) unpacked/classic (dm or dq)', 644, 64]}
SECTOR_PAD = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
SECTOR_SIZE_PACKED = 512
SECTOR_SIZE_UNPACKED = 644
SECTOR_SIZE_UNPACKED_DB = 2056
WORD_STYLE_PACKED = 0
WORD_STYLE_Q64 = 2
WORD_STYLE_UNPACKED = 1
phy_sector_read(sec) tuple[source]

Read data from a disk image at the given sector number.

Returns:

list

List Contents::

A full-sector (depending on the device) of 60-bit words plus the two (or four) control words (from the start of the sector) which are 12 bit values.

Generally:

Each sector is 512 bytes * 8 bits per byte = 4096 bits

4096 bits = 68 CM words (60-bits) with a remainder of 16 bit (2 bytes).

However, the used sector format is:

[12-bits] cw1                   12 +
[12-bits] cw2                   12 = 24 bits or 3 8-bit bytes
[64 CM Words @ 60-bits each]  3840 (same as an NOS-I Block)
----------------------------------
                              3864
                               232 remaining (29 8-bit bytes)
SECTOR_PAD is exactly 29 bytes

On “db” type devices:

One CM word is stored per 8 bytes (4 bits unused) Each disk sector contains 4, 64-word entries (logical sector) Each disk sector is 256 * 8 bytes = 2048 bytes

PLUS 2, 2-byte PPWords containing Control Bytes PLUS 2, 2-byte PPWords containing Extended Mem Address = 2048 + 8 = 2056 bytes per cluster of old sectors

phy_sector_write(sec, data) None[source]

Write the specified control words (two 12-bit values) and data (a sequence of exactly 64 60-bit values) at the specified sector offset.

This performs I/O for “Container” (i.e. “Physical”) sectors.

class nosio.NosCatEntry(fn: str, ui: int, ent: tuple, flen: int, ftrk: int, fsec: int, daf: str, devnum: int)[source]

Bases: object

class nosio.NosDiskDevice(fn: Path | str, mode: str = 'rb')[source]

Bases: DiskDevice

NOS file structured disk.

DISK SECTOR

Every sector, from a user standpoint, contains up to 64 (100B) CM words. However, the system always prefixes the sector with two header bytes (24 bits). These two header bytes contain file Linkage and other information. The general format of a disk sector is as follows.

header

byte 1

header

byte 2

from 0 —1008 CM

words of data

There are five types of sectors known to the system and labeled via the header bytes. These are:

  • End—of—record (EOR) sector

  • End—of—file (EOF) sector

  • End—of—information (EOI) sector

  • System sector

  • Full sector

Header byte 2 contains a word count of the number of CM words within the sector as written by the user. The word count (WC) is in the range 0 to 1008. If the word count equals 1008, the sector is full. If the word count is Less than 1006, the sector is called a short PRU and indicates an EOR. Table 7-2 shows the relationship between the sector types and the contents of the header bytes.

Sector Type
Header Byte 1
Header Byte 2

Comment

EOR
Next Sector/Track

0 < WC < 100B May contain data

EOF
Next Sector/Track

No data

EOI
0
0

No data

System
3777B
77B

System data only

Full
Next Sector/Track
WC=100B

Full sector

A full sector differs from an EOR sector by WC=100B rather than WC<100B as for the EOR sector.

To differentiate between a link to another track rather than to the next sector in header byte 1, the upper bit (bit 11) is set. The PP common decks that read/write mass storage perform the reading and writing of the header bytes. Also, CIO reads/writes the header bytes for disk I/O.

In the above table, the system sector for a file is indicated by special header byte values. This is done to prevent accidental reading through the system sector itself. The system sector is always sector 0 of the first track of a file.

cat_byname

Entries by Filename (key=filename, returns UI list)

Type:

dict

cat_byui

Entries by UI (key=userindex, returns filename list)

Type:

dict

cat_entries

Catalog Entries

Type:

list

cat_index

Entries by (key=(Filename, UI))

Type:

dict

catalog_tks

List of Catalog Tracks

Type:

list

cw1

Control Word 1

Type:

int

cw2

Control Word 2

Type:

int

emw1

Extended Memory Address Word 1

Type:

int

emw2

Extended Memory Address Word 2

Type:

int

devmask

Device Mask

Type:

int

devnum_list

List of device numbers referenced by this disk’s catalog (0 = This device)

Type:

list

devtype

Device Type ‘di’ ‘dj’ ‘dm’ ‘dq’ ‘db’

Type:

str

_next_free_trk

Free Tracks

Type:

int

label_sector

Label Sector

Type:

list

level

NOS Pack Format Level (0 <= NOS1.3; 1 >= NOS 1.4; 2 >= NOS 2+)

Type:

int

dsector_to_container

Device LOGICAL Sector to Host PHYSICAL Sector Conversion routine

Type:

function

lsec

Current Logical Sector

Type:

int

seclimit

Sector Limits for the selected device

Type:

int

secmask

Secondary Mask

Type:

int

current_track

Current Track

Type:

int

trt

Track Reservation Table

Type:

list

converters = {'db': <function dsect_to_csect_db>, 'di': <function dsect_to_csect_di>, 'dj': <function dsect_to_csect_dj>, 'dk': <function dsect_to_csect_dk>, 'dl': <function dsect_to_csect_dl>, 'dm': <function dsect_to_csect_dm>, 'dq': <function dsect_to_csect_dq>}
dump_catalog(ordinal: bool = True, byfilename: bool = True, byuserindex: bool = True) None[source]

Print the contents of the Disk Catalog

formats catalog entry for display which includes

file(record)name userindex size ‘d’irect or ‘i’ndirect access track / sector start

dump_ident(brief: bool = True) None[source]

Print some pack label information

find_files(filespec) dict[source]
get_nosfile(fn: str, ui: int) NosFile[source]

Open a NOS disk file

Iterates through the catalog entries and looks for a matching entry for a given name and userindex.

Raises:

FileNotFoundError

property logsec: int
property physec: int
seclimits = {'db': 640, 'di': 107, 'dj': 227, 'dk': 112, 'dl': 227, 'dm': 640, 'dq': 640}
sector_io(track=None, lsec=None, syssec=False, new_data=None) tuple[source]

Read (or write) a sector from the specified NOS logical track and sector address.

End of record indication is:

None (not EOR/EOF/EOI) or
the level number (EOR = 0, EOF > 0, EOI = -1),
    (same as NOS tape read).

The next track/sector are saved in the object state, and are used as default position for the next read.

By default, the system sector is skipped; if syssec is True, it will be returned.

Returns:

the block data as a list of words, the end of record indication.

property subsector
trklimits = {'db': 1682, 'di': 1632, 'dj': 1640, 'dk': 1632, 'dl': 1640, 'dm': 1682, 'dq': 1682}
class nosio.NosFile(disk: any, ent_obj: NosCatEntry)[source]

Bases: object

Encapsulation of the NOS file

daf

TRUE=Direct Access FALSE=Indirect

Type:

bool

_disk

Placeholder for the underlying disk device

Type:

any

ent_obj

Catalog Entry Object

Type:

nosio.ControlBlock

fsec

First Sector

Type:

int

ftrk

First Track

Type:

int

len

Length of the file

Type:

int

pack_struct

pack structure

Type:

str

tracks

Tracks associated with this file

Type:

None|list

property disk: any

This property changes the underlying disk containing this file.

When this occurs, the track table needs to be re-read to correct this file’s in-memory track list.

dump_nosfile(dformat='dump') None[source]

Dump the contents of a permanent file

https://www.bitsavers.org/pdf/cdc/Tom_Hunter_Scans//NOS_2_Analysis_Handbook_Ref_60459300M_Dec88_Part_1.pdf P 17-9 https://www.bitsavers.org/pdf/cdc/Tom_Hunter_Scans//NOS_2_Analysis_Handbook_Ref_60459300M_Dec88_Part_2.pdf

get_relative_sector(logical_sector: int, mfoffset: bool = True) list[source]

Read a relative sector (by number) from this file.

When Plato Master files are read, the system sector is ignored in the sector count (PLATO files don’t take that sector into account) so adding ONE to the sector number will always cause the system sector to be skipped when the track number is resolved for plato files.

This is the default case for get_relative_sector. We turn it off while we’re dumping files.

itemize_nosfile() list[source]

Map the contents of a permanent file

Return a list of records, their types, their starting positions, and lengths.

read_block(blk) list[source]
write_block(blk, data) None[source]
class nosio.PlatoDiskDevice(fn, mode='rb')[source]

Bases: DiskDevice

Plato disk (not NOS file structured).

read_block(blk: int) list[source]

Read a PLATO block from a disk image at the given block number.

(Blocks are 5 sequential sectors each.)

Return data is a 320 entry list of 60 bit words, without the control words because they are not used in PLATO.