Dans

Un script Python nommé parse_dsk.py qui m’ a permis d’analyser 3805 images de disquettes au format .dsk issues de l’Amstrad CPC . L’objectif est de générer un fichier cat.txt pour chaque image, simulant la commande CAT du CPC : liste des fichiers avec nom (aligné sur 8+3 caractères en majuscules), taille en Ko, type (BAS pour BASIC, BIN pour binaire, ??? pour inconnu), et indicateur de protection en lecture ( P ) . Avec 3801 fichiers traités sur 3805, les 4 restants sont probablement corrompus, mais le script les gère en produisant un cat.txt vide avec un diagnostic. Cet article détaille les protections de disquettes utilisées à l’époque, explique le code étape par étape, et met en lumière les défis techniques.

Les protections de disquettes :

Les protections de disquettes étaient essentielles pour les éditeurs de logiciels des années 80, visant à empêcher la copie pirate sur des machines comme l’Amstrad CPC. Ces méthodes exploitaient les limitations des lecteurs de disquettes 3 pouces (178 Ko par face) et des contrôleurs FDC . Voici une documentation des principaux types, basée sur des sources spécialisées (CPCWiki, analyses de protections comme Speedlock) :

  1. Speedlock (utilisé sur CPC et Spectrum) :
    • Fonctionnement : Cette protection, développée par David Looker, modifie la structure physique de la disquette. Elle utilise des secteurs avec des tailles non standards (ex. 256 ou 1024 octets au lieu de 512), des ID de secteurs invalides (ex. 0x41-0x49 au lieu de 0xC1-0xC9 pour les formats DATA), ou des bits faibles (weak bits) qui changent de valeur à chaque lecture. Lors de l’exécution, le loader vérifie ces anomalies ; une copie standard (ex. avec le commande DISC du CPC) perd ces bits, rendant le logiciel inutilisable.
    • Variantes : Speedlock 8k (pour disquettes étendues) ou cassette. Sur CPC, elle inclut souvent un check caché dans les fichiers de données (ex. graphics.dat).
    • Détection : Le script vérifie les headers et les tailles de secteurs ; si invalides, il passe en mode « secours » pour éviter un crash.
  2. Alkatraz (spécifique aux jeux CPC comme ceux de Ocean) :
    • Fonctionnement : Créée par Alkatraz Protection System, cette méthode utilise des tracks avec des données invalides (ex. CRC errors intentionnels ou secteurs dupliqués). Le loader du jeu vérifie ces erreurs ; une copie standard les corrige, rendant le jeu instable. Elle inclut aussi des checks en mémoire (ex. code auto-modifiant) pour détecter les crackers.
    • Variantes : Souvent combinée avec des loaders personnalisés. Sur CPC, elle exploitait le FDC pour des reads multiples sur le même secteur.
    • Détection : Le script recherche la signature ALKATRAZ dans les premiers octets ; si détectée, il crée un cat.txt vide avec un message « Protégé (Speedlock/Alkatraz) ».
  3. DiscSys (utilisé sur CPC par des éditeurs comme Dinamic) :
    • Fonctionnement : Cette protection crée des tracks invalides avec des données non copiables (ex. densité variable ou fuzzy bits). Le logiciel vérifie l’intégrité physique ; une copie logicielle (ex. avec Discology) échoue à reproduire ces anomalies.
    • Variantes : Souvent combinée avec des checks en runtime (ex. lecture répétée d’un secteur faible).
    • Détection : Le script identifie les tailles de tracks non standards (ex. != 4864 octets pour DATA).
  4. Autres protections courantes :
    • Weak/Flaky Bits : Bits intentionnellement faibles qui varient à chaque lecture. Utilisé dans Dungeon Master ou Gauntlet. Détection : Vérification de valeurs changeantes sur plusieurs reads (non gérée ici, car on travaille sur des images .dsk statiques).
    • Custom Formats : Secteurs avec ID invalides ou formats mixtes (ex. MFM/FM). Exemple : Action Replay sur CPC.
    • Hidden Checks : Code dans les fichiers de données qui vérifie la présence de protections (ex. fuzzy bits dans graphics.dat).

Ces protections rendaient la copie difficile, mais les crackers (comme ceux de XOR ou NPS) les contournent en patchant les loaders. Dans parse_dsk.py, on gère ces cas en mode brute force pour extraire les catalogues, même sur des disquettes protégées.


Explication du code :

Le script parse_dsk.py est conçu pour être facilement déboguable, avec des commentaires détaillés, des logs DEBUG pour chaque étape, et une structure modulaire. Il utilise une approche directe sur les objets bytes pour manipuler les données binaires, sans struct pour des raisons de simplicité (offsets fixes, pas de formats imbriqués complexes). Voici un breakdown fonction par fonction :

  1. Configuration du logger :
    • Utilise logging.basicConfig pour logger en DEBUG dans errors.log. Cela permet de tracer chaque parsing (Début du parsing de {dsk_path}) et les erreurs (ex. « Permission refusée »).
    • Débogage : Change level=logging.ERROR en logging.DEBUG pour plus de traces.
  2. Vérification des permissions (check_write_permission) :
    • Vérifie si le répertoire et le fichier cat.txt sont accessibles en écriture avec os.access. Si non, logue l’erreur et retourne False.
    • Débogage : Les exceptions sont capturées et loguées pour identifier les problèmes de droits (fréquents sur des répertoires partagés).
  3. Validation des entrées de répertoire (is_valid_dir_entry) :
    • Vérifie la taille (32 octets), l’utilisateur (pas supprimé ou invalide), et le nom (seulement ASCII 32-126, pas de ?).
    • Débogage : Logs DEBUG pour chaque raison d’invalidité (ex. « Entrée invalide : caractère non ASCII dans le nom ({name}) »).
  4. Détermination du type de fichier (get_file_type) :
    • Lit le premier octet du premier secteur du fichier pour identifier BAS (0x00) ou BIN (0x01). Sinon, ???.
    • Débogage : Facile à étendre pour d’autres types (ex. ASC pour ASCII).
  5. Écriture d’un cat.txt vide (write_empty_cat) :
    • Crée un cat.txt vide avec un message (ex. « Aucun répertoire valide détecté – disque vide, corrompu ou protégé »).
    • Débogage : Logue la création pour tracer les fichiers problématiques.
  6. Parsing principal (parse_dsk_and_write_cat) :
    • Lit le .dsk en binaire.
    • Détecte le format (Extended CPC, MV-CPC, CP/M, etc.).
    • Extrait les métadonnées (pistes, faces).
    • Recherche le répertoire sur les pistes ou en mode brute force (scan par 512 octets).
    • Parse les entrées valides et calcule les tailles et types.
    • Génère un cat.txt formaté (noms majuscules, alignement 8+3).
    • Gestion des erreurs : write_empty_cat pour tout échec (corruption, permissions).
    • Débogage : Logs pour chaque piste, secteur valide, et entrée invalide.
  7. Parcours récursif :
    • Utilise os.walk pour traiter tous les .dsk.
    • Débogage : Chaque fichier est logué avec Début du parsing de {dsk_path}.

Le script est résilient : il crée un cat.txt même pour les fichiers corrompus, et les logs facilitent l’analyse (ex. dump hex pour les erreurs).


 
#!/usr/bin/env python3
import os
import stat
import logging

# Configurer le logger pour des traces détaillées
logging.basicConfig(
    filename='/arnold/dsk/errors.log',
    level=logging.DEBUG,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

def check_write_permission(path):
    """Vérifie les droits d'écriture pour le répertoire et le fichier."""
    dir_path = os.path.dirname(path) or '.'
    try:
        if not os.access(dir_path, os.W_OK):
            logging.error(f"Permission d'écriture absente pour le répertoire {dir_path}")
            print(f"Erreur : Permission d'écriture absente pour {dir_path}")
            return False
        if os.path.exists(path) and not os.access(path, os.W_OK):
            logging.error(f"Permission d'écriture absente pour le fichier {path}")
            print(f"Erreur : Permission d'écriture absente pour {path}")
            return False
        return True
    except Exception as e:
        logging.error(f"Erreur de permission pour {path} : {e}")
        print(f"Erreur : Permission refusée pour {path} : {e}")
        return False

def is_valid_dir_entry(entry):
    """Vérifie si une entrée de répertoire est valide pour un CAT CPC authentique."""
    if len(entry) != 32:
        logging.debug(f"Entrée invalide : taille incorrecte ({len(entry)} octets)")
        return False
    user = entry[0]
    if user == 0xE5 or user > 15:
        logging.debug(f"Entrée invalide : utilisateur {user} (supprimé ou invalide)")
        return False
    name_bytes = entry[1:12]  # Nom (8 octets) + extension (3 octets)
    name = ''.join(chr(b & 0x7F) if 32 <= (b & 0x7F) <= 126 else '?' for b in name_bytes[0:8]).strip()
    ext = ''.join(chr(b & 0x7F) if 32 <= (b & 0x7F) <= 126 else '?' for b in name_bytes[8:11]).strip()
    filename = name + ('.' + ext if ext else '')
    # Rejeter si le nom ou l'extension contient un '?' ou est vide
    if not name or not filename or '?' in filename:
        logging.debug(f"Entrée invalide : nom vide ou contient '?' ({filename})")
        return False
    # Vérifier chaque caractère du nom complet
    for c in filename:
        if ord(c) < 32 or ord(c) > 126:
            logging.debug(f"Entrée invalide : caractère non ASCII dans le nom ({filename})")
            return False
    return True

def get_file_type(sectors, blocks):
    """Détermine le type de fichier (BAS, BIN, ou ???)."""
    for block in blocks:
        if block == 0:
            continue
        sector_id = block * 2
        if sector_id in sectors and len(sectors[sector_id]) >= 2:
            header = sectors[sector_id][0:2]
            if header[0] == 0x00:
                return 'BAS'
            elif header[0] == 0x01:
                return 'BIN'
            break
    return '???'

def write_empty_cat(output_path, reason):
    """Écrit un cat.txt vide avec un message d'erreur, formaté comme un CAT CPC."""
    output = f"Directory of A:\n\n0 file(s),   0K free.\n(Note: {reason})\n"
    try:
        with open(output_path, 'w') as out:
            out.write(output)
        print(f"Succès : cat.txt créé pour {output_path} ({reason})")
        logging.info(f"cat.txt créé pour {output_path} ({reason})")
    except PermissionError as e:
        logging.error(f"Permission refusée pour écrire {output_path} : {e}")
        print(f"Erreur : Permission refusée pour écrire {output_path} : {e}")

def parse_dsk_and_write_cat(dsk_path, output_path):
    """Parse un .dsk et écrit un cat.txt au format CAT CPC."""
    logging.debug(f"Début du parsing de {dsk_path}")
    if not check_write_permission(output_path):
        write_empty_cat(output_path, "Échec de la vérification des permissions")
        return

    try:
        with open(dsk_path, 'rb') as f:
            dsk = f.read()

        # Vérifier si le fichier est vide ou trop petit
        if len(dsk) < 256:
            write_empty_cat(output_path, f"Disque vide ou corrompu - taille {len(dsk)} octets")
            return

        # Identifier le format
        is_extended = dsk[0:34] == b'EXTENDED CPC DSK File\r\nDisk-Info\r\n'
        is_mv_cpc = dsk[0:34] == b'MV - CPCEMU Disk-File\r\nDisk-Info\r\n'
        is_mv_cpc_date = dsk[0:11] == b'MV - CPCEMU' and b'/' in dsk[11:34]
        is_mv_cpc_variant = dsk[0:11] == b'MV - CPCEMU' and 'Disk-File' in dsk[0:34].decode('ascii', errors='ignore')
        is_protected = dsk[0:8] in [b'SPEEDLOC', b'ALKATRAZ']
        is_cpm = 'CP/M' in dsk[0:34].decode('ascii', errors='ignore') or dsk_path.lower().endswith('_cpm_version.dsk')
        format_type = "inconnu"
        if is_extended:
            format_type = "Extended CPC"
        elif is_mv_cpc:
            format_type = "MV-CPC"
        elif is_mv_cpc_date:
            format_type = "MV-CPC-Date"
        elif is_mv_cpc_variant:
            format_type = "MV-CPC-Variant"
        elif is_protected:
            format_type = "Protégé (Speedlock/Alkatraz)"
        elif is_cpm:
            format_type = "CP/M"
        else:
            header_str = dsk[0:34].decode('ascii', errors='ignore')
            logging.debug(f"Format inconnu pour {dsk_path}. Préfixe: {dsk[0:11].decode(errors='ignore')}, Header: {header_str}, Hex: {dsk[:512].hex()}")

        print(f"Debug : Format = {format_type} pour {dsk_path}")

        # Mode secours pour disques protégés, vides ou formats inconnus
        if is_protected or len(dsk) < 1024 or format_type == "inconnu":
            write_empty_cat(output_path, f"Disque protégé, vide ou format non reconnu - {format_type}")
            return

        # Extraire infos disque
        try:
            num_tracks = dsk[0x30]
            num_sides = dsk[0x31]
            if is_extended or is_mv_cpc_variant or is_cpm or is_mv_cpc_date:
                track_sizes = [dsk[0x34 + i] * 256 for i in range(num_tracks * num_sides)]
            else:
                track_size = dsk[0x32] + (dsk[0x33] * 256)
                track_sizes = [track_size] * (num_tracks * num_sides)
        except IndexError as e:
            logging.error(f"Erreur de lecture des métadonnées pour {dsk_path} : {e}. Hex: {dsk[:512].hex()}")
            write_empty_cat(output_path, f"Métadonnées invalides - disque corrompu - {str(e)}")
            return
        offset = 0x100

        print(f"Debug : {num_tracks} pistes, {num_sides} faces, taille piste = {track_sizes[0]} octets")

        if num_sides > 1:
            print(f"Avertissement : {dsk_path} multi-faces, seul face 0 parsé.")

        # Lire une piste
        def read_track(track_offset, expected_track=0):
            if track_offset + 0x100 > len(dsk):
                logging.error(f"Offset piste {hex(track_offset)} hors limites dans {dsk_path}.")
                return None, None
            track_info = dsk[track_offset:track_offset + 0x100]
            if len(track_info) < 12 or not track_info[0:10].startswith(b'Track-Info'):
                logging.error(f"Header piste invalide à {hex(track_offset)} dans {dsk_path} (premiers 12 octets: {track_info[:12].hex()}).")
                return None, None
            actual_track = track_info[0x10]
            side = track_info[0x11]
            if actual_track != expected_track or side != 0:
                logging.error(f"Piste inattendue ({actual_track}, face {side}) à {hex(track_offset)} dans {dsk_path}.")
                return None, None
            sector_size_code = track_info[0x14]
            num_sectors = track_info[0x15]
            sectors = {}
            data_offset = track_offset + 0x100
            for s in range(num_sectors):
                so = 0x18 + s * 8
                if so + 8 > len(track_info):
                    logging.error(f"Liste secteurs incomplète dans {dsk_path}.")
                    return None, None
                R = track_info[so + 2]
                actual_length = 512 if sector_size_code == 2 else (track_info[so + 6] + (track_info[so + 7] * 256))
                if data_offset + actual_length > len(dsk):
                    print(f"Avertissement : Secteur {hex(R)} tronqué (offset {hex(data_offset)}) dans {dsk_path}.")
                    continue
                sectors[R] = dsk[data_offset:data_offset + actual_length]
                data_offset += actual_length
            return sectors, sector_size_code

        # Chercher le répertoire (toutes pistes)
        dir_data = b''
        dir_found = False
        sectors = {}
        for track_num in range(num_tracks):
            track_offset = offset + sum(track_sizes[:track_num])
            track_size = track_sizes[track_num]
            if track_size == 0:
                print(f"Avertissement : Piste {track_num} non formatée dans {dsk_path}.")
                continue
            track_sectors, sector_size_code = read_track(track_offset, expected_track=track_num)
            if track_sectors is None:
                continue
            sectors.update(track_sectors)
            dir_data = b''
            for r in sorted(sectors.keys()):
                if len(sectors[r]) >= 512:
                    temp_data = sectors[r]
                    num_entries = len(temp_data) // 32
                    valid_entries = sum(1 for i in range(num_entries) if is_valid_dir_entry(temp_data[i*32:(i+1)*32]))
                    if valid_entries > 0:
                        dir_data += temp_data
                        logging.debug(f"Entrées valides trouvées à piste {track_num}, secteur {hex(r)} pour {dsk_path}")
            if len(dir_data) >= 512:
                dir_found = True
                print(f"Debug : Répertoire trouvé sur piste {track_num} dans {dsk_path} (taille = {len(dir_data)} octets)")
                logging.debug(f"Répertoire trouvé sur piste {track_num} pour {dsk_path}")
                break

        # Mode brute force renforcé pour formats non standards (y compris CP/M)
        if not dir_found:
            print(f"Debug : Mode brute force renforcé pour {dsk_path}")
            logging.debug(f"Mode brute force activé pour {dsk_path}")
            dir_data = b''
            offset = 0x100
            max_iterations = 10000
            iteration = 0
            try:
                while offset + 512 <= len(dsk) and iteration < max_iterations and len(dir_data) < 4096:
                    temp_data = dsk[offset:offset + 512]
                    num_entries = len(temp_data) // 32
                    valid_entries = sum(1 for i in range(num_entries) if is_valid_dir_entry(temp_data[i*32:(i+1)*32]))
                    if valid_entries > 0:
                        dir_data += temp_data
                        logging.debug(f"Entrées valides trouvées à offset {hex(offset)} pour {dsk_path}")
                    offset += 512
                    iteration += 1
                if len(dir_data) >= 512:
                    dir_found = True
                    print(f"Debug : Répertoire trouvé en mode brute force renforcé dans {dsk_path} (taille = {len(dir_data)} octets)")
                    logging.info(f"Répertoire trouvé en mode brute force pour {dsk_path}")
                else:
                    logging.error(f"Aucun répertoire valide trouvé en mode brute force pour {dsk_path} après {iteration} itérations. Hex: {dsk[:512].hex()}")
                    write_empty_cat(output_path, "Aucun répertoire valide détecté en mode brute force")
                    return
            except Exception as e:
                logging.error(f"Erreur dans mode brute force pour {dsk_path} à offset {hex(offset)} : {e}. Hex: {dsk[:512].hex()}")
                write_empty_cat(output_path, f"Erreur brute force - {str(e)}")
                return

        # Mode secours ultime si aucun répertoire n’est trouvé
        if not dir_found or len(dir_data) < 512:
            write_empty_cat(output_path, "Aucun répertoire valide détecté - disque vide, corrompu ou protégé")
            return

        # Parser répertoire
        num_entries = len(dir_data) // 32
        files = {}
        used_blocks = set()
        for i in range(num_entries):
            entry = dir_data[i * 32:(i + 1) * 32]
            if not is_valid_dir_entry(entry):
                continue
            user = entry[0]
            name_bytes = entry[1:12]
            name = ''.join(chr(b & 0x7F) if 32 <= (b & 0x7F) <= 126 else '?' for b in name_bytes[0:8]).strip()
            ext = ''.join(chr(b & 0x7F) if 32 <= (b & 0x7F) <= 126 else '?' for b in name_bytes[8:11]).strip()
            filename = name + ('.' + ext if ext else '')
            read_only = bool(name_bytes[8] & 0x80)
            system = bool(name_bytes[9] & 0x80)
            if system:
                continue
            ex = entry[12]
            s2 = entry[14]
            extent = ex + (s2 * 32)
            rc = entry[15]
            blocks = [b for b in entry[16:32] if b != 0]
            if filename not in files:
                files[filename] = {'entries': [], 'read_only': read_only, 'blocks': []}
            files[filename]['entries'].append({'extent': extent, 'rc': rc})
            files[filename]['blocks'].extend(blocks)
            used_blocks.update(blocks)

        if not files:
            write_empty_cat(output_path, "Aucun fichier valide détecté - répertoire vide ou corrompu")
            return

        # Calculer tailles et types
        for filename in files:
            entries = sorted(files[filename]['entries'], key=lambda e: e['extent'])
            total_rc = sum(e['rc'] for e in entries)
            size_k = (total_rc + 7) // 8
            files[filename]['size_k'] = size_k
            files[filename]['type'] = get_file_type(sectors, files[filename]['blocks'])

        # Calculer espace libre
        total_blocks = 180 if len(dir_data) >= 2048 else 160
        reserved_blocks = 2 if format_type in ["MV-CPC", "Extended CPC", "MV-CPC-Date", "MV-CPC-Variant", "CP/M"] else 4
        used_k = len(used_blocks)
        free_k = max(0, total_blocks - reserved_blocks - used_k)

        # Générer output au format CAT CPC
        output = "Directory of A:\n\n"
        for filename in sorted(files, key=lambda x: x.upper()):  # Trier en ignorant la casse
            parts = filename.rsplit('.', 1)  # Séparer sur le dernier point
            if len(parts) == 2:
                name, ext = parts
            else:
                name, ext = parts[0], ''  # Pas d'extension
            name = name.upper().ljust(8)[:8]  # Nom en majuscules, 8 caractères max
            ext = ext.upper().ljust(3)[:3]    # Extension en majuscules, 3 caractères max
            display_name = f"{name}.{ext}" if ext else name
            line = f"{display_name.ljust(12)} {files[filename]['size_k']:>2}K {files[filename]['type']}"
            if files[filename]['read_only']:
                line += " p"
            output += line + "\n"
        output += f"\n{len(files):>2} file(s), {free_k:>3}K free.\n"
        if len(dir_data) < 2048:
            output += f"(Note: Répertoire partiel - format possiblement CP/M ou non standard)\n"

        # Écrire cat.txt
        try:
            with open(output_path, 'w') as out:
                out.write(output)
            print(f"Succès : cat.txt créé pour {dsk_path} ({len(files)} fichiers listés)")
            logging.info(f"cat.txt créé pour {dsk_path} ({len(files)} fichiers)")
        except PermissionError as e:
            logging.error(f"Permission refusée pour écrire {output_path} : {e}")
            print(f"Erreur : Permission refusée pour écrire {output_path} : {e}")
            write_empty_cat(output_path, f"Permission refusée pour écrire - {str(e)}")

    except Exception as e:
        logging.error(f"Erreur générale lors du parsing de {dsk_path} : {e}. Hex: {dsk[:512].hex()}")
        print(f"Erreur générale lors du parsing de {dsk_path} : {e}")
        write_empty_cat(output_path, f"Erreur générale lors du parsing - {str(e)}")

# Parcours récursif
print("Début du parsing de tous les .dsk...")
for root, dirs, files in os.walk('/arnold/dsk'):
    for file in files:
        if file.lower().endswith('.dsk'):
            dsk_path = os.path.join(root, file)
            cat_path = os.path.join(root, 'cat.txt')
            print(f"Traitement : {dsk_path}")
            parse_dsk_and_write_cat(dsk_path, cat_path)
print("Fin du parsing.")

Installation

  1. Copier le script Python dans un répertoire sur votre machine (ex : parse_dsk.py).
  2. S’assurer que Python 3 est installé.
  3. Adapter le chemin racine si nécessaire (par défaut le script parcourt /arnold/dsk).
  4. Donner les permissions nécessaires pour que le script puisse écrire les fichiers cat.txt et le fichier de log errors.log.

python3 parse_dsk.py

Le script utilise logging et écrit par défaut dans /arnold/dsk/errors.log. Modifiez logging.basicConfig() si vous préférez un autre emplacement.


Utilisation et sortie

Pour chaque image .dsk trouvée, le script :

  • Analyse l’entête et essaie d’identifier le format.
  • Parcourt les pistes (face 0) et reconstitue les secteurs disponibles.
  • Cherche des entrées de répertoire valides (32 octets) et les assemble.
  • Si aucun répertoire n’est trouvé, active un mode de balayage brut (brute force) sur le fichier image.
  • Si un répertoire valide est reconstitué, le script extrait les noms, tailles et types et écrit un cat.txt au même emplacement que l’image.

Exemple d’un cat.txt généré :

Directory of A:

HELLO.BAS 4K BAS

GAME.BIN 16K BIN p

2 file(s), 162K free.

Le script affiche aussi des messages de debug et des erreurs sur la sortie standard pour suivi rapide lors d’un run manuel.


Limitations et points d’attention

  • Le script n’essaie d’analyser que la face 0 des images multi‑faces (il log un avertissement si une image contient plusieurs faces).
  • Pour des disques fortement endommagés, le mode brute force peut ne rien trouver — dans ce cas un cat.txt minimal est généré pour indiquer l’échec.
  • Les heuristiques d’identification de type (BAS/BIN) sont basiques et peuvent donner ??? sur des fichiers atypiques.

Logs et débogage

  • Le script écrit un log détaillé (DEBUG) dans /arnold/dsk/errors.log.
  • En cas d’erreurs de permission d’écriture, le script affiche un message et consigne l’erreur dans le log.
  • Pour investiguer une image problématique, regardez les premiers octets hexadécimaux inscrits dans le log — ils aident à comprendre la nature de l’image.

Suggestions d’améliorations possibles

  • Supporter explicitement l’analyse des deux faces et fusionner les répertoires si nécessaire.
  • Ajouter une option CLI pour choisir le répertoire racine à scanner et le niveau de log.
  • Exporter les résultats sous forme CSV ou JSON pour intégration dans une base de données.
  • Affiner la détection des types de fichier en analysant plus d’octets du header ou en reconnaissant des signatures connues.
  • Ajouter des tests unitaires pour les fonctions critiques (is_valid_dir_entry, read_track, get_file_type).
  • Supprimer les noms de fichiers bizarre dedans afin d’avoir le même rendu que sur un cpc …

Conclusion

Ce script est un outil pratique pour automatiser l’inventaire d’images .dsk Amstrad CPC. Il combine parsing structuré et stratégies de récupération bruteforce pour maximiser les chances d’extraire un listing même sur des images imparfaites.




Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Auteur/autrice

sdgadmin@tux.ovh

Publications similaires

Dans

awstats-viewer

une application Yunohost qui affiche les stats d’AWStats,

Lire la suite
Dans

Arnold

The Amstrad CPC supports the following file formats: DSK disk image files TAP tape image files SNA snapshot files BIN snapshot files...

Lire la suite
Dans

Blackjack

compter les cartes L’astuce : augmenter les mises quand le paquet est favorable, et miser peu quand il ne l’est pas.

Lire la suite
Dans

cpcdb.py

Le script va maintenant : ✅ Se connecter au serveur MySQL ✅ Créer la table dsk_games dans la base cpcdb ✅ Scanner...

Lire la suite
Dans

iamuz.py

Script python pour télécharger le contenu d’une archive en fonction de l’id sur Internet Archive,

Lire la suite