Task 023: .AIO File Format

Task 023: .AIO File Format

File Format Specifications for .AIO

The .AIO file format is the APL Transfer Format File used by IBM APL (particularly IBM Personal Computer APL and APL2/PC variants) for serializing and transferring APL workspaces. It enables portability of APL objects (variables, functions, system variables, and operators) between systems. The format is text-based, character-oriented, and designed for interchange, with support for both migration (simple objects) and extended (complex/nested objects) serialization forms. It is not binary but uses a fixed-record structure with optional timestamps and comments. The format is defined in IBM APL documentation, including the APL2 Language Reference Manual.

.AIO files are typically distributed on diskettes or archives for legacy IBM APL software. They are readable by APL interpreters like IBM APL2, and compatible with similar formats in other APL implementations (e.g., .atf in MicroAPL or APLX).

1. List of All Properties of This File Format Intrinsic to Its File System

These are the core structural and intrinsic properties of the .AIO file format, derived from its specifications:

  • Fixed Record Length: Each record is exactly 80 bytes long.
  • Record Type Indicator: The first byte of each record indicates its type (' ' for continuation of an object, 'X' for the last or only record of an object, '?' for non-object records like timestamps or comments).
  • Content Area: Bytes 2-72 contain the serialized object representation (in migration or extended transfer form).
  • Sequence Number Area: Bytes 73-80 are optional and contain sequence or line numbers (ignored during reading/parsing).
  • Line Terminator: Each record is followed by a CR (ASCII 0x0D) or CR/LF (ASCII 0x0D 0x0A), not part of the 80-byte record.
  • Serialization Format: Supports migration form (simple arrays/functions as type/name/shape/data) or extended form (APL expressions like 'name ← value' or '⎕FX' for functions).
  • Object Types Supported: Variables (numeric, character, nested), functions, operators, system variables (e.g., ⎕CT, ⎕IO), and external objects.
  • Encoding: ASCII or EBCDIC, auto-detected based on valid first-byte values (e.g., ASCII: 0x20, 0x58, 0x3F; EBCDIC equivalents).
  • File Extension: .AIO (specific to IBM PC APL variants; no header or magic number, as it's text-based).
  • Portability: Character-based for cross-system transfer; requires binary mode transmission to preserve exact bytes.

3. Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .AIO File Dump

.AIO File Dumper
Drag and drop a .AIO file here

    

4. Python Class for .AIO File Handling

import re

class AIOFileHandler:
    def __init__(self, filepath):
        self.filepath = filepath
        self.properties = {
            'Fixed Record Length': '80 bytes',
            'Line Terminator': 'CR or CR/LF (inferred)',
            'Serialization Format': 'Migration/Extended Transfer Form',
            'Object Types Supported': 'Variables, Functions, Operators, System Variables',
            'File Extension': '.AIO',
            'Portability': 'Character-based cross-system transfer',
            'Extracted Objects': []
        }
        self.read_and_decode()

    def read_and_decode(self):
        with open(self.filepath, 'r', encoding='ascii', errors='ignore') as f:  # ASCII default; ignore EBCDIC issues
            content = f.read()
        # Remove terminators and split into 80-byte records
        content = re.sub(r'\r\n?|\n', '', content)
        records = re.findall(r'.{1,80}', content)
        current_object = ''
        self.properties['Records Found'] = len(records)
        self.properties['Encoding'] = self.detect_encoding(records[0][0] if records else '')

        for i, record in enumerate(records):
            if len(record) != 80:
                print(f"Warning: Record {i+1} not 80 bytes")
            indicator = record[0]
            content_area = record[1:72].strip()
            seq_num = record[72:80].strip()
            if indicator in (' ', 'X'):
                current_object += content_area
                if indicator == 'X':
                    # Parse object (e.g., match type, name, shape/value)
                    match = re.match(r'([CNF])(.+?)\s(\d\s.*)', current_object)
                    obj = {
                        'Record Type Indicator': indicator,
                        'Content Area': content_area,
                        'Sequence Number Area': seq_num,
                        'Parsed Object': {'type': match.group(1), 'name': match.group(2), 'shape_value': match.group(3)} if match else {'raw': current_object}
                    }
                    self.properties['Extracted Objects'].append(obj)
                    current_object = ''
            elif indicator == '?':
                obj = {
                    'Record Type Indicator': indicator,
                    'Content Area': content_area,  # Timestamp/comment
                    'Sequence Number Area': seq_num
                }
                self.properties['Extracted Objects'].append(obj)

    def print_properties(self):
        print('Dumped Properties for .AIO File:')
        for key, value in self.properties.items():
            print(f"{key}:")
            if isinstance(value, list):
                for i, obj in enumerate(value):
                    print(f"  Object {i+1}:")
                    for obj_key, obj_val in obj.items():
                        print(f"    {obj_key}: {obj_val}")
            else:
                print(f"  {value}")

    def write(self, new_filepath=None):
        # Reconstruct and write (simple: dump extracted objects back to records)
        filepath = new_filepath or self.filepath
        with open(filepath, 'w', encoding='ascii') as f:
            for obj in self.properties['Extracted Objects']:
                # Basic reconstruction; extend for full write support
                record = obj['Record Type Indicator'] + obj['Content Area'].ljust(71) + obj['Sequence Number Area'].ljust(8)
                f.write(record + '\r\n')  # Use CR/LF

    def detect_encoding(self, first_byte):
        code = ord(first_byte)
        if code in (0x20, 0x58, 0x3F):
            return 'ASCII'
        elif code in (0x40, 0xE7, 0x6F):  # EBCDIC approx
            return 'EBCDIC'
        return 'Unknown'

# Example usage:
# handler = AIOFileHandler('example.AIO')
# handler.print_properties()
# handler.write('modified.AIO')

5. Java Class for .AIO File Handling

import java.io.*;
import java.util.*;
import java.util.regex.*;

public class AIOFileHandler {
    private String filepath;
    private Map<String, Object> properties = new LinkedHashMap<>();

    public AIOFileHandler(String filepath) {
        this.filepath = filepath;
        properties.put("Fixed Record Length", "80 bytes");
        properties.put("Line Terminator", "CR or CR/LF (inferred)");
        properties.put("Serialization Format", "Migration/Extended Transfer Form");
        properties.put("Object Types Supported", "Variables, Functions, Operators, System Variables");
        properties.put("File Extension", ".AIO");
        properties.put("Portability", "Character-based cross-system transfer");
        properties.put("Extracted Objects", new ArrayList<Map<String, Object>>());
        readAndDecode();
    }

    private void readAndDecode() {
        StringBuilder content = new StringBuilder();
        try (BufferedReader reader = new BufferedReader(new FileReader(filepath))) {
            String line;
            while ((line = reader.readLine()) != null) {
                content.append(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        String cleaned = content.toString().replaceAll("\\r\\n?|\\n", "");
        List<String> records = new ArrayList<>();
        for (int i = 0; i < cleaned.length(); i += 80) {
            records.add(cleaned.substring(i, Math.min(i + 80, cleaned.length())));
        }
        properties.put("Records Found", records.size());
        properties.put("Encoding", detectEncoding(records.isEmpty() ? "" : records.get(0).substring(0, 1)));

        String currentObject = "";
        List<Map<String, Object>> extracted = (List<Map<String, Object>>) properties.get("Extracted Objects");
        for (int i = 0; i < records.size(); i++) {
            String record = records.get(i);
            if (record.length() != 80) {
                System.out.println("Warning: Record " + (i + 1) + " not 80 bytes");
            }
            String indicator = record.substring(0, 1);
            String contentArea = record.substring(1, Math.min(72, record.length())).trim();
            String seqNum = record.length() > 72 ? record.substring(72).trim() : "";
            Map<String, Object> obj = new HashMap<>();
            if (indicator.equals(" ") || indicator.equals("X")) {
                currentObject += contentArea;
                if (indicator.equals("X")) {
                    Pattern pattern = Pattern.compile("([CNF])(.+?)\\s(\\d\\s.*)");
                    Matcher match = pattern.matcher(currentObject);
                    Map<String, String> parsed = new HashMap<>();
                    if (match.matches()) {
                        parsed.put("type", match.group(1));
                        parsed.put("name", match.group(2));
                        parsed.put("shape_value", match.group(3));
                    } else {
                        parsed.put("raw", currentObject);
                    }
                    obj.put("Record Type Indicator", indicator);
                    obj.put("Content Area", contentArea);
                    obj.put("Sequence Number Area", seqNum);
                    obj.put("Parsed Object", parsed);
                    extracted.add(obj);
                    currentObject = "";
                }
            } else if (indicator.equals("?")) {
                obj.put("Record Type Indicator", indicator);
                obj.put("Content Area", contentArea);
                obj.put("Sequence Number Area", seqNum);
                extracted.add(obj);
            }
        }
    }

    public void printProperties() {
        System.out.println("Dumped Properties for .AIO File:");
        for (Map.Entry<String, Object> entry : properties.entrySet()) {
            System.out.println(entry.getKey() + ":");
            if (entry.getValue() instanceof List) {
                List<Map<String, Object>> list = (List<Map<String, Object>>) entry.getValue();
                for (int i = 0; i < list.size(); i++) {
                    System.out.println("  Object " + (i + 1) + ":");
                    for (Map.Entry<String, Object> objEntry : list.get(i).entrySet()) {
                        System.out.println("    " + objEntry.getKey() + ": " + objEntry.getValue());
                    }
                }
            } else {
                System.out.println("  " + entry.getValue());
            }
        }
    }

    public void write(String newFilepath) {
        if (newFilepath == null) newFilepath = filepath;
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(newFilepath))) {
            List<Map<String, Object>> extracted = (List<Map<String, Object>>) properties.get("Extracted Objects");
            for (Map<String, Object> obj : extracted) {
                String record = (String) obj.get("Record Type Indicator") + ((String) obj.get("Content Area")).format("%-71s", "") + ((String) obj.get("Sequence Number Area")).format("%-8s", "");
                writer.write(record + "\r\n");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private String detectEncoding(String firstByte) {
        if (firstByte.isEmpty()) return "Unknown";
        int code = (int) firstByte.charAt(0);
        if (code == 32 || code == 88 || code == 63) return "ASCII";
        if (code == 64 || code == 231 || code == 111) return "EBCDIC"; // Approx
        return "Unknown";
    }

    // Example usage:
    // public static void main(String[] args) {
    //     AIOFileHandler handler = new AIOFileHandler("example.AIO");
    //     handler.printProperties();
    //     handler.write("modified.AIO");
    // }
}

6. JavaScript Class for .AIO File Handling

class AIOFileHandler {
    constructor(filepath) {
        this.filepath = filepath;
        this.properties = {
            'Fixed Record Length': '80 bytes',
            'Line Terminator': 'CR or CR/LF (inferred)',
            'Serialization Format': 'Migration/Extended Transfer Form',
            'Object Types Supported': 'Variables, Functions, Operators, System Variables',
            'File Extension': '.AIO',
            'Portability': 'Character-based cross-system transfer',
            'Extracted Objects': []
        };
        // Note: Node.js required for file I/O (use fs module)
        const fs = require('fs');
        this.readAndDecode(fs.readFileSync(filepath, 'ascii'));
    }

    readAndDecode(content) {
        content = content.toString().replace(/[\r\n]+/g, '');
        const records = content.match(/.{1,80}/g) || [];
        this.properties['Records Found'] = records.length;
        this.properties['Encoding'] = this.detectEncoding(records[0] ? records[0][0] : '');

        let currentObject = '';
        records.forEach((record, i) => {
            if (record.length !== 80) console.warn(`Record ${i+1} not 80 bytes`);
            const indicator = record[0];
            const contentArea = record.slice(1, 72).trim();
            const seqNum = record.slice(72).trim();
            const obj = {};
            if (indicator === ' ' || indicator === 'X') {
                currentObject += contentArea;
                if (indicator === 'X') {
                    const match = currentObject.match(/([CNF])(.+?)\s(\d\s.*)/);
                    obj['Parsed Object'] = match ? { type: match[1], name: match[2], shape_value: match[3] } : { raw: currentObject };
                    obj['Record Type Indicator'] = indicator;
                    obj['Content Area'] = contentArea;
                    obj['Sequence Number Area'] = seqNum;
                    this.properties['Extracted Objects'].push(obj);
                    currentObject = '';
                }
            } else if (indicator === '?') {
                obj['Record Type Indicator'] = indicator;
                obj['Content Area'] = contentArea;
                obj['Sequence Number Area'] = seqNum;
                this.properties['Extracted Objects'].push(obj);
            }
        });
    }

    printProperties() {
        console.log('Dumped Properties for .AIO File:');
        for (const [key, value] of Object.entries(this.properties)) {
            console.log(`${key}:`);
            if (Array.isArray(value)) {
                value.forEach((obj, i) => {
                    console.log(`  Object ${i+1}:`);
                    for (const [objKey, objVal] of Object.entries(obj)) {
                        console.log(`    ${objKey}: ${JSON.stringify(objVal)}`);
                    }
                });
            } else {
                console.log(`  ${value}`);
            }
        }
    }

    write(newFilepath = this.filepath) {
        const fs = require('fs');
        let output = '';
        this.properties['Extracted Objects'].forEach(obj => {
            let record = obj['Record Type Indicator'] + obj['Content Area'].padEnd(71) + obj['Sequence Number Area'].padEnd(8);
            output += record + '\r\n';
        });
        fs.writeFileSync(newFilepath, output, 'ascii');
    }

    detectEncoding(firstByte) {
        const code = firstByte.charCodeAt(0);
        if (code === 32 || code === 88 || code === 63) return 'ASCII';
        if (code === 64 || code === 231 || code === 111) return 'EBCDIC';
        return 'Unknown';
    }
}

// Example usage (Node.js):
// const handler = new AIOFileHandler('example.AIO');
// handler.printProperties();
// handler.write('modified.AIO');

7. C++ Class for .AIO File Handling

#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <map>
#include <regex>

class AIOFileHandler {
private:
    std::string filepath;
    std::map<std::string, std::string> static_properties;
    int records_found;
    std::string encoding;
    std::vector<std::map<std::string, std::string>> extracted_objects;

public:
    AIOFileHandler(const std::string& fp) : filepath(fp), records_found(0), encoding("Unknown") {
        static_properties["Fixed Record Length"] = "80 bytes";
        static_properties["Line Terminator"] = "CR or CR/LF (inferred)";
        static_properties["Serialization Format"] = "Migration/Extended Transfer Form";
        static_properties["Object Types Supported"] = "Variables, Functions, Operators, System Variables";
        static_properties["File Extension"] = ".AIO";
        static_properties["Portability"] = "Character-based cross-system transfer";
        read_and_decode();
    }

    void read_and_decode() {
        std::ifstream file(filepath, std::ios::in);
        std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
        file.close();
        content.erase(std::remove(content.begin(), content.end(), '\r'), content.end());
        content.erase(std::remove(content.begin(), content.end(), '\n'), content.end());
        std::vector<std::string> records;
        for (size_t i = 0; i < content.length(); i += 80) {
            records.push_back(content.substr(i, 80));
        }
        records_found = records.size();
        if (!records.empty()) encoding = detect_encoding(records[0][0]);

        std::string current_object;
        for (size_t i = 0; i < records.size(); ++i) {
            std::string record = records[i];
            if (record.length() != 80) std::cerr << "Warning: Record " << i + 1 << " not 80 bytes" << std::endl;
            char indicator = record[0];
            std::string content_area = record.substr(1, 71);
            content_area.erase(content_area.find_last_not_of(" ") + 1); // Trim
            std::string seq_num = record.substr(72);
            seq_num.erase(seq_num.find_last_not_of(" ") + 1);
            std::map<std::string, std::string> obj;
            if (indicator == ' ' || indicator == 'X') {
                current_object += content_area;
                if (indicator == 'X') {
                    std::regex pattern("([CNF])(.+?)\\s(\\d\\s.*)");
                    std::smatch match;
                    if (std::regex_match(current_object, match, pattern)) {
                        obj["Parsed Object Type"] = match[1];
                        obj["Parsed Object Name"] = match[2];
                        obj["Parsed Object Shape/Value"] = match[3];
                    } else {
                        obj["Parsed Object Raw"] = current_object;
                    }
                    obj["Record Type Indicator"] = std::string(1, indicator);
                    obj["Content Area"] = content_area;
                    obj["Sequence Number Area"] = seq_num;
                    extracted_objects.push_back(obj);
                    current_object.clear();
                }
            } else if (indicator == '?') {
                obj["Record Type Indicator"] = std::string(1, indicator);
                obj["Content Area"] = content_area;
                obj["Sequence Number Area"] = seq_num;
                extracted_objects.push_back(obj);
            }
        }
    }

    void print_properties() {
        std::cout << "Dumped Properties for .AIO File:" << std::endl;
        for (const auto& prop : static_properties) {
            std::cout << prop.first << ": " << prop.second << std::endl;
        }
        std::cout << "Records Found: " << records_found << std::endl;
        std::cout << "Encoding: " << encoding << std::endl;
        std::cout << "Extracted Objects:" << std::endl;
        for (size_t i = 0; i < extracted_objects.size(); ++i) {
            std::cout << "  Object " << i + 1 << ":" << std::endl;
            for (const auto& obj_prop : extracted_objects[i]) {
                std::cout << "    " << obj_prop.first << ": " << obj_prop.second << std::endl;
            }
        }
    }

    void write(const std::string& new_filepath = "") {
        std::string out_path = new_filepath.empty() ? filepath : new_filepath;
        std::ofstream file(out_path);
        for (const auto& obj : extracted_objects) {
            std::string record(80, ' ');
            record[0] = obj.at("Record Type Indicator")[0];
            std::string content = obj.at("Content Area");
            record.replace(1, content.length(), content);
            std::string seq = obj.at("Sequence Number Area");
            record.replace(72, seq.length(), seq);
            file << record << "\r\n";
        }
        file.close();
    }

private:
    std::string detect_encoding(char first_byte) {
        if (first_byte == ' ' || first_byte == 'X' || first_byte == '?') return "ASCII";
        // EBCDIC checks simplified
        if (first_byte == '@' || first_byte == (char)231 || first_byte == 'o') return "EBCDIC";
        return "Unknown";
    }
};

// Example usage:
// int main() {
//     AIOFileHandler handler("example.AIO");
//     handler.print_properties();
//     handler.write("modified.AIO");
//     return 0;
// }