/*******************************************************************************
* Copyright (c) 2008-2024 JackAsser, Krill, Claus, Björn Esser
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*******************************************************************************/

#define VERSION "4.2"

#define _CRT_SECURE_NO_WARNINGS /* avoid security warnings for MSVC */

#include <ctype.h>
#include <locale.h>
#include <math.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <wchar.h>

#ifdef _WIN32
#include <io.h>
#include <fcntl.h>
#endif

#define min(a, b) (((a) < (b)) ? (a) : (b))

#ifdef _WIN32
#include <windows.h>
#define FILESEPARATOR '\\'
#else
#define FILESEPARATOR '/'
#endif

#define DIRENTRIESPERBLOCK     8
#define DIRTRACK_D41_D71       18
#define DIRTRACK_D81           40
#define SECTORSPERTRACK_D81    40
#define MAXNUMFILES_D81        ((SECTORSPERTRACK_D81 - 3) * DIRENTRIESPERBLOCK)
#define DIRENTRYSIZE           32
#define BLOCKSIZE              256
#define BLOCKOVERHEAD          2
#define TRACKLINKOFFSET        0
#define SECTORLINKOFFSET       1
#define FILETYPEOFFSET         2
#define FILETYPEDEL            0
#define FILETYPESEQ            1
#define FILETYPEPRG            2
#define FILETYPEUSR            3
#define FILETYPEREL            4
#define FILETYPETRANSWARPMASK  0x100
#define FILETRACKOFFSET        3
#define FILESECTOROFFSET       4
#define FILENAMEOFFSET         5
#define FILENAMEMAXSIZE        16
#define FILENAMEEMPTYCHAR      (' ' | 0x80)
#define BAMMESSAGEOFFSET       0xab
#define BAMMESSAGEMAXSIZE      0x100-BAMMESSAGEOFFSET
#define TRANSWARPSIGNATROFFSLO 21
#define TRANSWARPSIGNATURELO   'T'
#define TRANSWARPSIGNATROFFSHI 22
#define TRANSWARPSIGNATUREHI   'W'
#define DIRDATACHECKSUMOFFSET  23
#define TRANSWARPTRACKOFFSET   24
#define FILECHECKSUMOFFSET     25
#define LOADADDRESSLOOFFSET    26
#define LOADADDRESSHIOFFSET    27
#define ENDADDRESSLOOFFSET     28
#define ENDADDRESSHIOFFSET     29
#define FILEBLOCKSLOOFFSET     30
#define FILEBLOCKSHIOFFSET     31
#define D64NUMBLOCKS           (664 + 19)
#define D64SIZE                (D64NUMBLOCKS * BLOCKSIZE)
#define D64SIZE_EXTENDED       (D64SIZE + 5 * 17 * BLOCKSIZE)
#define D71SIZE                (D64SIZE * 2)
#define D81SIZE                (D81NUMTRACKS * SECTORSPERTRACK_D81 * BLOCKSIZE)
#define D64NUMTRACKS           35
#define D64NUMTRACKS_EXTENDED  (D64NUMTRACKS + 5)
#define D71NUMTRACKS           (D64NUMTRACKS * 2)
#define D81NUMTRACKS           80
#define BAM_OFFSET_SPEED_DOS   0xc0
#define BAM_OFFSET_DOLPHIN_DOS 0xac
#define DIRSLOTEXISTS          0
#define DIRSLOTFOUND           1
#define DIRSLOTNOTFOUND        2
/* for sector chain analysis */
#define UNALLOCATED            0 /* unused as of now */
#define ALLOCATED              1 /* part of a valid sector chain */
#define FILESTART              2 /* analysed to be the start of a sector chain */
#define FILESTART_TRUNCATED    3 /* analysed to be the start of a sector chain, was truncated */
#define POTENTIALLYALLOCATED   4 /* currently being analysed */
/* error codes for sector chain validation */
#define VALID                  0 /* valid chain */
#define ILLEGAL_TRACK          1 /* ends with illegal track pointer */
#define ILLEGAL_SECTOR         2 /* ends with illegal sector pointer */
#define LOOP                   3 /* loop in current chain */
#define COLLISION              4 /* collision with other file */
#define CHAINED                5 /* ends at another file start */
#define CHAINED_TRUNCATED      6 /* ends at start of a truncated file */
#define FIRST_BROKEN           7 /* issue already in first sector */
/* undelete levels */
#define RESTORE_DIR_ONLY        0 /* Only restore all dir entries without touching any t/s links */
#define RESTORE_VALID_FILES     1 /* Fix dir entries for files with valid t/s chains */
#define RESTORE_VALID_CHAINS    2 /* Also add wild sector chains with valid t/s chains */
#define RESTORE_INVALID_FILES   3 /* Also fix dir entries with invalid t/s chains */
#define RESTORE_INVALID_CHAINS  4 /* Also add and fix wild invalid t/s chains */
#define RESTORE_INVALID_SINGLES 5 /* Also include single block files */
/* error codes for directory */
#define DIR_OK                 0
#define DIR_ILLEGAL_TS         1
#define DIR_CYCLIC_TS          2

#define TRANSWARP                "TRANSWARP"
#define TRANSWARPBASEBLOCKSIZE   0xc0
#define TRANSWARPBUFFERBLOCKSIZE 0x1f
#define TRANSWARPBLOCKSIZE       (TRANSWARPBASEBLOCKSIZE + TRANSWARPBUFFERBLOCKSIZE)
#define TRANSWARPKEYSIZE         29 /* 232 bits */
#define TRANSWARPKEYHASHROUNDS   33

/* maximum number of files for transwarp order permutation */
#define MAXPERMUTEDFILES 11
/* maximum number of free sectors on an extended D64 */
#define MAXFREESECTORS 768
/* maximum number of patches */
#define MAXNUMPATCHES 32
/* maximum number of patterns for file extraction */
#define MAXNUMEXTRACTIONS 32

/* Table for conversion of uppercase PETSCII to Unicode */
static unsigned int p2u_uppercase_tab[256] = {
    '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.',
    '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.',
    ' ', '!', '\"', '#', '$', '%', '&', 0x2019, '(', ')', '*', '+', ',', '-', '.', '/',
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?',
    '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
    'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', 0xa3, ']', 0x2191, 0x2190,
    0x1fb79, 0x2660, 0x1fb72, 0x1fb78, 0x1fb77, 0x1fb76, 0x1fb7a, 0x1fb71, 0x1fb74, 0x256e, 0x2570, 0x256f, 0x1fb7c, 0x2572, 0x2571, 0x1fb7d,
    0x1fb7e, 0x25cf, 0x1fb7b, 0x2665, 0x1fb70, 0x256d, 0x2573, 0x25cb, 0x2663, 0x1fb75, 0x2666, 0x253c, 0x1fb8c, 0x2502, 0x3c0, 0x25e5,
    '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.',
    '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.',
    ' ', 0x258c, 0x2584, 0x2594, 0x2581, 0x258e, 0x1fb95, 0x1fb75, 0x1fb8f, 0x25e4, 0x1fb75, 0x251c, 0x2597, 0x2514, 0x2510, 0x2581,
    0x250c, 0x2534, 0x252c, 0x2524, 0x258e, 0x258d, 0x2590, 0x2594, 0x2580, 0x2583, 0x1fb7f, 0x2596, 0x259d, 0x2518, 0x2598, 0x259a,
    0x2500, 0x2660, 0x1fb72, 0x2500, 0x1fb77, 0x1fb76, 0x1fb7a, 0x1fb71, 0x1fb74, 0x256e, 0x2570, 0x256f, 0x1fb7c, 0x2572, 0x2571, 0x1fb7d,
    0x1fb7e, 0x25cf, 0x1fb7b, 0x2665, 0x1fb70, 0x256d, 0x2573, 0x25cb, 0x2663, 0x1fb75, 0x2666, 0x253c, 0x1fb8c, 0x2502, 0x3c0, 0x25e5,
    ' ', 0x258c, 0x2584, 0x2594, 0x2581, 0x258e, 0x1fb95, 0x1fb75, 0x1fb8f, 0x25e4, 0x1fb75, 0x251c, 0x2597, 0x2514, 0x2510, 0x2581,
    0x250c, 0x2534, 0x252c, 0x2524, 0x258e, 0x258d, 0x1fb88, 0x1fb82, 0x1fb83, 0x2583, 0x1fb7f, 0x2596, 0x259d, 0x2518, 0x2598, 0x3c0
};

/* Table for conversion of lowercase PETSCII to Unicode */
static unsigned int p2u_lowercase_tab[256] = {
    '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.',
    '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.',
    ' ', '!', '\"', '#', '$', '%', '&', 0x2019, '(', ')', '*', '+', ',', '-', '.', '/',
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?',
    '@', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
    'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '[', 0xa3, ']', 0x2191, 0x2190,
    0x1fb79, 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
    'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 0x253c, 0x1fb8c, 0x2502, 0x1fb95, 0x1fb98,
    '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.',
    '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.',
    ' ', 0x258c, 0x2584, 0x2594, 0x2581, 0x258e, 0x1fb95, 0x1fb75, 0x1fb8f, 0x1fb99, 0x1fb75, 0x251c, 0x2597, 0x2514, 0x2510, 0x2581,
    0x250c, 0x2534, 0x252c, 0x2524, 0x258e, 0x258d, 0x2590, 0x2594, 0x2580, 0x2583, 0x1fb7f, 0x2713, 0x259d, 0x2518, 0x2598, 0x259a,
    0x2500, 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
    'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 0x253c, 0x1fb8c, 0x2502, 0x1fb95, 0x1fb98,
    ' ', 0x258c, 0x2584, 0x2594, 0x2581, 0x258e, 0x1fb95, 0x1fb75, 0x1fb8f, 0x1fb99, 0x1fb75, 0x251c, 0x2597, 0x2514, 0x2510, 0x2581,
    0x250c, 0x2534, 0x252c, 0x2524, 0x258e, 0x258d, 0x1fb88, 0x1fb82, 0x1fb83, 0x2583, 0x2713, 0x2596, 0x259d, 0x2518, 0x2598, 0x1fb96
};

typedef struct {
    const unsigned char* alocalname;                  /* local file name or name of loop file in ASCII */
    unsigned char        plocalname[FILENAMEMAXSIZE]; /* loop file in PETSCII */
    unsigned char        pfilename[FILENAMEMAXSIZE];  /* disk file name in PETSCII */
    int                  direntryindex;
    int                  direntrysector;
    int                  direntryoffset;
    int                  sectorInterleave;
    int                  first_sector_new_track;
    int                  track;
    int                  sector;
    int                  nrSectors;
    int                  nrSectorsShown;
    int                  filetype;
    int                  mode;
    int                  force_new;
    int                  size;
    int                  last_track;
    bool                 have_key;
    unsigned char        key[TRANSWARPKEYSIZE];
} imagefile;

typedef struct {
    const char* filename;
    int track;
    int sector;
    int offset;
} patch;

enum mode {
    MODE_BEGINNING_SECTOR_MASK   = 0x003f, /* 6 bits */
    MODE_MIN_TRACK_MASK          = 0x0fc0, /* 6 bits */
    MODE_MIN_TRACK_SHIFT         = 6,
    MODE_SAVETOEMPTYTRACKS       = 0x1000,
    MODE_FITONSINGLETRACK        = 0x2000,
    MODE_SAVECLUSTEROPTIMIZED    = 0x4000,
    MODE_LOOPFILE                = 0x8000,
    MODE_TRANSWARPBOOTFILE       = 0x10000,
    MODE_NOFILE                  = 0x20000
};

typedef enum {
    IMAGE_D64,
    IMAGE_D64_EXTENDED_SPEED_DOS,
    IMAGE_D64_EXTENDED_DOLPHIN_DOS,
    IMAGE_D71,
    IMAGE_D81
} image_type;

static const char *filetypename_uc[] = {
    "DEL", "SEQ", "PRG", "USR", "REL", "CBM", "???", "???",
    "???", "???", "???", "???", "???", "???", "???", "???"
};

static const char *filetypename_lc[] = {
    "del", "seq", "prg", "usr", "rel", "cbm", "???", "???",
    "???", "???", "???", "???", "???", "???", "???", "???"
};

static const char *error_name[] = {
    "no error", "illegal track", "illegal sector", "loop", "collision", "chained", "chained", "first block broken"
};

static const char *dir_error_string[] = {
    "no error", "illegal track or sector in directory sector chain", "cycle in directory sector chain"
};

static const int
sectors_per_track[] = {
    /*  1-17 */ 21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,
    /* 18-24 */ 19,19,19,19,19,19,19,
    /* 25-30 */ 18,18,18,18,18,18,
    /* 31-35 */ 17,17,17,17,17,
    /* 36-52 */ 21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,
    /* 53-59 */ 19,19,19,19,19,19,19,
    /* 60-65 */ 18,18,18,18,18,18,
    /* 66-70 */ 17,17,17,17,17
};

static const int
sectors_per_track_extended[] = {
    /*  1-17 */ 21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,
    /* 18-24 */ 19,19,19,19,19,19,19,
    /* 25-30 */ 18,18,18,18,18,18,
    /* 31-35 */ 17,17,17,17,17,
    /* 36-40 */ 17,17,17,17,17,
    /* 41-57 */ 21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,
    /* 58-64 */ 19,19,19,19,19,19,19,
    /* 65-70 */ 18,18,18,18,18,18,
    /* 71-75 */ 17,17,17,17,17,
    /* 76-80 */ 17,17,17,17,17
};

static int quiet           = 0;      /* global quiet flag */
static int verbose         = 0;      /* global verbose flag */
static int num_files       = 0;      /* number of files to be written provided by the user */
static int max_hash_length = 16;     /* number of bytes of the filenames to calculate the hash over */
static int unicode         = 0;      /* which unicode mapping to use: 0 = none, 1 = upper case, 2 = lower case */
static int modified        = 0;      /* image needs to be written */
static int dir_error       = DIR_OK; /* directory has an error */

/* Prints the command line help */
static void
usage(void)
{
    printf("\n*** This is cc1541 version " VERSION " built on " __DATE__ " ***\n\n");
    printf("Usage: cc1541 [options] image.[d64|d71|d81]\n\n");
    printf("Disk Related:\n");
    printf("============\n");
    printf("-n diskname   Disk name, default='cc1541'.\n");
    printf("-i id         Disk ID, default='00 2a'.\n");
    printf("-4            Use tracks 35-40 with SPEED DOS BAM formatting.\n");
    printf("-5            Use tracks 35-40 with DOLPHIN DOS BAM formatting.\n");
    printf("-d track      Maintain a shadow directory (copy of the actual directory without\n");
    printf("              a valid BAM).\n");
    printf("-g filename   Write additional g64 output file with given name.\n");
    printf("-H message    Hidden BAM message. Only for D64 (up to 85 chars) or SPEED DOS\n");
    printf("              (up to 20 chars).\n");
    printf("-p t,s,o,file Write binary patch from file to track t, sector s, offset o.\n");
    printf("-V            Do not modify image unless it is in valid CBM DOS format.\n\n");
    printf("File Related:\n");
    printf("============\n");
    printf("-w localname  Write local file to disk, if filename is not set then the\n");
    printf("              local name is used. After file written, the filename is unset.\n");
    printf("-f filename   Use filename as name when writing next file, use prefix # to\n");
    printf("              include arbitrary PETSCII characters (e.g. -f \"start#a0,8,1\").\n");
    printf("-T filetype   Filetype for next file, allowed parameters are PRG, SEQ, USR, REL\n");
    printf("              and DEL, or a decimal number between 0 and 255. Default is PRG.\n");
    printf("-P            Set write protect flag for next file.\n");
    printf("-O            Set open flag for next file.\n");
    printf("-B numblocks  Write the given value as file size in blocks to the directory for\n");
    printf("              the next file. Not applicable for Transwarp files.\n");
    printf("-L            Add dir entry without writing file (track and sector will be 0),\n");
    printf("              requires a filename given with -f.\n");
    printf("-l filename   Write loop file (an additional dir entry) to existing file to\n");
    printf("              disk. Wildcards * and ? are supported. Set filename with -f.\n");
    printf("-N            Force creation of a new directory entry, even if a file with the\n");
    printf("              same name exists already.\n");
    printf("-o            Do not overwrite if file with same name exists already.\n");
    printf("-X filename   Extract file from disk image. Wildcards * and ? are supported.\n\n");
    printf("Track/Sector Control:\n");
    printf("====================\n");
    printf("-S value      Default sector interleave, default=10. Not applicable for D81.\n");
    printf("-s value      Next file sector interleave, valid after each file.\n");
    printf("              The interleave value falls back to the default value set by -S\n");
    printf("              after the first sector of the next file. Not applicable for D81\n");
    printf("              or Transwarp files.\n");
    printf("-e            Start next file on an empty track (default start sector is\n");
    printf("              current sector plus interleave).\n");
    printf("-E            Try to fit file on a single track.\n");
    printf("-r track      Restrict next file blocks to the specified track or higher.\n");
    printf("-b sector     Set next file beginning sector to the specified value.\n");
    printf("              Not applicable for D81 or Transwarp files.\n");
    printf("-F value      Next file first sector on a new track (default=0).\n");
    printf("              Any negative value assumes aligned tracks and uses current\n");
    printf("              sector + interleave - value. After each file, the value falls\n");
    printf("              back to the default. Not applicable for D81.\n");
    printf("-t            Use directory track to also store files (makes -x useless)\n");
    printf("              (default no).\n");
    printf("-u numblocks  When using -t, amount of dir blocks to leave free (default=2).\n");
    printf("-x            Don't split files over directory track hole (default split files).\n");
    printf("-c            Save next file cluster-optimized (d71 only).\n\n");
    printf("Fastloader Specific:\n");
    printf("===================\n");
    printf("-M numchars   Hash computation maximum filename length, this must\n");
    printf("              match loader option FILENAME_MAXLENGTH in Krill's loader.\n");
    printf("              Default is 16.\n");
    printf("-m            Ignore filename hash collisions, without this switch a collision\n");
    printf("              results in an error.\n");
    printf("-W localname  Like -w, but encode file in Transwarp format.\n");
    printf("              Provide Transwarp bootfile as last file using\n");
    printf("              \"-f 'transwarp vX.YZ' -w 'transwarp vX.YZ.prg'\"\n");
    printf("-A value      Select allocation strategy for Transwarp files (default 1).\n");
    printf("              0: allocate in the order given on the commandline.\n");
    printf("              1: save disk space by reordering transwarp files.\n");
    printf("-K key        Set an encryption key for Transwarp files, a string of up to 29\n");
    printf("              characters.\n\n");
    printf("Miscellaneous:\n");
    printf("=============\n");
    printf("-h            Print this command line help.\n");
    printf("-v            Be verbose.\n");
    printf("-q            Be quiet.\n");
    printf("-U mapping    Print PETSCII as Unicode (requires Unicode 13.0 font, e.g.\n");
    printf("              UNSCII). Use mapping 0 for ASCII output, 1 for upper case, 2 for\n");
    printf("              lower case, default is 0.\n");
    printf("-R level      Try to restore deleted and formatted files.\n");
    printf("              level 0: Only restore dir entries without touching any t/s links.\n");
    printf("              level 1: Fix dir entries for files with valid t/s chains.\n");
    printf("              level 2: Also add wild sector chains with valid t/s chains.\n");
    printf("              level 3: Also fix dir entries with invalid t/s chains.\n");
    printf("              level 4: Also add and fix wild invalid t/s chains.\n");
    printf("              level 5: Also add reasonable wild single blocks.\n");
    printf("-a            Print command line options that would create the same directory as\n");
    printf("              the one in the given image (for directory art import).\n");
    printf("\n");
    printf("Find a more extensive online documentation here:\n");
    printf("https://acoustic-velocity.com/cc1541\n");
    printf("\n");

    exit(-1);
}

/* Returns a pointer to the filename in a path */
static const unsigned char*
basename(const unsigned char* path)
{
    const unsigned char* name;
    (name = (unsigned char*)strrchr((char *)path, FILESEPARATOR)) ? ++name : (name = path);
    return name;
}

/* Calculates a hash from a filename to be used by Krill's loader */
static unsigned int
filenamehash(const unsigned char *filename)
{
    int pos = min(max_hash_length, (int)strlen((char *)filename));
    while ((pos > 0) && (((unsigned char) filename[pos - 1]) == FILENAMEEMPTYCHAR)) {
        --pos;
    }

    unsigned char hashlo = pos;
    unsigned char hashhi = pos;
    int carry = 1;

    for (int i = pos - 1; i >= 0; --i) {
        unsigned int sum = hashlo + filename[i] + carry;
        carry = (sum >= 256) ? 1 : 0;
        sum &= 0xff;
        hashlo = sum;
        sum += hashhi + carry;
        carry = (sum >= 256) ? 1 : 0;
        hashhi = sum;
    }

    return (hashhi << 8) | hashlo;
}

/* Returns the size of an image in bytes */
static unsigned int
image_size(image_type type)
{
    switch (type) {
    case IMAGE_D64:
        return D64SIZE;

    case IMAGE_D64_EXTENDED_SPEED_DOS:
    /* fall through */
    case IMAGE_D64_EXTENDED_DOLPHIN_DOS:
        return D64SIZE_EXTENDED;

    case IMAGE_D71:
        return D71SIZE;

    case IMAGE_D81:
        return D81SIZE;

    default:
        return 0;
    }
}

/* Returns the number of tracks in an image */
static unsigned int
image_num_tracks(image_type type)
{
    switch (type) {
    case IMAGE_D64:
        return D64NUMTRACKS;

    case IMAGE_D64_EXTENDED_SPEED_DOS:
    /* fall through */
    case IMAGE_D64_EXTENDED_DOLPHIN_DOS:
        return D64NUMTRACKS_EXTENDED;

    case IMAGE_D71:
        return D71NUMTRACKS;

    case IMAGE_D81:
        return D81NUMTRACKS;

    default:
        return 0;
    }
}

/* Returns the number of sectors for a given track */
static int
num_sectors(image_type type, int track)
{
    return (type == IMAGE_D81) ? SECTORSPERTRACK_D81
           : (((type == IMAGE_D64_EXTENDED_SPEED_DOS) || (type == IMAGE_D64_EXTENDED_DOLPHIN_DOS)) ? sectors_per_track_extended[track - 1]
              : sectors_per_track[track - 1]);
}

/* Returns the number of blocks in an image */
static unsigned int
image_num_blocks(image_type type)
{
    int num_blocks = 0;
    for (unsigned int t = 1; t <= image_num_tracks(type); t++) {
        num_blocks += num_sectors(type, t);
    }
    return num_blocks;
}

/* Returns the directory track of an image */
static int
dirtrack(image_type type)
{
    return (type == IMAGE_D81) ? DIRTRACK_D81 : DIRTRACK_D41_D71;
}

/* Converts an ASCII character to PETSCII */
static unsigned char
a2p(unsigned char a)
{
    switch (a) {
    case '\n':
        return 0x0d;
    case '_':
        return 0xa4;
    case 0x7e:
        return 0xff;
    default:
        if ((a >= 0x5b) && (a < 0x5f)) {
            return a;
        }
        if ((a >= 0x60) && (a <= 0x7e)) {
            return a ^ 0x20;
        }
        if ((a >= 'A') && (a <= 'Z')) {
            return a | 0x80;
        }
        return a;
    }
}

/* Converts a PETSCII character to ASCII */
static unsigned char
p2a(unsigned char p)
{
    switch (p) {
    case 0x0a:
    case 0x0d:
        return '\n';
    case 0x40:
    case 0x60:
        return p;
    case 0xa0:
    case 0xe0:
        return ' ';
    case 0xa4:
    case 0xe4:
        return '_';
    default:
        switch (p & 0xe0) {
        case 0x40:
        case 0x60:
            p ^= 0x20;
            break;
        case 0xc0:
            p ^= 0x80;
            break;
        }
    }
    return ((isprint(p) ? p : '.'));
}

/* Converts a PETSCII character to ASCII if possible, returns negative PETSCII code otherwise */
static int
p2a_escape(unsigned char p)
{
    if((p >= 32 && p <= 122) && (p != 33) && (p != 34) && (p != 35) && (p != 36) && (p != 92) && (p != 94) && (p != 95) && (p != 96)) {
        switch (p & 0xe0) {
        case 0x40:
        case 0x60:
            return p ^ 0x20;
            break;
        default:
            return p;
        }
    } else {
        return -p;
    }
}

/* Converts a PETSCII string to ASCII with hex escapes where needed, esacpe slash if slash false */
static char*
petscii2ascii(const unsigned char* petscii, char* ascii, int len, bool slash)
{
    /* find end of petscii string */
    while(len > 1 && petscii[len-1] == 0xa0) {
        len--;
    }
    int apos = 0;
    for(int ppos = 0; ppos < len; ppos++) {
        int a = p2a_escape(petscii[ppos]);
        if(!slash && (a == 47)) {
            a = -a; /* force slash to be escaped */
        }
        if(a < 0) {
            snprintf(ascii+apos, 4, "#%02x", -a);
            apos += 3;
        } else {
            ascii[apos++] = a;
        }
    }
    ascii[apos] = 0;
    return ascii;
}

/* Converts an ASCII string to PETSCII filled up with Shift-Space to length */
static void
ascii2petscii(const unsigned char* ascii, unsigned char* petscii, int len)
{
    int pos = 0;
    while (ascii[pos] != '\0' && pos < len) {
        petscii[pos] = a2p(ascii[pos]);
        ++pos;
    }
    while (pos < len) {
        petscii[pos] = FILENAMEEMPTYCHAR;
        ++pos;
    }
}

/* Prints a PETSCII filename as ASCII with escapes */
static void
print_filename_with_escapes(const unsigned char* petscii, int len)
{
    char ascii[3 * FILENAMEMAXSIZE + 1];
    fputs(petscii2ascii(petscii, ascii, len, true), stdout);
}

/* Determines length of 0xa0 terminated PETSCII string */
static int
pstrlen(const unsigned char* string)
{
    int len = 0;
    while(string[len] != 0xa0 && len < FILENAMEMAXSIZE) {
        len++;
    }
    return len;
}

/* Writes decimal number into PETSCII string */
#define PPUTNUM_BUF_LEN 17
static void
pputnum(unsigned char* string, unsigned int num)
{
    char buffer[PPUTNUM_BUF_LEN];
    snprintf(buffer, PPUTNUM_BUF_LEN, "%d", num);
    for(unsigned int len = 0; len < strlen(buffer); len++) {
        string[len] = a2p(buffer[len]);
    }
}

/* Writes 2 digit decimal number into PETSCII string */
#define PPUTNUM2_BUF_LEN 3
static void
pputnum2(unsigned char* string, unsigned int num)
{
    char buffer[PPUTNUM2_BUF_LEN];
    snprintf(buffer, PPUTNUM2_BUF_LEN, "%02d", num);
    for(unsigned int len = 0; len < strlen(buffer); len++) {
        string[len] = a2p(buffer[len]);
    }
}

/* Writes 4 digit hex number into PETSCII string */
#define PPUTHEX_BUF_LEN 5
static void
pputhex(unsigned char* string, unsigned int num)
{
    char buffer[PPUTHEX_BUF_LEN];
    snprintf(buffer, PPUTHEX_BUF_LEN, "%04x", num);
    for(int len = 0; len < 4; len++) {
        string[len] = a2p(buffer[len]);
    }
}

/* Converts a two digit hex string to an int */
static unsigned int
hex2int(char hex)
{
    if ((hex < '0' || hex > '9') && (hex < 'a' || hex > 'f') && (hex < 'A' || hex > 'F')) {
        fprintf(stderr, "ERROR: Invalid hex string in filename\n");
        exit(-1);
    }
    if (hex <= '9') {
        hex -= '0';
    } else if(hex >= 'A' && hex <= 'F') {
        hex -= 'A' - 10;
    } else if(hex >= 'a' && hex <= 'f') {
        hex -= 'a' - 10;
    }
    return (unsigned int)hex;
}

/* Converts an ASCII string to PETSCII with escape evaluation, filled up with emptychar to length */
static void
evalhexescape(const unsigned char* ascii, unsigned char* petscii, int len, unsigned char emptychar)
{
    int read = 0, write = 0;

    while (ascii[read] != '\0' && write < len) {
        if (ascii[read] == '#') {
            unsigned int hi = hex2int(ascii[++read]);
            unsigned int lo = hex2int(ascii[++read]);
            petscii[write] = (unsigned char)(16 * hi + lo);
        } else {
            petscii[write] = a2p(ascii[read]);
        }
        read++;
        write++;
    }
    while (write < len) {
        petscii[write] = emptychar;
        ++write;
    }
}

/* Converts a unicode character to utf8 */
char* utf8_encode(int c, char* out)
{
    if (c < 0x80)
        *out++ =(c & 0xff);
    else if (c < 0x800) {
        *out++ = (0xC0 | ((c >> 6) & 0x1f));
        *out++ = (0x80 | (c & 0x3f));
    } else if (c < 0x10000) {
        *out++ = (0xE0 | ((c >> 12) & 0xf));
        *out++ = (0x80 | ((c >> 6) & 0x3f));
        *out++ = (0x80 | (c & 0x3f));
    } else {
        *out++ = (0xF0 | ((c >> 18) & 0x07));
        *out++ = (0x80 | ((c >> 12) & 0x3f));
        *out++ = (0x80 | ((c >> 6) & 0x3f));
        *out++ = (0x80 | (c & 0x3f));
    }
    return out;
}

#ifdef _WIN32
/* Enables console formatting under Windows if possible */
static bool
EnableVTMode()
{
    HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
    if (hOut == INVALID_HANDLE_VALUE) {
        return false;
    }
    DWORD dwMode = 0;
    if (!GetConsoleMode(hOut, &dwMode)) {
        return false;
    }
    dwMode |= 0x0004; /* ENABLE_VIRTUAL_TERMINAL_PROCESSING not defined for older SDKs */
    if (!SetConsoleMode(hOut, dwMode)) {
        return false;
    }
    return true;
}
#endif

/* Enables reverse printing to console */
static void
reverse_print_on(void)
{
#ifdef _WIN32
    /* Avoid escape values for inverse printing under Windows if they are not supported by the console */
    if (EnableVTMode()) {
        printf("\033[7m");
    }
#else
    printf("\033[7m");
#endif
}

/* Disables reverse printing to console */
static void
reverse_print_off(void)
{
#ifdef _WIN32
    /* Avoid escape values for inverse printing under Windows if they are not supported by the console */
    if (EnableVTMode()) {
        printf("\033[m");
    }
#else
    printf("\033[m");
#endif
}

/* Prints a PETSCII character */
static void
putp(unsigned char petscii, FILE *file)
{
    if (unicode) {
        int u;
        int reverse = 0;

        if (petscii < 0x20 || (petscii >= 0x80 && petscii <= 0x9f)) {
            reverse = 1;
            reverse_print_on();
            petscii += 0x40;
        }

        if (unicode == 1) {
            u = p2u_uppercase_tab[petscii];
        } else {
            u = p2u_lowercase_tab[petscii];
        }

#ifdef _WIN32
        _setmode(_fileno(file), _O_U16TEXT);
        putwc(u, file);
        _setmode(_fileno(file), _O_TEXT);
#else
        {
            char temp[5];
            *utf8_encode(u, temp) = 0; /* fancy way to zero-terminate temp after conversion */
            fputs(temp, file);
        }
#endif

        if(reverse) {
            reverse_print_off();
        }
    } else {
        putc(p2a(petscii), file);
    }
}

/* Prints a PETSCII string */
static void
print_petscii(unsigned char *petscii, int len)
{
    for(int i = 0; i < len; i++) {
        putp(petscii[i], stdout);
    }
}

/* Prints the given PETSCII filename like the C64 when listing the directory */
static void
print_dirfilename(unsigned char* pfilename)
{
    int ended = 0;
    putc('\"', stdout);
    for (int pos = 0; pos < FILENAMEMAXSIZE; pos++) {
        unsigned char c = pfilename[pos];
        if (c == FILENAMEEMPTYCHAR) {
            if (!ended) {
                putc('\"', stdout);
                ended = 1;
            } else {
                putc(' ', stdout);
            }
        } else {
            putp(c, stdout);
        }
    }
    if (!ended) {
        putc('\"', stdout);
    } else {
        putc(' ', stdout);
    }
}

/* Prints the given PETSCII filename */
static void
print_filename(FILE *file, unsigned char* pfilename)
{
    putc('\"', file);
    for (int pos = 0; pos < FILENAMEMAXSIZE; pos++) {
        if (pfilename[pos] == FILENAMEEMPTYCHAR) {
            break;
        }
        putp(pfilename[pos], file);
    }
    putc('\"', file);
}

/* Calculates the overall sector index from a given track and sector, returns -1 for invalid track/sector */
static int
linear_sector(image_type type, int track, int sector)
{
    if ((track < 1) || (track > ((type == IMAGE_D81) ? D81NUMTRACKS : ((type == IMAGE_D64) ? D64NUMTRACKS : (type == IMAGE_D71 ? D71NUMTRACKS : D64NUMTRACKS_EXTENDED))))) {
        return -1;
    }

    int numsectors = num_sectors(type, track);
    if ((sector < 0) || (sector >= numsectors)) {
        return -1;
    }

    int linear_sector = 0;
    for (int i = 0; i < track - 1; i++) {
        linear_sector += num_sectors(type, i + 1);
    }
    linear_sector += sector;

    return linear_sector;
}

/* Returns the image offset of the bam entry for a given track */
static int
get_bam_offset(image_type type, unsigned int track)
{
    int bam;
    unsigned int offset;

    if (type == IMAGE_D81) {
        if (track <= 40) {
            bam = linear_sector(type, dirtrack(type), 1 /* sector */) * BLOCKSIZE;
            offset = bam + (track * 6) + 11;
        } else {
            bam = linear_sector(type, dirtrack(type), 2 /* sector */) * BLOCKSIZE;
            offset = bam + ((track - 40) * 6) + 11;
        }
    } else if ((type == IMAGE_D71) && (track > D64NUMTRACKS)) {
        /* access second side bam */
        bam = linear_sector(type, dirtrack(type) + D64NUMTRACKS, 0) * BLOCKSIZE;
        offset = bam + (track - D64NUMTRACKS - 1) * 3;
    } else {
        if (((type == IMAGE_D64_EXTENDED_SPEED_DOS) || (type == IMAGE_D64_EXTENDED_DOLPHIN_DOS)) && (track > D64NUMTRACKS)) {
            track -= D64NUMTRACKS;
            bam = linear_sector(type, dirtrack(type), 0) * BLOCKSIZE + ((type == IMAGE_D64_EXTENDED_SPEED_DOS) ? BAM_OFFSET_SPEED_DOS : BAM_OFFSET_DOLPHIN_DOS);
        } else {
            bam = linear_sector(type, dirtrack(type), 0) * BLOCKSIZE;
        }
        offset = bam + track * 4 + 1;
    }
    return offset;
}

/* Checks if a given sector is marked as free in the BAM and also not used by directory */
static int
is_sector_free(image_type type, const unsigned char* image, int track, int sector, int numdirblocks, int dir_sector_interleave)
{
    int bam;
    const unsigned char* bitmap;

    if (sector < 0) {
        fprintf(stderr, "ERROR: Illegal sector %d for track %d\n", sector, track);

        exit(-1);
    }

    if (type == IMAGE_D81) {
        if (track <= 40) {
            bam = linear_sector(type, dirtrack(type), 1 /* sector */) * BLOCKSIZE;
            bitmap = image + bam + (track * 6) + 11;
        } else {
            bam = linear_sector(type, dirtrack(type), 2 /* sector */) * BLOCKSIZE;
            bitmap = image + bam + ((track - 40) * 6) + 11;
        }
    } else if ((type == IMAGE_D71) && (track > D64NUMTRACKS)) {
        /* access second side bam */
        bam = linear_sector(type, dirtrack(type) + D64NUMTRACKS, 0) * BLOCKSIZE;
        bitmap = image + bam + (track - D64NUMTRACKS - 1) * 3;
    } else {
        if (((type == IMAGE_D64_EXTENDED_SPEED_DOS) || (type == IMAGE_D64_EXTENDED_DOLPHIN_DOS)) && (track > D64NUMTRACKS)) {
            track -= D64NUMTRACKS+1;
            bam = linear_sector(type, dirtrack(type), 0) * BLOCKSIZE + ((type == IMAGE_D64_EXTENDED_SPEED_DOS) ? BAM_OFFSET_SPEED_DOS : BAM_OFFSET_DOLPHIN_DOS);
        } else {
            bam = linear_sector(type, dirtrack(type), 0) * BLOCKSIZE;
        }
        bitmap = image + bam + (track * 4) + 1;
    }

    int byte = sector >> 3;
    int bit = sector & 7;

    int is_not_dir_block = 1;
    if ((track == dirtrack(type)) && (numdirblocks > 0)) {
        int dirsector = 0;
        int s = 2;
        for (int i = 0; is_not_dir_block && (i < numdirblocks); i++) {
            switch (i) {
            case 0:
                dirsector = 0;
                break;

            case 1:
                dirsector = 1;
                break;

            default:
                dirsector += dir_sector_interleave;
                if (dirsector >= num_sectors(type, track)) {
                    dirsector = s;
                    s++;
                }
                break;
            }
            is_not_dir_block = (sector != dirsector);
        }
    }

    return is_not_dir_block && ((bitmap[byte] & (1 << bit)) != 0);
}

/* Marks given sector in BAM */
static void
mark_sector(image_type type, unsigned char* image, int track, int sector, int free)
{
    if (free != is_sector_free(type, image, track, sector, 0, 0)) {
        int bam;
        unsigned char* bitmap;
        if (type == IMAGE_D81) {
            if (track <= 40) {
                bam = linear_sector(type, dirtrack(type), 1 /* sector */) * BLOCKSIZE;
                bitmap = image + bam + (track * 6) + 11;
            } else {
                bam = linear_sector(type, dirtrack(type), 2 /* sector */) * BLOCKSIZE;
                bitmap = image + bam + ((track - 40) * 6) + 11;
            }

            /* update number of free sectors on track */
            if (free) {
                ++bitmap[-1];
            } else {
                --bitmap[-1];
            }
        } else if ((type == IMAGE_D71) && (track > D64NUMTRACKS)) {
            /* update number of free sectors on track */
            bam = linear_sector(type, dirtrack(type), 0) * BLOCKSIZE;
            if (free) {
                image[bam + 0xdd + track - D64NUMTRACKS - 1]++;
            } else {
                image[bam + 0xdd + track - D64NUMTRACKS - 1]--;
            }

            /* access bitmap on second side bam */
            bam = linear_sector(type, dirtrack(type) + D64NUMTRACKS, 0) * BLOCKSIZE;
            bitmap = image + bam + (track - D64NUMTRACKS - 1) * 3;
        } else {
            if (((type == IMAGE_D64_EXTENDED_SPEED_DOS) || (type == IMAGE_D64_EXTENDED_DOLPHIN_DOS)) && (track > D64NUMTRACKS)) {
                track -= D64NUMTRACKS+1;
                bam = linear_sector(type, dirtrack(type), 0) * BLOCKSIZE + ((type == IMAGE_D64_EXTENDED_SPEED_DOS) ? BAM_OFFSET_SPEED_DOS : BAM_OFFSET_DOLPHIN_DOS);
            } else {
                bam = linear_sector(type, dirtrack(type), 0) * BLOCKSIZE;
            }
            bitmap = image + bam + (track * 4) + 1;

            /* update number of free sectors on track */
            if (free) {
                ++image[bam + (track * 4)];
            } else {
                --image[bam + (track * 4)];
            }
        }

        /* update bitmap */
        int byte = sector >> 3;
        int bit = sector & 7;

        if (free) {
            bitmap[byte] |= 1 << bit;
        } else {
            bitmap[byte] &= ~(1 << bit);
        }
    }
}

/* Returns offset for header on directory track */
static int
get_header_offset(image_type type)
{
    int offset;

    if (type == IMAGE_D81) {
        offset = 4;
    } else {
        offset = 0x90;
    }
    return offset;
}

/* Returns offset for id on directory track */
static int
get_id_offset(image_type type)
{
    int offset;

    if (type == IMAGE_D81) {
        offset = 0x16;
    } else {
        offset = 0xa2;
    }
    return offset;
}

/* Updates the directory with the given header, id and BAM message */
static void
update_directory(image_type type, unsigned char* image, unsigned char* header, unsigned char* id, unsigned char *bam_message, int shadowdirtrack)
{
    unsigned int bam = linear_sector(type, dirtrack(type), 0) * BLOCKSIZE;

    if (type != IMAGE_D81) {
        image[bam + 0x03] = (type == IMAGE_D71) ? 0x80 : 0x00;
    }

    /* Set header and ID */
    unsigned char pheader[FILENAMEMAXSIZE];
    unsigned char pid[5];
    evalhexescape(header, pheader, FILENAMEMAXSIZE, FILENAMEEMPTYCHAR);
    evalhexescape(id, pid, 5, FILENAMEEMPTYCHAR);
    memcpy(image + bam + get_header_offset(type), pheader, FILENAMEMAXSIZE);
    memcpy(image + bam + get_id_offset(type), pid, 5);

    /* Set BAM message */
    if(bam_message != NULL) {
        unsigned char pbam_message[BAMMESSAGEMAXSIZE];
        int bam_message_len = BAMMESSAGEMAXSIZE;
        if(type == IMAGE_D64_EXTENDED_SPEED_DOS) {
            bam_message_len = 0xbf - BAMMESSAGEOFFSET; /* avoid conflict with extended BAM and allow for a 0 at the end*/
        }
        evalhexescape(bam_message, pbam_message, bam_message_len, 0);
        memcpy(image + bam + BAMMESSAGEOFFSET, pbam_message, bam_message_len);
    }

    if (type == IMAGE_D81) {
        unsigned int bam = linear_sector(type, dirtrack(type), 1 /* sector */) * BLOCKSIZE;
        image[bam + 0x04] = id[0];
        image[bam + 0x05] = id[1];

        bam = linear_sector(type, dirtrack(type), 2 /* sector */) * BLOCKSIZE;
        image[bam + 0x04] = id[0];
        image[bam + 0x05] = id[1];
    }

    if (shadowdirtrack > 0) {
        unsigned int shadowbam = linear_sector(type, shadowdirtrack, 0 /* sector */) * BLOCKSIZE;
        memcpy(image + shadowbam, image + bam, BLOCKSIZE);

        image[shadowbam + 0x00] = shadowdirtrack;
    }
}

/* Writes an empty directory and BAM */
static void
initialize_directory(image_type type, unsigned char* image, unsigned char* header, unsigned char* id, unsigned char * bam_message, int shadowdirtrack, int usedirtrack)
{
    unsigned int dir = linear_sector(type, dirtrack(type), 0 /* sector */) * BLOCKSIZE;

    /* Clear image */
    memset(image, 0, image_size(type));

    /* Write initial BAM */
    if (type == IMAGE_D81) {
        image[dir + 0x00] = dirtrack(type);
        image[dir + 0x01] = 3;
        image[dir + 0x02] = 0x44;

        image[dir + 0x14] = FILENAMEEMPTYCHAR;
        image[dir + 0x15] = FILENAMEEMPTYCHAR;

        image[dir + 0x1b] = FILENAMEEMPTYCHAR;
        image[dir + 0x1c] = FILENAMEEMPTYCHAR;

        unsigned int bam = linear_sector(type, dirtrack(type), 1 /* sector */) * BLOCKSIZE;
        image[bam + 0x00] = dirtrack(type);
        image[bam + 0x01] = 2;
        image[bam + 0x02] = 0x44;
        image[bam + 0x03] = 0xbb;
        image[bam + 0x06] = 0xc0;

        bam = linear_sector(type, dirtrack(type), 2 /* sector */) * BLOCKSIZE;
        image[bam + 0x00] = 0;
        image[bam + 0x01] = 255;
        image[bam + 0x02] = 0x44;
        image[bam + 0x03] = 0xbb;
        image[bam + 0x06] = 0xc0;
    } else {
        image[dir + 0x00] = dirtrack(type);
        image[dir + 0x01] = 1;
        image[dir + 0x02] = 0x41;
        image[dir + 0x03] = (type == IMAGE_D71) ? 0x80 : 0x00;

        image[dir + 0xa0] = FILENAMEEMPTYCHAR;
        image[dir + 0xa1] = FILENAMEEMPTYCHAR;

        image[dir + 0xa7] = FILENAMEEMPTYCHAR;
        image[dir + 0xa8] = FILENAMEEMPTYCHAR;
        image[dir + 0xa9] = FILENAMEEMPTYCHAR;
        image[dir + 0xaa] = FILENAMEEMPTYCHAR;
    }

    /* Mark all sectors unused */
    for (unsigned int t = 1; t <= image_num_tracks(type); t++) {
        for (int s = 0; s < num_sectors(type, t); s++) {
            mark_sector(type, image, t, s, 1 /* free */);
        }
    }

    /* Reserve space for BAM */
    mark_sector(type, image, dirtrack(type), 0 /* sector */, 0 /* not free */);
    if (type == IMAGE_D71) {
        mark_sector(type, image, dirtrack(type) + D64NUMTRACKS, 0, 0 /* not free */);
        if(!usedirtrack) {
            for (int s = 1; s < num_sectors(type, dirtrack(type) + D64NUMTRACKS); s++) {
                mark_sector(type, image, dirtrack(type) + D64NUMTRACKS, s, 0 /* not free */);
            }
        }
    } else if (type == IMAGE_D81) {
        mark_sector(type, image, dirtrack(type), 1 /* sector */, 0 /* not free */);
        mark_sector(type, image, dirtrack(type), 2 /* sector */, 0 /* not free */);
    }

    /* first dir block */
    unsigned int dirblock = linear_sector(type, dirtrack(type), (type == IMAGE_D81) ? 3 : 1) * BLOCKSIZE;
    image[dirblock + SECTORLINKOFFSET] = 255;
    mark_sector(type, image, dirtrack(type), (type == IMAGE_D81) ? 3 : 1 /* sector */, 0 /* not free */);

    if (shadowdirtrack > 0) {
        dirblock = linear_sector(type, shadowdirtrack, (type == IMAGE_D81) ? 3 : 1 /* sector */) * BLOCKSIZE;
        image[dirblock + SECTORLINKOFFSET] = 255;

        mark_sector(type, image, shadowdirtrack, 0 /* sector */, 0 /* not free */);
        mark_sector(type, image, shadowdirtrack, (type == IMAGE_D81) ? 3 : 1 /* sector */, 0 /* not free */);
    }

    update_directory(type, image, header, id, bam_message, shadowdirtrack);
}

/* Computes Transwarp dirdata checksum */
static unsigned char
transwarp_dirdata_checksum(const unsigned char *image, int dir_entry_offset)
{
    int dirdata_checksum = 0;
    int carry = 1;
    for (int offset = DIRDATACHECKSUMOFFSET; offset <= FILEBLOCKSLOOFFSET; ++offset) {
        dirdata_checksum += (image[dir_entry_offset + offset] + carry);
        carry = (dirdata_checksum >= 0x0100) ? 1 : 0;
        dirdata_checksum &= 0xff;
    }

    return dirdata_checksum;
}

/* Checks if a given dir entry points to a Transwarp file */
static bool
is_transwarp_file(const unsigned char *image, int dir_entry_offset)
{
    return (image[dir_entry_offset + TRANSWARPSIGNATROFFSLO] == TRANSWARPSIGNATURELO)
           && (image[dir_entry_offset + TRANSWARPSIGNATROFFSHI] == TRANSWARPSIGNATUREHI)
           && (transwarp_dirdata_checksum(image, dir_entry_offset) == 0);
}

/* Checks if a given dir entry points to the Transwarp bootfile */
static bool
is_transwarp_bootfile(const unsigned char *image, int dir_entry_offset)
{
    return memcmp(image + dir_entry_offset + FILENAMEOFFSET, TRANSWARP, strlen(TRANSWARP)) == 0;
}

/* Return Transwarp file stat */
static int
transwarp_stat(image_type type, const unsigned char *image, int dir_entry_offset, int *start_track, int *end_track, int *low_track, int *high_track)
{
    *start_track = 0;
    *end_track = 0;
    *low_track = 0;
    *high_track = 0;

    int filesize = (image[dir_entry_offset + ENDADDRESSLOOFFSET]  | (image[dir_entry_offset + ENDADDRESSHIOFFSET] << 8))
                   - (image[dir_entry_offset + LOADADDRESSLOOFFSET] | (image[dir_entry_offset + LOADADDRESSHIOFFSET] << 8));
    if (filesize <= 0) {
        return 0;
    }

    *start_track = image[dir_entry_offset + TRANSWARPTRACKOFFSET];
    *end_track = *start_track;

    int size = filesize;

    while (filesize > 0) {
        filesize -= (TRANSWARPBLOCKSIZE * num_sectors(type, *end_track));
        if (filesize > 0) {
            *end_track = (*end_track < DIRTRACK_D41_D71) ? (*end_track - 1) : (*end_track + 1);
        }
    }

    *low_track = (*start_track < *end_track) ? *start_track : *end_track;
    *high_track = (*start_track > *end_track) ? *start_track : *end_track;

    return size;
}

/* Return Transwarp file stat */
static int
transwarp_size(image_type type, int start_track, int end_track, int filesize,
               int *transwarp_blocks, int *nonredundant_blocks, int *redundant_blocks, int *nonredundant_blocks_on_last_track)
{
    *transwarp_blocks = 0;

    int last_track_sectors;
    if (start_track <= end_track) {
        for (int track = start_track; track <= end_track; ++track) {
            last_track_sectors = num_sectors(type, track);
            *transwarp_blocks += last_track_sectors;
        }
    } else {
        for (int track = start_track; track >= end_track; --track) {
            last_track_sectors = num_sectors(type, track);
            *transwarp_blocks += last_track_sectors;
        }
    }

    int spare_bytes = TRANSWARPBLOCKSIZE - (filesize % TRANSWARPBLOCKSIZE);
    if (spare_bytes == TRANSWARPBLOCKSIZE) {
        spare_bytes = 0;
    }

    *nonredundant_blocks = (filesize / TRANSWARPBLOCKSIZE) + ((spare_bytes == 0) ? 0 : 1);
    *redundant_blocks = *transwarp_blocks - *nonredundant_blocks;
    *nonredundant_blocks_on_last_track = last_track_sectors - *redundant_blocks;

    return spare_bytes;
}

/* Deletes a file from disk and BAM, but leaves the directory entry */
static void
wipe_file(image_type type, unsigned char* image, imagefile* file)
{
    int b = linear_sector(type, dirtrack(type), file->direntrysector) * BLOCKSIZE + file->direntryoffset;

    if (is_transwarp_file(image, b)) {
        int start_track;
        int end_track;
        int low_track;
        int high_track;
        int filesize = transwarp_stat(type, image, b, &start_track, &end_track, &low_track, &high_track);
        if (filesize <= 0) {
            fprintf(stderr, "ERROR: Cannot overwrite Transwarp file ");
            print_filename(stderr, file->pfilename);
            fprintf(stderr, "\n");

            exit(-1);
        }

        for (int track = low_track; track <= high_track; ++track) {
            for (int sector = 0; sector < num_sectors(type, track); ++sector) {
                int block_offset = linear_sector(type, track, sector) * BLOCKSIZE;
                memset(image + block_offset, 0, BLOCKSIZE);
                mark_sector(type, image, track, sector, 1 /* free */);
            }
        }

        return;
    }

    unsigned int track = image[b + FILETRACKOFFSET];
    unsigned int sector = image[b + FILESECTOROFFSET];

    if (sector >= 0x80) {
        return; /* loop file */
    }

    while (track != 0) {
        int block_offset = linear_sector(type, track, sector) * BLOCKSIZE;
        int next_track = image[block_offset + TRACKLINKOFFSET];
        int next_sector = image[block_offset + SECTORLINKOFFSET];
        memset(image + block_offset, 0, BLOCKSIZE); /* this also fixes any cyclic t/s chain */
        mark_sector(type, image, track, sector, 1 /* free */);
        track = next_track;
        sector = next_sector;
    }
}

/* Sets image offset to the next DIR entry, returns false when the DIR end was reached */
static bool
next_dir_entry(image_type type, const unsigned char* image, int *track, int *sector, int *offset, char *blockmap)
{
    int b = linear_sector(type, *track, *sector); /* assuming here that the given t/s is valid */
    blockmap[b] = 1;
    if (*offset % BLOCKSIZE == 7 * DIRENTRYSIZE) {
        /* last entry in sector */
        int next_track = image[b * BLOCKSIZE + TRACKLINKOFFSET];
        if (next_track == 0) {
            /* this was the last DIR sector */
            return false;
        }
        int next_sector = image[b * BLOCKSIZE + SECTORLINKOFFSET];
        b = linear_sector(type, next_track, next_sector);
        if(b < 0) {
            dir_error = DIR_ILLEGAL_TS;
            return false;
        }
        if(blockmap[b]) {
            dir_error = DIR_CYCLIC_TS;
            return false;
        }
        *track = next_track;
        *sector = next_sector;
        *offset = 0;
    } else {
        *offset += DIRENTRYSIZE;
    }
    return true;
}

/* Finds all filenames with the given hash */
static int
count_hashes(image_type type, const unsigned char* image, unsigned int hash, bool print)
{
    int num = 0;
    char *blockmap = calloc(image_num_blocks(type), sizeof(char));
    if(blockmap == NULL) {
        fprintf(stderr, "ERROR: Memory allocation error\n");
        exit(-1);
    }

    int ds = (type == IMAGE_D81) ? 3 : 1;
    int dt = dirtrack(type);
    int offset = 0;
    do {
        int dirblock = linear_sector(type, dt, ds) * BLOCKSIZE + offset;
        int filetype = image[dirblock + FILETYPEOFFSET];

        if (filetype != FILETYPEDEL) {
            unsigned char *filename = (unsigned char *) image + dirblock + FILENAMEOFFSET;
            if (hash == filenamehash(filename)) {
                ++num;

                if (print) {
                    printf(" [$%04x] ", filenamehash(filename));
                    print_filename(stdout, filename);
                    printf("\n");
                }
            }
        }
    } while (next_dir_entry(type, image, &dt, &ds, &offset, blockmap));
    free(blockmap);

    return num;
}

/* Checks if multiple filenames have the same hash */
static bool
check_hashes(image_type type, const unsigned char* image)
{
    bool collision = false;
    char *blockmap = calloc(image_num_blocks(type), sizeof(char));
    if(blockmap == NULL) {
        fprintf(stderr, "ERROR: Memory allocation error\n");
        exit(-1);
    }
    printf("\n");

    int ds = (type == IMAGE_D81) ? 3 : 1;
    int dt = dirtrack(type);
    int offset = 0;
    do {
        int dirblock = linear_sector(type, dt, ds) * BLOCKSIZE + offset;
        int filetype = image[dirblock + FILETYPEOFFSET];

        if (filetype != FILETYPEDEL) {
            unsigned char *filename = (unsigned char *) image + dirblock + FILENAMEOFFSET;
            if (count_hashes(type, image, filenamehash(filename), false /* print */) > 1) {
                collision = 1;
                fprintf(stderr, "Hash of filename ");
                print_filename(stderr, filename);
                fprintf(stderr, " [$%04x] is not unique\n", filenamehash(filename));
                count_hashes(type, image, filenamehash(filename), true /* print */);
            }
        }
    } while (next_dir_entry(type, image, &dt, &ds, &offset, blockmap));
    free(blockmap);
    return collision;
}

/* Compare filenames with wildcards */
static bool
match_filename(const unsigned char *pattern, const unsigned char *filename)
{
    for(int c = 0; c < FILENAMEMAXSIZE; c++) {
        if(pattern[c] == 42) { /* PETSCII * */
            return true;
        }
        if((pattern[c] != 63) && (pattern[c] != filename[c])) { /* PETSCII ? or same character */
            return false;
        }
    }
    return true;
}

/* Searches for an existing DIR entry with the given name, returns false if it does not exist */
static bool
find_existing_file(image_type type, unsigned char* image, unsigned char* filename, int *index, int *track, int *sector, int *offset)
{
    *track = dirtrack(type);
    *sector = (type == IMAGE_D81) ? 3 : 1;
    *offset = 0;
    *index = 0;
    bool exists = false;
    char *blockmap = calloc(image_num_blocks(type), sizeof(char));
    if(blockmap == NULL) {
        fprintf(stderr, "ERROR: Memory allocation error\n");
        exit(-1);
    }

    do {
        int b = linear_sector(type, *track, *sector) * BLOCKSIZE + *offset;
        int filetype = image[b + FILETYPEOFFSET];
        if(filetype != 0 && match_filename(filename, image + b + FILENAMEOFFSET)) {
            exists = true;
            break;
        }
        ++(*index);
    } while (next_dir_entry(type, image, track, sector, offset, blockmap));

    free(blockmap);
    return exists;
}

/* Returns an empty DIR slot, allocates a new DIR sector if required */
static void
new_dir_slot(image_type type, unsigned char* image, int dir_sector_interleave, int shadowdirtrack, int *index, int *dirsector,  int *entry_offset, imagefile files[])
{
    int track = dirtrack(type);
    *dirsector = (type == IMAGE_D81) ? 3 : 1;
    *entry_offset = 0;
    int lindex = 0;

    char *blockmap = calloc(image_num_blocks(type), sizeof(char));
    if(blockmap == NULL) {
        fprintf(stderr, "ERROR: Memory allocation error\n");
        exit(-1);
    }

    do {
        int b = linear_sector(type, track, *dirsector) * BLOCKSIZE + *entry_offset;
        if (image[b + FILETYPEOFFSET] == FILETYPEDEL) {
            /* Check if the apparently free slot is used by a new filetype 0 file */
            bool used = false;
            for(int f = 0; f < num_files; f++) {
                if(files[f].direntryindex == lindex) {
                    used = true;
                    break;
                }
            }
            if(!used) {
                *index = lindex;
                free(blockmap);
                return; /* found an empty slot */
            }
        }
        ++lindex;
    } while (next_dir_entry(type, image, &track, dirsector, entry_offset, blockmap));
    free(blockmap);

    /* allocate new dir block */
    int last_sector = *dirsector;
    int next_sector = -1;
    for (int s = 1; s < num_sectors(type, dirtrack(type)); s++) {
        int sector = (last_sector + s * dir_sector_interleave) % num_sectors(type, dirtrack(type));
        if (is_sector_free(type, image, dirtrack(type), sector, 0, 0)) {
            next_sector = sector;
            break;
        }
    }
    if (next_sector == -1) {
        fprintf(stderr, "ERROR: Dir track full\n");
        exit(-1);
    }
    int b = linear_sector(type, dirtrack(type), last_sector) * BLOCKSIZE;
    image[b + TRACKLINKOFFSET] = dirtrack(type);
    image[b + SECTORLINKOFFSET] = next_sector;

    mark_sector(type, image, dirtrack(type), next_sector, 0 /* not free */);
    b = linear_sector(type, dirtrack(type), next_sector) * BLOCKSIZE;
    memset(image + b, 0, BLOCKSIZE);
    image[b + SECTORLINKOFFSET] = 255;
    *dirsector = next_sector;
    *entry_offset = 0;

    if (shadowdirtrack > 0) {
        b = linear_sector(type, shadowdirtrack, last_sector) * BLOCKSIZE;
        image[b + TRACKLINKOFFSET] = shadowdirtrack;
        image[b + SECTORLINKOFFSET] = next_sector;
        mark_sector(type, image, shadowdirtrack, next_sector, 0 /* not free */);

        b = linear_sector(type, shadowdirtrack, next_sector) * BLOCKSIZE;
        memset(image + b, 0, BLOCKSIZE);
        image[b + SECTORLINKOFFSET] = 255;
    }
    *index = lindex;
}

/* Returns suitable index and offset for given filename (either existing slot when overwriting, first free slot or slot in newly allocated segment) */
static bool
find_dir_slot(image_type type, unsigned char* image, unsigned char* filename, int dir_sector_interleave, int shadowdirtrack, int *index, int *dirsector,  int *entry_offset, imagefile files[])
{
    int track;
    if(find_existing_file(type, image, filename, index, &track, dirsector, entry_offset)) {
        return true;
    }
    new_dir_slot(type, image, dir_sector_interleave, shadowdirtrack, index, dirsector, entry_offset, files);
    return false;
}

/* Adds the specified new entries to the directory */
static void
create_dir_entries(image_type type, unsigned char* image, imagefile* files, int num_files, int dir_sector_interleave, unsigned int shadowdirtrack, int nooverwrite)
{
    /* this does not check for uniqueness of filenames */

    int num_overwritten_files = 0;

    if (verbose && num_files > 0) {
        printf("\nCreating dir entries:\n");
    }

    for (int i = 0; i < num_files; i++) {
        /* find or create slot */
        imagefile *file = files + i;

        if (verbose) {
            printf("  ");
            print_dirfilename(file->pfilename);
        }

        if(file->force_new) {
            new_dir_slot(type, image, dir_sector_interleave, shadowdirtrack, &file->direntryindex, &file->direntrysector, &file->direntryoffset, files);
        } else if (find_dir_slot(type, image, file->pfilename, dir_sector_interleave, shadowdirtrack, &file->direntryindex, &file->direntrysector, &file->direntryoffset, files)) {
            if (nooverwrite) {
                fprintf(stderr, "ERROR: Filename exists on disk image already and -o was set\n");
                exit(-1);
            }

            wipe_file(type, image, file);
            num_overwritten_files++;
        }

        int file_entry_offset = linear_sector(type, dirtrack(type), file->direntrysector) * BLOCKSIZE + file->direntryoffset;
        image[file_entry_offset + FILETYPEOFFSET] = file->filetype & 0xff;
        if (verbose && (file->filetype & FILETYPETRANSWARPMASK)) {
            printf(" [Transwarp]");
        }
        memcpy(image + file_entry_offset + FILENAMEOFFSET, file->pfilename, FILENAMEMAXSIZE);

        if (is_transwarp_bootfile(image, file_entry_offset)) {
            if (file->filetype & FILETYPETRANSWARPMASK) {
                if (verbose) {
                    printf("\n");
                }

                fprintf(stderr, "ERROR: Attempt to write Transwarp bootfile as Transwarp file\n");

                exit(-1);
            }

            file->mode |= MODE_TRANSWARPBOOTFILE;
            if (verbose) {
                printf(" [Transwarp bootfile]");
            }

            if (i != 0) {
                // allocate Transwarp bootfile first
                if ((files[0].mode & MODE_TRANSWARPBOOTFILE) != 0) {
                    if (verbose) {
                        printf("\n");
                    }

                    fprintf(stderr, "ERROR: Multiple Transwarp bootfiles\n");
                    exit(-1);
                }

                imagefile transwarp_bootfile = *file;
                for (int j = i; j > 0; --j) {
                    files[j] = files[j - 1];
                }
                files[0] = transwarp_bootfile;
                file = files;
            }
        }

        if (shadowdirtrack > 0) {
            file_entry_offset = linear_sector(type, shadowdirtrack, file->direntrysector) * BLOCKSIZE + file->direntryoffset;
            image[file_entry_offset + FILETYPEOFFSET] = file->filetype;
            memcpy(image + file_entry_offset + FILENAMEOFFSET, file->pfilename, FILENAMEMAXSIZE);
        }

        if (verbose) {
            printf("\n");
        }
    }

    if (!quiet && (num_overwritten_files > 0)) {
        printf("%d out of %d files exist and will be overwritten\n", num_overwritten_files, num_files);
    }
}

/* Prints the allocated tracks and sectors for every file */
static void
print_file_allocation(image_type type, const unsigned char* image, imagefile* files, int num_files)
{
    if (num_files <= 0) {
        imagefile existing_files[144];
        memset(existing_files, 0, sizeof existing_files);

        int t = dirtrack(type);
        int s = (type == IMAGE_D81) ? 3 : 1;
        int o = 0;
        char *blockmap = calloc(image_num_blocks(type), sizeof(char));
        if(blockmap == NULL) {
            fprintf(stderr, "ERROR: Memory allocation error\n");
            exit(-1);
        }

        do {
            int b = linear_sector(type, t, s) * BLOCKSIZE + o;
            int filetype = image[b + FILETYPEOFFSET] & 0xf;
            switch (filetype) {
            case FILETYPEPRG: {
                int track = image[b + FILETRACKOFFSET];
                int sector = image[b + FILESECTOROFFSET];
                int numsectors = image[b + FILEBLOCKSLOOFFSET] + 256 * image[b + FILEBLOCKSHIOFFSET];
                const unsigned char *filename = image + b + FILENAMEOFFSET;
                b = linear_sector(type, track, sector);
                if (b >= 0) {
                    memcpy(existing_files[num_files].pfilename, filename, FILENAMEMAXSIZE);
                    existing_files[num_files].track = track;
                    existing_files[num_files].sector = sector;
                    existing_files[num_files].direntryindex = num_files;
                    existing_files[num_files].direntrysector = s;
                    existing_files[num_files].direntryoffset = o;
                    existing_files[num_files].nrSectors = numsectors;

                    if (is_transwarp_file(image, b)) {
                        existing_files[num_files].filetype = filetype | FILETYPETRANSWARPMASK;
                        existing_files[num_files].sectorInterleave = 1;

                        int start_track;
                        int end_track;
                        int low_track;
                        int high_track;
                        int filesize = transwarp_stat(type, image, b, &start_track, &end_track, &low_track, &high_track);

                        existing_files[num_files].size = filesize;

                        existing_files[num_files].track = start_track;
                        existing_files[num_files].last_track = end_track;

                        ++num_files;

                        break;
                    }

                    while (true) {
                        b = linear_sector(type, track, sector);
                        if(b < 0) {
                            break; /* TODO: print info about illegal t/s */
                        }
                        int offset = b * BLOCKSIZE;
                        int next_track = image[offset + 0];
                        int next_sector = image[offset + 1];
                        if ((track == 0) || (next_track == 0)) {
                            break;
                        }
                        if ((track == next_track) && (next_sector > sector)) {
                            existing_files[num_files].sectorInterleave = next_sector - sector;
                            break;
                        }
                        track = next_track;
                        sector = next_sector;
                    }

                    ++num_files;

                    break;
                }
            }

            default:
                break;
            }
        } while (next_dir_entry(type, image, &t, &s, &o, blockmap));
        free(blockmap);
        files = existing_files;
    }

    if(num_files > 0) {
        printf("\nFile allocation:\n");
    }

    for (int i = 0; i < num_files; i++) {
        printf("%02d/%02d %3d (0x%02x 0x%02x:0x%02x) ", files[i].track, files[i].sector, files[i].nrSectors & 0xffff,
               files[i].direntryindex, files[i].direntrysector, files[i].direntryoffset);
        if (files[i].alocalname) {
            printf("\"%s\" => ", files[i].alocalname);
        }
        print_filename(stdout, files[i].pfilename);
        printf(" (SL: %d)", files[i].sectorInterleave);

        if ((files[i].mode & MODE_LOOPFILE) && (files[i].sectorInterleave != 0)) {

            continue;
        }

        if (files[i].filetype & FILETYPETRANSWARPMASK) {
            int transwarp_blocks;
            int nonredundant_blocks;
            int redundant_blocks;
            int nonredundant_blocks_on_last_track;
            int spare_bytes = transwarp_size(type, files[i].track, files[i].last_track, files[i].size, &transwarp_blocks, &nonredundant_blocks, &redundant_blocks, &nonredundant_blocks_on_last_track);

            int num_blocks = 0;
            int filesize = files[i].size + 2;
            while (filesize > 0) {
                ++num_blocks;
                filesize -= 254;
            }

            printf("\n          Transwarp: %d total/%d actual (%d standard) blocks, tracks %d-%d, %d used and %d redundant block%s on last track, 0x%x spare bytes in last block, size 0x%x\n",
                   transwarp_blocks, nonredundant_blocks, num_blocks,
                   (files[i].track < files[i].last_track) ? files[i].track : files[i].last_track,
                   (files[i].track >= files[i].last_track) ? files[i].track : files[i].last_track,
                   nonredundant_blocks_on_last_track, redundant_blocks, (redundant_blocks == 1) ? "" : "s",
                   spare_bytes, files[i].size);

            continue;
        }

        int track = files[i].track;
        int sector = files[i].sector;

        bool firsttrack = true;
        int firstsector = sector;
        bool fileblocks[SECTORSPERTRACK_D81];
        memset(fileblocks, 0, sizeof fileblocks);
        fileblocks[sector] = true;

        int j = 0;
        while (track != 0) {
            if (j == 0) {
                printf("\n          ");
            }
            printf("%02d/%02d", track, sector);
            int b = linear_sector(type, track, sector);
            if(b < 0) {
                break; /* TODO: print info about illegal t/s link? */
            }
            int offset = b * BLOCKSIZE;
            int next_track = image[offset + 0];
            int next_sector = image[offset + 1];
            if ((track != next_track) && (next_track != 0)) {
                /* track change */
                if (next_sector != 0) {
                    /* interleave violation */
                    printf("!-");
                } else {
                    printf(" -");
                }
            } else if ((next_sector < sector) && (next_track != 0)) {
                /* sector wrap */
                int expected_next_sector = ((sector + abs(files[i].sectorInterleave)) % num_sectors(type, track));
                bool on_nonempty_firsttrack = (expected_next_sector < next_sector) && firsttrack && (firstsector != 0);
                if ((expected_next_sector != next_sector) && (!on_nonempty_firsttrack)) {
                    while ((expected_next_sector < next_sector) && fileblocks[expected_next_sector]) {
                        ++expected_next_sector;
                    }
                    if (expected_next_sector != next_sector) {
                        /* interleave violation */
                        printf("!.");
                    } else {
                        printf(" .");
                    }
                } else {
                    printf(" .");
                }
            } else if (((next_sector - sector) != abs(files[i].sectorInterleave)) && (next_track != 0)) {
                /* interleave violation */
                printf(" !");
            } else {
                printf("  ");
            }

            if (track != next_track) {
                memset(fileblocks, 0, sizeof fileblocks);
                firsttrack = false;
            }
            track = next_track;
            sector = next_sector;
            if (next_track != 0) {
                fileblocks[sector] = true;
            }

            j++;
            if (j == 10) {
                j = 0;
            }
        }
        printf("\n");
    }
    printf("\n");
}


static void
assign_blocktags(image_type type, const unsigned char *image, int(*blocktags)[SECTORSPERTRACK_D81])
{
    char c = '@';

    char *blockmap = calloc(image_num_blocks(type), sizeof(char));
    if(blockmap == NULL) {
        fprintf(stderr, "ERROR: Memory allocation error\n");
        exit(-1);
    }
    int ds = (type == IMAGE_D81) ? 3 : 1;
    int dt = dirtrack(type);
    int offset = 0;
    do {
        int dirblock = linear_sector(type, dt, ds) * BLOCKSIZE + offset;
        int filetype = image[dirblock + FILETYPEOFFSET] & 0xf;
        if (filetype != 0) {
            int filetrack = image[dirblock + FILETRACKOFFSET];
            int filesector = image[dirblock + FILESECTOROFFSET];

            if (is_transwarp_file(image, dirblock)) {
                int start_track;
                int end_track;
                int low_track;
                int high_track;
                int filesize = transwarp_stat(type, image, dirblock, &start_track, &end_track, &low_track, &high_track);
                if (filesize <= 0) {
                    continue;
                }

                int transwarp_blocks;
                int nonredundant_blocks;
                int redundant_blocks;
                int nonredundant_blocks_on_last_track;
                transwarp_size(type, start_track, end_track, filesize, &transwarp_blocks, &nonredundant_blocks, &redundant_blocks, &nonredundant_blocks_on_last_track);

                for (int track = low_track; track <= high_track; ++track) {
                    for (int sector = 0; sector < SECTORSPERTRACK_D81; ++sector) {
                        blocktags[track][sector] = c + (((track == end_track) && (sector >= nonredundant_blocks_on_last_track)) ? 256 : 0);
                    }
                }
            } else {
                bool new_track = true;
                while (filetrack != 0) {
                    int b = linear_sector(type, filetrack, filesector);
                    if(b < 0) {
                        break;
                    }
                    int block_offset = b * BLOCKSIZE;
                    int next_track = image[block_offset + TRACKLINKOFFSET];
                    int next_sector = image[block_offset + SECTORLINKOFFSET];

                    blocktags[filetrack][filesector] = c + (new_track ? 256 : 0);
                    new_track = (filetrack != next_track);

                    filetrack = next_track;
                    filesector = next_sector;
                }
            }

            switch (c) {
            default:
                ++c;
                break;
            case 'Z':
                c = '0';
                break;
            case '9':
                c = 'a';
                break;
            case 'z':
                c = '@';
                break;
            }
        }

    } while (next_dir_entry(type, image, &dt, &ds, &offset, blockmap));
    free(blockmap);
}

/* Returns true if the file starting on the given filetrack and filesector uses the given track */
static bool
fileontrack(image_type type, const unsigned char *image, int track, int filetrack, int filesector)
{
    while (filetrack != 0) {
        if (filetrack == track) {
            return true;
        }

        int b = linear_sector(type, filetrack, filesector);
        if(b < 0) {
            return false;
        }
        int block_offset = b * BLOCKSIZE;
        int next_track = image[block_offset + TRACKLINKOFFSET];
        int next_sector = image[block_offset + SECTORLINKOFFSET];
        filetrack = next_track;
        filesector = next_sector;
    }

    return false;
}

/* Prints all filenames of files that use the given track */
static void
print_track_usage(image_type type, const unsigned char *image, int(*blocktags)[SECTORSPERTRACK_D81], int track)
{

    char *blockmap = calloc(image_num_blocks(type), sizeof(char));
    if(blockmap == NULL) {
        fprintf(stderr, "ERROR: Memory allocation error\n");
        exit(-1);
    }
    int ds = (type == IMAGE_D81) ? 3 : 1;
    int dt = dirtrack(type);
    int offset = 0;
    do {
        int dirblock = linear_sector(type, dt, ds) * BLOCKSIZE + offset;
        int filetype = image[dirblock + FILETYPEOFFSET] & 0xf;
        if (filetype != 0) {
            int filetrack = image[dirblock + FILETRACKOFFSET];
            int filesector = image[dirblock + FILESECTOROFFSET];
            bool ontrack = (type == IMAGE_D71) ? fileontrack(type, image, track, (filetrack > D64NUMTRACKS) ? filetrack - D64NUMTRACKS : filetrack + D64NUMTRACKS, filesector) : false;

            if (is_transwarp_file(image, dirblock)) {
                int start_track;
                int end_track;
                int low_track;
                int high_track;
                int filesize = transwarp_stat(type, image, dirblock, &start_track, &end_track, &low_track, &high_track);
                if (filesize <= 0) {
                    continue;
                }

                ontrack = (low_track <= track) && (track <= high_track);
                if (ontrack == false) {
                    continue;
                }

                filetrack = start_track;
            }

            if (ontrack || fileontrack(type, image, track, filetrack, filesector)) {
                unsigned char *filename = (unsigned char *) image + dirblock + FILENAMEOFFSET;
                if (track == filetrack) {
                    reverse_print_on();
                }
                printf("%c", blocktags[filetrack][filesector]);
                if (track == filetrack) {
                    reverse_print_off();
                }
                printf(": ");
                print_filename(stdout, filename);
                printf(" ");
            }
        }
    } while (next_dir_entry(type, image, &dt, &ds, &offset, blockmap));
    free(blockmap);
}

/* Prints the BAM allocation map and returns the number of free blocks */
static int
check_bam(image_type type, const unsigned char* image)
{
    int sectorsFree = 0;
    int sectorsFreeOnDirTrack = 0;
    int sectorsOccupied = 0;
    int sectorsOccupiedOnDirTrack = 0;

    int blocktags[D81NUMTRACKS][SECTORSPERTRACK_D81];

    if (verbose) {
        memset(blocktags, 0, sizeof blocktags);
        assign_blocktags(type, image, blocktags);

        printf("Block allocation:\n");
    }

    int max_track = (type == IMAGE_D81) ? D81NUMTRACKS
                    : (((type == IMAGE_D64_EXTENDED_SPEED_DOS) || (type == IMAGE_D64_EXTENDED_DOLPHIN_DOS)) ? D64NUMTRACKS_EXTENDED
                       : D64NUMTRACKS);
    for (int t = 1; t <= max_track; t++) {

        if (verbose) {
            printf("  %2d: ", t);
        }
        for (int s = 0; s < num_sectors(type, t); s++) {
            if (is_sector_free(type, image, t, s, 0, 0)) {
                if (verbose) {
                    printf(".");
                }
                if (t != dirtrack(type)) {
                    sectorsFree++;
                } else {
                    sectorsFreeOnDirTrack++;
                }
            } else {
                if (verbose) {
                    int blocktag = blocktags[t][s];
                    if (blocktag == 0) {
                        blocktag = '#';
                    }
                    if (blocktag >= 256) {
                        reverse_print_on();
                    }
                    printf("%c", blocktag);
                    if (blocktag >= 256) {
                        reverse_print_off();
                    }
                }
                if (t != dirtrack(type)) {
                    sectorsOccupied++;
                } else {
                    sectorsOccupiedOnDirTrack++;
                }
            }
        }

        if (type == IMAGE_D71) {
            for (int i = num_sectors(type, t); i < 23; i++) {
                if (verbose) {
                    printf(" ");
                }
            }
            int t2 = t + D64NUMTRACKS;

            if (verbose) {
                printf("%2d: ", t2);
            }
            for (int s = 0; s < num_sectors(type, t2); s++) {
                if (is_sector_free(type, image, t2, s, 0, 0)) {
                    if (verbose) {
                        printf(".");
                    }
                    if (t2 != dirtrack(type)) {
                        sectorsFree++;
                    } else {
                        /* track 53 is usually empty except the extra BAM block */
                        sectorsFreeOnDirTrack++;
                    }
                } else {
                    if (verbose) {
                        printf("#");
                    }
                    sectorsOccupied++;
                }
            }
        }

        for (int i = ((type == IMAGE_D81) ? 42 : 23) - num_sectors(type, t); i > 0; --i) {
            if (verbose) {
                printf(" ");
            }
        }
        if (verbose) {
            print_track_usage(type, image, blocktags, t);
            printf("\n");
        }
    }
    if (verbose) {
        printf("%3d/%3d blocks free (%d/%d including dir track)\n", sectorsFree, sectorsFree + sectorsOccupied,
               sectorsFree + sectorsFreeOnDirTrack, sectorsFree + sectorsFreeOnDirTrack + sectorsOccupied + sectorsOccupiedOnDirTrack);
    }

    return sectorsFree;
}

/* Prints the filetype like the C64 when listing the directory */
static void
print_filetype(int filetype)
{
    if ((filetype & 0x80) == 0) {
        printf("*");
    } else {
        printf(" ");
    }
    if(unicode == 1) {
        printf("%s", filetypename_uc[filetype & 0xf]);
    } else {
        printf("%s", filetypename_lc[filetype & 0xf]);
    }
    if ((filetype & 0x40) != 0) {
        printf("<");
    } else {
        printf(" ");
    }
}

/* Prints the directory like the C64 when listing the directory */
static void
print_directory(image_type type, unsigned char* image, int blocks_free)
{
    unsigned char* bam = image + linear_sector(type, dirtrack(type), 0) * BLOCKSIZE;
    char *blockmap = calloc(image_num_blocks(type), sizeof(char));
    if(blockmap == NULL) {
        fprintf(stderr, "ERROR: Memory allocation error\n");
        exit(-1);
    }

    printf("\n0 ");
    reverse_print_on();
    printf("\"");
    print_petscii(bam + get_header_offset(type), 16);
    printf("\" ");
    print_petscii(bam + get_id_offset(type), 5);
    reverse_print_off();

    if (verbose) {
        printf("   fn hash  ASCII");
    }
    printf("\n");

    int ds = (type == IMAGE_D81) ? 3 : 1;
    int dt = dirtrack(type);
    int offset = 0;
    do {
        int dirblock = linear_sector(type, dt, ds) * BLOCKSIZE + offset;
        int filetype = image[dirblock + FILETYPEOFFSET];
        int blocks = image[dirblock + FILEBLOCKSLOOFFSET] + 256 * image[dirblock + FILEBLOCKSHIOFFSET];

        if (filetype != FILETYPEDEL) {
            unsigned char* filename = (unsigned char*)image + dirblock + FILENAMEOFFSET;
            printf("%-3d  ", blocks);
            print_dirfilename(filename);
            print_filetype(filetype);
            if (verbose) {
                printf(" [$%04x]  \"", filenamehash(filename));
                print_filename_with_escapes(filename, FILENAMEMAXSIZE);
                printf("\"");
            }
            printf("\n");
        }
    } while (next_dir_entry(type, image, &dt, &ds, &offset, blockmap));
    free(blockmap);
    if(unicode == 1) {
        printf("%d BLOCKS FREE.\n", blocks_free);
    } else {
        printf("%d blocks free.\n", blocks_free);
    }

    /* detect and print bam message */
    if((type == IMAGE_D64 || type == IMAGE_D64_EXTENDED_SPEED_DOS) && bam[BAMMESSAGEOFFSET] != 0) {
        printf("\nBAM message: \"");
        /* only checking the first byte and hoping for the best... */
        int b = BAMMESSAGEOFFSET;
        unsigned char c;
        while(b < 256 && ((c = bam[b]) != 0)) {
            putp(c, stdout);
            b++;
        }
        printf("\"\n");
    }
}

/* Performs GCR encoding on 32 bit value */

static const unsigned char NIBBLE_TO_GCR[] = {
    0x0a, 0x0b, 0x12, 0x13,
    0x0e, 0x0f, 0x16, 0x17,
    0x09, 0x19, 0x1a, 0x1b,
    0x0d, 0x1d, 0x1e, 0x15
};

static void
encode_4_bytes_gcr(char* in, char* out)
{
    out[0] = (NIBBLE_TO_GCR[(in[0] >> 4) & 0xf] << 3) | (NIBBLE_TO_GCR[ in[0]       & 0xf] >> 2); /* 11111222 */
    out[1] = (NIBBLE_TO_GCR[ in[0]       & 0xf] << 6) | (NIBBLE_TO_GCR[(in[1] >> 4) & 0xf] << 1) | (NIBBLE_TO_GCR[ in[1]       & 0xf] >> 4); /* 22333334 */
    out[2] = (NIBBLE_TO_GCR[ in[1]       & 0xf] << 4) | (NIBBLE_TO_GCR[(in[2] >> 4) & 0xf] >> 1); /* 44445555 */
    out[3] = (NIBBLE_TO_GCR[(in[2] >> 4) & 0xf] << 7) | (NIBBLE_TO_GCR[ in[2]       & 0xf] << 2) | (NIBBLE_TO_GCR[(in[3] >> 4) & 0xf] >> 3); /* 56666677 */
    out[4] = (NIBBLE_TO_GCR[(in[3] >> 4) & 0xf] << 5) |  NIBBLE_TO_GCR[ in[3]       & 0xf]; /* 77788888 */
}

/* Transwarp encoding utility functions */

static void
generate_gcr_decoding_table(const unsigned char nibble_to_gcr[], int8_t gcr_to_nibble[])
{
    for (int i = 0; i < 32; ++i) {
        gcr_to_nibble[i] = -(i + 1);
    }

    for (int i = 0; i < 16; ++i) {
        gcr_to_nibble[nibble_to_gcr[i]] = i;
    }
}

static unsigned char
even_bits(unsigned char value)
{
    return (((value >> 6) & 1) << 3)
           | (((value >> 4) & 1) << 2)
           | (((value >> 2) & 1) << 1)
           | (((value >> 0) & 1) << 0);
}

static unsigned char
odd_bits(unsigned char value)
{
    return (((value >> 7) & 1) << 3)
           | (((value >> 5) & 1) << 2)
           | (((value >> 3) & 1) << 1)
           | (((value >> 1) & 1) << 0);
}

typedef struct transwarp_encode_context {
    unsigned int  version;
    unsigned char previous;
    unsigned char previous1;
    unsigned char previous2;
    unsigned char accu;
    unsigned char carry;
    unsigned char recvcarry;
    unsigned char carry2;
    unsigned char sendaccu;
    unsigned char sendcarry;
} transwarp_encode_context;

static const int ENCODE[5][64] = {
    {
        0xf6, 0xee, 0xf5, 0xed, 0x9a, 0xde, 0x96, 0xda, 0xf3, 0xea, 0xf2, 0x9e, 0x93, 0xd6, 0x92, 0xd3,
        0xd2, 0xca, 0xce, 0xbe, 0xb3, 0x7e, 0xb2, 0x7d, 0xcd, 0xba, 0xcb, 0xb6, 0xae, 0x7b, 0xaa, 0x7a,
        0x76, 0x6e, 0x75, 0x6d, 0x5e, 0x5b, 0x5d, 0x5a, 0x73, 0x6b, 0x72, 0x6a, 0xdb, 0x9d, 0xeb, 0xd5,
        0x56, 0x4e, 0x55, 0x4d, 0xbd, 0xb5, 0xbb, 0xad, 0x53, 0x4b, 0x52, 0xdd, 0xab, 0x4a, 0x9b, 0x95
    }, {
        0xf6, 0xee, 0xf5, 0xed, 0x9a, 0xde, 0x96, 0xda, 0xf3, 0xea, 0xf2, 0x9e, 0x93, 0xd6, 0x92, 0xef,
        0xe7, 0x7c, 0x9f, 0x74, 0xe6, 0x6c, 0xdf, 0xa6, 0x9c, 0xba, 0x94, 0xb6, 0xae, 0x7b, 0xaa, 0x7a,
        0x76, 0x6e, 0x75, 0x6d, 0x5e, 0x5b, 0x5d, 0x5a, 0x73, 0x6b, 0x72, 0x6a, 0xdb, 0xd7, 0xeb, 0xe5,
        0x56, 0x64, 0x55, 0x5c, 0xbd, 0xb5, 0xbb, 0xad, 0x65, 0x54, 0x5f, 0xdd, 0xab, 0xa7, 0x9b, 0xa5
    }, {
        0xa5, 0xa7, 0xa9, 0xab, 0xd5, 0xd7, 0xd9, 0xdb, 0x95, 0x99, 0x9b, 0x97, 0xe5, 0x9d, 0xeb, 0xe9
    }, {
        0xf6, 0xee, 0xf5, 0xed, 0x69, 0xde, 0x59, 0xda, 0xb9, 0xea, 0xb7, 0x6f, 0x57, 0xd6, 0x4f, 0xef,
        0xe7, 0xca, 0xce, 0xbe, 0xe6, 0xbf, 0xdf, 0xa6, 0xcd, 0xba, 0xcb, 0xb6, 0xae, 0x7b, 0xaa, 0x7a,
        0x76, 0x6e, 0x75, 0x6d, 0x5e, 0x5b, 0x5d, 0x5a, 0x67, 0x6b, 0x66, 0x6a, 0xe9, 0xd7, 0xeb, 0xe5,
        0x56, 0x4e, 0x55, 0x4d, 0xbd, 0xb5, 0xbb, 0xad, 0x65, 0x4b, 0x5f, 0xdd, 0xab, 0xa7, 0xa9, 0xa5
    }, {
        0xcf, 0xaf, 0xc9, 0x79, 0x69, 0xde, 0x59, 0xda, 0xb9, 0x77, 0xb7, 0x6f, 0x57, 0xd6, 0x4f, 0xd3,
        0xd2, 0xca, 0xce, 0xbe, 0xb3, 0x7e, 0xb2, 0x7d, 0xcd, 0xba, 0xcb, 0xb6, 0xae, 0x7b, 0xaa, 0x7a,
        0x76, 0x6e, 0x75, 0x6d, 0x5e, 0x5b, 0x5d, 0x5a, 0x73, 0x6b, 0x72, 0x6a, 0xdb, 0xd7, 0xd9, 0xd5,
        0x56, 0x4e, 0x55, 0x4d, 0xbd, 0xb5, 0xbb, 0xad, 0x53, 0x4b, 0x52, 0xdd, 0xab, 0x4a, 0xa9, 0x49
    }
};

static const int DECODE[256] = {
    -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,
        -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,
        -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,
        -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,
        -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1, 0x00, 0x02, 0x12,   -1, 0x14, 0x16, 0x68,
        -1,   -1, 0x18, 0x1a, 0x12, 0x1c, 0x1e, 0x6a,   -1, 0x6c, 0x24, 0x26, 0x14, 0x2c, 0x2e, 0x18,
        -1,   -1,   -1,   -1, 0x16, 0x1a, 0x38, 0x3a,   -1, 0x6e, 0x30, 0x32, 0x46, 0x34, 0x36, 0x70,
        -1,   -1, 0x38, 0x3a, 0x54, 0x3c, 0x3e, 0x72,   -1, 0x74, 0x40, 0x42, 0x56, 0x44, 0x46,   -1,
        -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,
        -1,   -1, 0x68, 0x6a, 0x58, 0x80, 0x6c, 0x8a,   -1, 0x82, 0x6e, 0x88, 0x5a, 0xa2, 0x70, 0x5c,
        -1,   -1,   -1,   -1,   -1, 0x00, 0x44, 0x02,   -1, 0x08, 0x48, 0x0a,   -1, 0x04, 0x4a, 0x76,
        -1,   -1, 0x4c, 0x4e,   -1, 0x06, 0x50, 0x78,   -1, 0x7a, 0x52, 0x0c,   -1, 0x0e, 0x54, 0x46,
        -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1, 0x7c, 0x56, 0x58,   -1, 0x5a, 0x5c, 0x7e,
        -1,   -1, 0x5e, 0x60,   -1, 0x20, 0x62, 0x22,   -1, 0x28, 0x64, 0x2a,   -1, 0x10, 0x66, 0x4c,
        -1,   -1,   -1,   -1,   -1, 0xa0, 0x4e, 0x5e,   -1, 0xaa, 0x72, 0xa8,   -1, 0x74, 0x76, 0x60,
        -1,   -1, 0x78, 0x7a,   -1, 0x7c, 0x7e,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1
    };

static unsigned char
encode_read_diff(const int encode[64], unsigned char *accu, unsigned char *carry, unsigned char value)
{
    unsigned char value_to_encode;

    unsigned char target = DECODE[encode[value]] & 0x7e;
    for (value_to_encode = 0; value_to_encode < 64; ++value_to_encode) {
        unsigned char val = DECODE[encode[value_to_encode]] + *accu + *carry;
        if ((val & 0x7e) == target) {
            break;
        }
    }
    if (value_to_encode >= 64) {
        printf("Encoding error, 0x%x = 0x%x + %d + ?\n", target, *accu, *carry);
        for (value_to_encode = 0; value_to_encode < 64; ++value_to_encode) {
            unsigned char val = DECODE[encode[value_to_encode]] + *accu + *carry;
            printf("%d: 0x%x <- 0x%x\n", value_to_encode, val & 0x7e, DECODE[encode[value_to_encode]]);
        }

        fprintf(stderr, "ERROR: Transwarp encoding error\n");
        exit(-2);
    }

    int sum = DECODE[encode[value_to_encode]] + *accu + *carry;
    *accu = sum;
    *carry = sum >= 256;

    unsigned char check = DECODE[encode[value]];
    if ((*accu & 0x7e) != (check & 0x7e)) {
        fprintf(stderr, "ERROR: Transwarp encoding error, 0x%x: actual 0x%x != 0x%x expected\n", DECODE[encode[check & 0x3f]], *accu, check);
        exit(-3);
    }

    unsigned char temp = (*carry << 7) | (*accu >> 1);
    *carry = *accu & 1;
    *accu = (*carry << 7) | ((temp & 0xfb) >> 1);
    *carry = (*accu >> 6) & 1;

    return encode[value_to_encode];
}

unsigned char
encode_send_diff(unsigned char value, unsigned char *accu, unsigned char *carry)
{
    value = (value & ~((1 << 3) | (1 << 0)))
            | (((value >> 3) & 1) << 0)
            | (((value >> 0) & 1) << 3);
    value ^= 0xff;

    int diff = value - *accu - *carry;
    *carry = (diff < 0);

    *accu = value;

    *accu = (((*accu >> 7) & 1) << 7)
            | (((*accu >> 1) & 1) << 6)
            | (((*accu >> 7) & 1) << 5)
            | (*carry << 4)
            | (((*accu >> 6) & 1) << 2)
            | (((*accu >> 5) & 1) << 1);
    *carry = (*accu >> 6) & 1;

    return diff;
}

static unsigned char
encode_receive_diff(const transwarp_encode_context *ctx, unsigned char in, unsigned char *previous, unsigned char *carry)
{
    int offset = (ctx->version <= 84) ? 0 : 2;

    int out = in - *carry - offset;
    int diff = out - ((*previous & 0xc0) | (out & 0x3f));
    *carry = (diff < 0);
    out = ((out ^ *previous) & 0x3f) | diff;
    *previous = in;

    return out;
}

static unsigned char
encode_buffer_byte(const int encode[][64], unsigned char previous, unsigned char *carry, unsigned char in, unsigned char *out)
{
    unsigned char even = (previous >> 1) ^ in;
    unsigned char odd = ((*carry << 7) | (previous >> 1)) ^ in;

    *carry = previous & 1;

    out[(31 * 5) + 4] = encode[2][even_bits(even)];
    out[4] = encode[2][odd_bits(odd)];

    return in;
}

static void
encode_base_bytes(const unsigned char scramble[][256],
                  transwarp_encode_context *ctx, const unsigned char in[3], unsigned char *out)
{
    unsigned char in0 = encode_receive_diff(ctx, in[0], &(ctx->previous), &(ctx->recvcarry));
    unsigned char in1 = encode_receive_diff(ctx, in[1], &(ctx->previous), &(ctx->recvcarry));
    unsigned char in2 = encode_receive_diff(ctx, in[2], &(ctx->previous), &(ctx->recvcarry));

    in0 = scramble[0][in0];
    in1 = scramble[1][in1];
    in2 = scramble[2][in2];

    unsigned char val3 = in0 & 0x3f;
    out[0] = encode_read_diff(ENCODE[3], &(ctx->accu), &(ctx->carry), val3);

    unsigned char val4 = ((in0 >> 6)
                          | (in1 << 2)) & 0x3f;
    out[1] = encode_read_diff(ENCODE[4], &(ctx->accu), &(ctx->carry), val4);

    unsigned char val0 = ((in1 >> 4)
                          | (in2 << 4)) & 0x3f;
    out[2] = encode_read_diff(ENCODE[0], &(ctx->accu), &(ctx->carry), val0);

    unsigned char val1 = in2 >> 2;
    out[3] = encode_read_diff(ENCODE[1], &(ctx->accu), &(ctx->carry), val1);
}

static unsigned char
crc8(unsigned char value)
{
    for (int i = 0; i < 8; ++i) {
        value = (value & 0x80) ? ((value << 1) ^ 0x31) : (value << 1);
    }

    return value;
}

static bool
decode_5_bytes_gcr(const int8_t decoding_map[], const unsigned char *in, unsigned char *out)
{
    unsigned char gcr_hi;
    unsigned char gcr_lo;

    gcr_hi = in[0] >> 3;
    gcr_lo = ((in[0] & 0x7) << 2) | (in[1] >> 6);
    int out0 = (decoding_map[gcr_hi] << 4) | decoding_map[gcr_lo];

    gcr_hi = (in[1] & 0x3e) >> 1;
    gcr_lo = ((in[1] & 0x1) << 4) | (in[2] >> 4);
    int out1 = (decoding_map[gcr_hi] << 4) | decoding_map[gcr_lo];

    gcr_hi = ((in[2] & 0xf) << 1) | (in[3] >> 7);
    gcr_lo = (in[3] & 0x7c) >> 2;
    int out2 = (decoding_map[gcr_hi] << 4) | decoding_map[gcr_lo];

    gcr_hi = ((in[3] & 0x3) << 3) | (in[4] >> 5);
    gcr_lo = in[4] & 0x1f;
    int out3 = (decoding_map[gcr_hi] << 4) | decoding_map[gcr_lo];

    out[0] = out0;
    out[1] = out1;
    out[2] = out2;
    out[3] = out3;

    return (out0 | out1 | out2 | out3) >= 0;
}

static int
decode_gcr_block(const int8_t decoding_map[], const unsigned char *encoded, unsigned char decoded[])
{
    bool ok = decode_5_bytes_gcr(decoding_map, encoded, decoded);
    decoded[0] = decoded[1];
    decoded[1] = decoded[2];
    decoded[2] = decoded[3];

    int computed_checksum = decoded[0] ^ decoded[1] ^ decoded[2];

    int i = 5;
    for (int j = 3; i < 320; i += 5, j += 4) {
        ok &= decode_5_bytes_gcr(decoding_map, encoded + i, decoded + j);
        computed_checksum ^= decoded[j] ^ decoded[j + 1] ^ decoded[j + 2] ^ decoded[j + 3];
    }

    unsigned char last_byte[4];
    ok &= decode_5_bytes_gcr(decoding_map, encoded + i, last_byte);
    decoded[255] = last_byte[0];

    computed_checksum ^= last_byte[0];
    computed_checksum = ok ? computed_checksum : -1;

    return ok ? computed_checksum : -1;
}

static int
encode_transwarp_block(const unsigned char scramble[][256], const int8_t gcr_to_nibble[32],
                       transwarp_encode_context* ctx, const unsigned char *indata, int filepos, unsigned char encoded[325])
{
    const unsigned char *unencoded = indata + filepos;

    unsigned char data[TRANSWARPBLOCKSIZE];
    if (filepos < 2) {
        for (int i = filepos, j = 0; j < TRANSWARPBLOCKSIZE; ++i, ++j) {
            data[j] = (i < 0) ? ' ' : ((i < 2) ? 0 : indata[i]);
        }

        unencoded = data;
    }

    unsigned char semiencoded[TRANSWARPBUFFERBLOCKSIZE];

    for (int i = TRANSWARPBUFFERBLOCKSIZE - 1; i >= 0; --i) {
        unsigned char value = encode_receive_diff(ctx, unencoded[TRANSWARPBASEBLOCKSIZE + i], &(ctx->previous2), &(ctx->carry2));

        value = scramble[3][value];

        semiencoded[i] = encode_send_diff(value, &(ctx->sendaccu), &(ctx->sendcarry));
    }

    unsigned char buffer_previous = ctx->previous1;
    unsigned char buffer_carry = 0;
    for (int i = 0; i < TRANSWARPBUFFERBLOCKSIZE; ++i) {
        int shuffle = (TRANSWARPBUFFERBLOCKSIZE - 1) - (i / 2) - ((i & 1) ? ((TRANSWARPBUFFERBLOCKSIZE / 2) + 1) : 0);
        unsigned char value = semiencoded[shuffle];
        buffer_previous = encode_buffer_byte(ENCODE, buffer_previous, &buffer_carry, value, encoded + 3 + (5 * (TRANSWARPBUFFERBLOCKSIZE - 1 - shuffle)));
    }

    unsigned char previous = unencoded[TRANSWARPBASEBLOCKSIZE - 1];

    const int CRCSTEP = 8;

    for (int i = 0; i < TRANSWARPBASEBLOCKSIZE; i += CRCSTEP) {
        previous = crc8(previous);
        previous ^= unencoded[i];
    }
    previous = crc8(previous);

    if (filepos > ((21 * TRANSWARPBLOCKSIZE) + 2)) {
        const int BACKCHECKOFFS = (21 * TRANSWARPBLOCKSIZE) + TRANSWARPBUFFERBLOCKSIZE;
        previous ^= indata[filepos - BACKCHECKOFFS];
        previous = crc8(previous);
        previous ^= indata[filepos - BACKCHECKOFFS + TRANSWARPBUFFERBLOCKSIZE - 1];
        previous = crc8(previous);
    }

    char head_data[4] = { 7, 0, 0, 0 };
    encode_4_bytes_gcr(head_data, (char *) encoded);
    char tail_data[4] = { 0, 0, 0, 0 };
    encode_4_bytes_gcr(tail_data, (char *) encoded + 320);

    unsigned char accu = ctx->previous;
    unsigned char carry = 0;
    for (int i = 0; i < TRANSWARPBASEBLOCKSIZE; ++i) {
        encode_receive_diff(ctx, unencoded[i], &accu, &carry);
    }

    unsigned char receive_checksum = (-previous - carry);

    unsigned char checksum = receive_checksum ^ (buffer_previous >> 1);

    unsigned char odd = odd_bits(checksum);

    odd ^= (buffer_carry << 3);

    static const unsigned char ENCODE_TOP[] = {
        0x05,
        0x07,
        0x0d,
        0x0b
    };
    encoded[2] = (encoded[2] & 0xf0) | ENCODE_TOP[odd & 0x3];

    encoded[317] = ENCODE[2][even_bits(checksum)];

    encoded[322] = ENCODE[2][odd & 0xc];

    unsigned char top_2_bits = encoded[2];
    unsigned char middle_4_bits = encoded[317];
    unsigned char bottom_2_bits = encoded[322];

    ctx->recvcarry = 0;

    ctx->accu = 0;
    ctx->carry = 0;
    for (int i = 0, j = 0; i < TRANSWARPBASEBLOCKSIZE; i += 3, j += 5) {
        encode_base_bytes(scramble, ctx, unencoded + i, encoded + 3 + j);

        ctx->accu = 8;
        ctx->carry = 0;

        unsigned char target = encoded[3 + j + 4];
        unsigned char target_accu = DECODE[target];
        unsigned char target_check = ENCODE[2][odd_bits(target_accu)];

        if (target != target_check) {
            fprintf(stderr, "ERROR: Transwarp encoding error, [%d] 0x%x != 0x%x <- 0x%x\n", i, target, target_check, DECODE[target]);

            exit(-4);
        }

        unsigned char store = ctx->accu ^ target_accu;
        encoded[3 + j + 4] = ENCODE[2][odd_bits(store)];

        unsigned char stored = DECODE[encoded[3 + j + 4]];
        ctx->accu ^= stored;

        if (ctx->accu != target_accu) {
            fprintf(stderr, "ERROR: Transwarp encoding error, [%d] actual 0x%x != 0x%x expected <- 0x%x\n", i, ctx->accu, target_accu, stored);

            exit(-5);
        }
    }
    if (carry != ctx->recvcarry) {
        fprintf(stderr, "ERROR: Transwarp encoding error, carry %d != %d recvcarry\n", carry, ctx->recvcarry);

        exit(-6);
    }

    unsigned char top_fix = encoded[2] ^ (DECODE[encoded[322]] & 0xf);
    top_fix = ((top_fix >> 2) & 2) | ((top_fix >> 1) & 1);
    encoded[2] = (encoded[2] & 0xf0) | ENCODE_TOP[top_fix];
    encoded[322] = (encoded[322] & 0xf0) | 0x5;

    unsigned char block_checksum;
    block_checksum = DECODE[middle_4_bits];
    block_checksum = (block_checksum >> 1) | (buffer_carry << 7);
    block_checksum ^= DECODE[bottom_2_bits];
    block_checksum ^= (top_2_bits & 0xa);

    if (block_checksum != checksum) {
        fprintf(stderr, "ERROR: Transwarp encoding error, actual 0x%x != 0x%x expected, 0x%x -> [0x%x] -> 0x%x -> 0x%x -> [0x%x]\n", block_checksum, checksum, checksum & 0xaa, ((checksum & 0xaa) >> 1) | (checksum & 0xaa), odd, ENCODE[2][odd], DECODE[encoded[317]]);

        exit(-1);
    }

    unsigned char gcr_decoded[4];
    bool ok = decode_5_bytes_gcr(gcr_to_nibble, encoded + 320, gcr_decoded);
    int gcr_checksum = ok ? gcr_decoded[1] : -1;

    unsigned char decoded[256];
    int computed_checksum = decode_gcr_block(gcr_to_nibble, encoded, decoded);

    ok &= decode_5_bytes_gcr(gcr_to_nibble, encoded, gcr_decoded);
    gcr_decoded[1] ^= gcr_checksum ^ computed_checksum;

    encode_4_bytes_gcr((char *) gcr_decoded, (char *) encoded);

    return ok == 0;
}

static void
permute(unsigned char *key, int len, int *set)
{
    for (int i = 0; i < (len - 1); ++i) {
        int divisor = len - i;

        int remainder = 0;
        for (int i = TRANSWARPKEYSIZE - 12; i >= 0; --i) {
            int dividend = (remainder << 8) | key[i];
            remainder = dividend % divisor;
            key[i] = dividend / divisor;
        }

        int n = set[remainder];
        set[remainder] = set[divisor - 1];
        set[divisor - 1] = n;
    }
}

/* Write file to disk using Transwarp encoding */
static unsigned long long
write_transwarp_file(image_type type, unsigned char *image, imagefile *file, unsigned char *filedata, int *filesize, unsigned int version, bool transwarp_bootfile_fits_on_dir_track)
{
    file->size = *filesize - 2;

    unsigned int track = DIRTRACK_D41_D71 - 1;

    if ((file->mode & MODE_MIN_TRACK_MASK) > 0) {
        /* for Transwarp files, a set minimum track is the file's starting track */
        track = (file->mode & MODE_MIN_TRACK_MASK) >> MODE_MIN_TRACK_SHIFT;
    } else {
        /* allocate */
        bool free_tracks[40];
        for (unsigned int t = 1; t <= image_num_tracks(type); ++t) {
            bool track_free = true;
            for (int sector = 0; sector < num_sectors(type, t); ++sector) {
                if (is_sector_free(type, image, t, sector, 0 /* numdirblocks */, 0 /* dir_sector_interleave */) == false) {
                    track_free = false;
                    break;
                }
            }
            free_tracks[t - 1] = track_free;
        }

        /* below dir track */
        while (track > 0) {
            if (free_tracks[track - 1]) {
                int filesize = file->size;
                int t = track;
                while ((filesize > 0)
                        && (t > 0)) {
                    if (free_tracks[t - 1] == false) {
                        break;
                    }
                    filesize -= (TRANSWARPBLOCKSIZE * num_sectors(type, t));
                    if (filesize >= 0) {
                        --t;
                    }
                }

                if (filesize <= 0) {
                    break;
                }
            }
            --track;
        }

        if (track <= 0) {
            /* above dir track */
            track = DIRTRACK_D41_D71 + (transwarp_bootfile_fits_on_dir_track ? 1 : 2);
            while (track <= image_num_tracks(type)) {
                if (free_tracks[track - 1]) {
                    int filesize = file->size;
                    int t = track;
                    while (filesize > 0) {
                        if (free_tracks[t - 1] == false) {
                            break;
                        }
                        filesize -= (TRANSWARPBLOCKSIZE * num_sectors(type, t));
                        if (filesize >= 0) {
                            ++t;
                        }
                    }

                    if (filesize <= 0) {
                        break;
                    }
                }
                ++track;
            }
        }
    }

    file->track = track;
    file->sector = 0;

    int8_t gcr_to_nibble[32];
    generate_gcr_decoding_table(NIBBLE_TO_GCR, gcr_to_nibble);

    unsigned char key[TRANSWARPKEYSIZE];
    memset(key, 0, sizeof key);

    if (file->have_key != 0) {
        if ((filedata[0] == 0x01)
                && (filedata[1] == 0x08)) {
            srand((unsigned int) time(NULL));

            unsigned int linelink = ((filedata[3] << 8) | filedata[2]) - 0x0801 + 2;
            if ((linelink > 0)
                    && ((linelink - 2) < (unsigned int) file->size)
                    && ((filedata[linelink - 1] | filedata[linelink] | filedata[linelink + 1]) == 0)) {
                while (filedata[linelink] == 0) {
                    filedata[linelink] = rand();
                }
            }

            filedata[2] = rand();
            while ((filedata[3] == 0)
                    || (filedata[3] == 8)) {
                filedata[3] = rand();
            }

            int file_size = file->size;
            int filetrack = track;
            while (file_size > 0) {
                file_size -= (TRANSWARPBLOCKSIZE * num_sectors(type, filetrack));
                if (file_size > 0) {
                    filetrack = (filetrack < DIRTRACK_D41_D71) ? (filetrack - 1) : (filetrack + 1);
                }
            }
            int spare_blocks = (0 - file_size) / TRANSWARPBLOCKSIZE;
            int spare_bytes = TRANSWARPBLOCKSIZE - (file->size % TRANSWARPBLOCKSIZE);
            spare_bytes = (spare_blocks * TRANSWARPBLOCKSIZE) + ((spare_bytes != TRANSWARPBLOCKSIZE) ? spare_bytes : 0);
            while (spare_bytes > (0x0801 - 0x0400)) {
                spare_bytes -= TRANSWARPBLOCKSIZE;
            }
            if (spare_bytes > 0) {
                int loadaddress = (filedata[1] << 8) | filedata[0];
                loadaddress -= spare_bytes;
                filedata[0] = loadaddress;
                filedata[1] = loadaddress >> 8;
                memmove(filedata + 2 + spare_bytes, filedata + 2, file->size);
                filedata[spare_bytes + 1] = 0;
                for (int i = 2; i <= spare_bytes; ++i) {
                    filedata[i] = rand();
                }
                file->size += spare_bytes;
                *filesize += spare_bytes;
            }
        }

        memcpy(key, file->key, sizeof key);

        for (int round = TRANSWARPKEYHASHROUNDS; round > 0; --round) {
            for (int i = 0; i < (TRANSWARPKEYSIZE - 1); ++i) {
                key[i] ^= key[i + 1];
            }

            for (int i = TRANSWARPKEYSIZE - 1; i >= 0; --i) {
                int product = key[i] * 0x6b;
                key[i] = product;
                int msb = product >> 8;
                for (int j = i + 1; j < TRANSWARPKEYSIZE; ++j) {
                    msb += key[j];
                    key[j] = msb;
                    msb >>= 8;
                }
            }

            int sum = round;
            for (int i = 0; i < TRANSWARPKEYSIZE; ++i) {
                sum += key[i];
                key[i] = sum;
                sum >>= 8;
            }
        }
    }

    unsigned long long dirdatakey = (key[TRANSWARPKEYSIZE - 1] * (1ULL << 56))
                                    + (key[TRANSWARPKEYSIZE - 2] * (1ULL << 48))
                                    + (key[TRANSWARPKEYSIZE - 3] * (1ULL << 40))
                                    + (key[TRANSWARPKEYSIZE - 4] * (1ULL << 32))
                                    + (key[TRANSWARPKEYSIZE - 5] * (1ULL << 24))
                                    + (key[TRANSWARPKEYSIZE - 6] * (1ULL << 16))
                                    + (key[TRANSWARPKEYSIZE - 7] * (1ULL << 8))
                                    + key[TRANSWARPKEYSIZE - 8];

    int initial_buffer_store_value = key[TRANSWARPKEYSIZE - 9];
    int initial_buffer_recvaccu_value = key[TRANSWARPKEYSIZE - 10];
    int initial_block_recvaccu_value = key[TRANSWARPKEYSIZE - 11];

    unsigned char scramble[4][256];
    for (int i = 0; i < 4; ++i) {
        int set[] = { 0, 2, 4, 1, 3, 5 };
        if (file->have_key) {
            permute(key, 6, set);
        }

        int set2[3][4];
        for (int j = 0; j < 3; ++j) {
            int set3[] = { 0, 1, 2, 3 };
            if (file->have_key) {
                permute(key, 4, set3);
            }
            for (int k = 0; k < 4; ++k) {
                for (int l = 0; l < 4; ++l) {
                    if (k == set3[l]) {
                        set2[j][k] = l;
                        break;
                    }
                }
            }
        }

        for (int j = 0; j < 256; ++j) {
            unsigned char scrambled = (set2[0][(((j >> set[0]) & 1) << 0)
                                               | (((j >> set[3]) & 1) << 1)] << 0)
                                      | (set2[1][(((j >> set[1]) & 1) << 0)
                                                 | (((j >> set[4]) & 1) << 1)] << 2)
                                      | (set2[2][(((j >> set[2]) & 1) << 0)
                                                 | (((j >> set[5]) & 1) << 1)] << 4)
                                      | (j & 0xc0);
            scramble[i][j] = scrambled;
        }
    }

    int sectors[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 };
    if (file->have_key != 0) {
        permute(key, 17, sectors);
    }

    transwarp_encode_context ctx;
    memset(&ctx, 0, sizeof ctx);
    ctx.version = version;

    ctx.previous1 = initial_buffer_store_value;

    bool done = false;
    int block_index = 0;
    int total_blocks = 0;

    int filepos = 2;

    for (; !done; (track >= DIRTRACK_D41_D71) ? ++track : --track) {
        if ((track < 1)
                || (track > image_num_tracks(type))) {
            fprintf(stderr, "ERROR: Disk full (track %d out of range) while writing Transwarp file ", track);
            print_filename(stderr, file->pfilename);
            fprintf(stderr, "\n");
            exit(-4);
        }

        int next_track_pos = filepos + (num_sectors(type, track) * TRANSWARPBLOCKSIZE);
        bool last_track = (next_track_pos >= *filesize);
        if (last_track) {
            int sector = 0;
            for (int s = 0; s < num_sectors(type, track); ++s) {
                sectors[s] = sector;
                if ((filepos + ((sector + 1) * TRANSWARPBLOCKSIZE)) >= *filesize) {
                    sector = 0;
                } else {
                    ++sector;
                }
            }
        }
        int next_track_block_index = block_index + num_sectors(type, track);
        int trackpos = filepos;

        transwarp_encode_context trackctx = ctx;

        for (int sector = 0; sector < num_sectors(type, track); ++sector) {
            if (is_sector_free(type, image, track, sector, 0 /* numdirblocks */, 0 /* dir_sector_interleave */) == false) {
                fprintf(stderr, "ERROR: t%d/s%d not free for Transwarp file ", track, sector);
                print_filename(stderr, file->pfilename);
                fprintf(stderr, "\n");
                check_bam(type, image);

                exit(-5);
            }

            bool last_block = false;
            int pos = trackpos + (sectors[sector] * TRANSWARPBLOCKSIZE);
            if ((pos + TRANSWARPBLOCKSIZE) >= *filesize) {
                pos = *filesize - TRANSWARPBLOCKSIZE;
                last_block = true;
            }

            unsigned char encoded[320 + 5];
            unsigned char previous = block_index + sectors[sector];
            previous ^= initial_block_recvaccu_value;

            ctx.previous = previous;
            ctx.previous2 = initial_buffer_recvaccu_value;

            int error = encode_transwarp_block((const unsigned char (*)[256]) scramble, gcr_to_nibble, &ctx, filedata, pos, encoded);
            if (error) {
                fprintf(stderr, "ERROR: encoding error on t%d/s%d\n", track, sector);

                exit(-6);
            }

            unsigned char decoded[256];
            int checksum = decode_gcr_block(gcr_to_nibble, encoded, decoded);
            if (checksum < 0) {
                fprintf(stderr, "ERROR: decoding error on t%d/s%d\n", track, sector);

                exit(-7);
            }

            int offset = linear_sector(type, track, sector) * BLOCKSIZE;
            memcpy(image + offset, decoded, BLOCKSIZE);

            ++total_blocks;

            if (last_block) {
                ctx = trackctx;
                done = true;
            }

            mark_sector(type, image, track, sector, 0 /* not free */);
        }

        filepos = next_track_pos;
        block_index = next_track_block_index;
        file->last_track = track;
    }

    file->nrSectors = total_blocks;

    return dirdatakey;
}

/* Find consecutive tracks for Transwarp file, returns wasted blocks, or -1 if file does not fit */
static int
allocate_transwarp(int size, const int sectors[], int num_tracks, int *start_track, int *next_track)
{
    *start_track = *next_track;
    if(*start_track < DIRTRACK_D41_D71 - 1) {
        while(*start_track >= 0) {
            int s = size;
            int t = *start_track;
            while((t >= 0) && sectors[t]) {
                s -= sectors[t];
                if(s <= 0) {
                    *next_track = t ? t - 1 : DIRTRACK_D41_D71 + 1;
                    return -s;
                }
                t--;
            }
            *start_track = t - 1;
        }
        *start_track = DIRTRACK_D41_D71 + 1;
    }
    while(*start_track < num_tracks) {
        int s = size;
        int t = *start_track;
        while((t < num_tracks) && sectors[t]) {
            s -= sectors[t];
            if(s <= 0) {
                *next_track = t + 1;
                return -s;
            }
            t++;
        }
        *start_track = t + 1;
    }
    return -1;
}

/* Calculate wasted space for Transwarp files, returns false if files do not fit */
static bool
calculate_waste(const int size[], int num_files, const int sectors[], int num_tracks, int order[], int *waste, int start_tracks[])
{
    int next_track = DIRTRACK_D41_D71 - 2;
    *waste = 0;

    for(int f = 0; f < num_files; f++) {
        int w = allocate_transwarp(size[order[f]], sectors, num_tracks, &start_tracks[f], &next_track);
        if(w < 0) {
            return false;
        }
        *waste += w;
    }
    return true;
}

/* Find order of Transwarp files with minimal wasted space, returns wasted sectors, or MAXFREESECTORS if the files do not fit */
static int
find_best_permutation(const int size[], int num_files, const int sectors[], int num_tracks, int start_tracks[])
{
    int c[MAXPERMUTEDFILES];
    int o[MAXPERMUTEDFILES];
    int t[MAXPERMUTEDFILES];
    for(int i = 0; i < num_files; i++) {
        c[i] = 0;
        o[i] = i;
    }
    int min_waste;
    if(!calculate_waste(size, num_files, sectors, num_tracks, o, &min_waste, start_tracks)) {
        min_waste = MAXFREESECTORS; /* everything is better than this */
    }
    int original = min_waste;
    int i = 1;
    /* Heap's algorithm for permutation */
    while(i < num_files) {
        if(c[i] < i) {
            int w;
            if((i & 1) == 0) {
                int tmp = o[0];
                o[0] = o[i];
                o[i] = tmp; /* swap o[0] and o[i] */
            } else {
                int tmp = o[c[i]];
                o[c[i]] = o[i];
                o[i] = tmp; /* swap o[c[i]] and o[i] */
            }
            c[i]++;
            i = 1;
            if(calculate_waste(size, num_files, sectors, num_tracks, o, &w, t) && (w < min_waste)) {
                min_waste = w;
                for(int j = 0; j < num_files; j++) {
                    start_tracks[o[j]] = t[j];
                }
            }
        } else {
            c[i] = 0;
            i++;
        }
    }
    if(verbose) {
        if(original == MAXFREESECTORS) {
            printf("\nTranswarp reordering made files fit on disk\n");
        } else {
            printf("\nTranswarp reordering saved %d blocks\n", original - min_waste);
        }
    }
    return min_waste;
}

/* Reorder files, find best Transwarp file order and set start tracks accordingly */
static void
optimize_transwarp_allocation(image_type type, unsigned char *image, imagefile *files, int num_files, int order[])
{
    /* find free tracks */
    int sectors[40]; /* size is number of tracks for extended D64 */
    unsigned int num_tracks = image_num_tracks(type);
    for (unsigned int t = 1; t <= num_tracks; ++t) {
        int ns = num_sectors(type, t);
        bool track_free = true;
        for (int sector = 0; sector < ns; ++sector) {
            if (is_sector_free(type, image, t, sector, 0 /* numdirblocks */, 0 /* dir_sector_interleave */) == false) {
                track_free = false;
                break;
            }
        }
        sectors[t-1] = track_free ? ns : 0;
    }

    /* sort files */
    int o = 0;
    for(int f = 0; f < num_files; f++) {
        /* Transwarp files with -t */
        int start_track = (files[f].mode & MODE_MIN_TRACK_MASK) >> MODE_MIN_TRACK_SHIFT;
        if((files[f].filetype & FILETYPETRANSWARPMASK) && (start_track > 0)) {
            start_track--; /* convert to array index */
            order[o++] = f;
            /* block tracks */
            int s = 0;
            struct stat st;
            if (stat((char*)files[f].alocalname, &st) == 0) {
                s = ((int)st.st_size - 2 - 1) / TRANSWARPBLOCKSIZE + 1; /* number of Transwarp blocks, rounded up */
            }
            unsigned int track_idx = start_track;
            while(s > 0) {
                s -= sectors[track_idx];
                sectors[track_idx] = 0;
                if(track_idx > DIRTRACK_D41_D71 - 2) {
                    track_idx++;
                    if(track_idx == num_tracks) {
                        return; /* give up, let actual file allocation deal with the error */
                    }
                } else if(track_idx == 0) {
                    track_idx = DIRTRACK_D41_D71;
                } else {
                    track_idx--;
                }
            }
        }
    }
    int first_transwarp = o;
    int sizes[MAXPERMUTEDFILES];
    int num_transwarp = 0;
    for(int f = 0; f < num_files; f++) {
        /* other Transwarp files */
        if((files[f].filetype & FILETYPETRANSWARPMASK) && ((files[f].mode & MODE_MIN_TRACK_MASK) <= 0)) {
            order[o++] = f;
            if(num_transwarp < MAXPERMUTEDFILES) {
                struct stat st;
                if (stat((char*)files[f].alocalname, &st) == 0) {
                    sizes[num_transwarp] = ((int)st.st_size - 2 - 1) / TRANSWARPBLOCKSIZE + 1; /* number of Transwarp blocks, rounded up */
                }
                num_transwarp++;
            }
        }
    }
    for(int f = 0; f < num_files; f++) {
        /* normal files */
        if(!(files[f].filetype & FILETYPETRANSWARPMASK)) {
            order[o++] = f;
        }
    }
    int start_tracks[MAXPERMUTEDFILES];
    find_best_permutation(sizes, num_transwarp, sectors, num_tracks, start_tracks);

    for(int f = 0; f < num_transwarp; f++) {
        files[order[first_transwarp + f]].mode |= ((start_tracks[f]+1) << MODE_MIN_TRACK_SHIFT);
    }
}

/* Write files to disk */
static void
write_files(image_type type, unsigned char *image, imagefile *files, int num_files, int usedirtrack, int dirtracksplit, int shadowdirtrack, int numdirblocks, int dir_sector_interleave, int allocation_strategy)
{
    unsigned char sector = 0;
    int bytes_to_write = 0;
    int lastTrack = 1;
    int lastSector = sector;
    int lastOffset = linear_sector(type, lastTrack, lastSector) * BLOCKSIZE;
    int lastMinTrack = 1;
    bool transwarp_bootfile_fits_on_dir_track = false;
    int *allocation_order = NULL;

    if (num_files > 0) {
        /* make sure the first file already takes first sector per track into account */
        sector = (type == IMAGE_D81) ? 0 : files[0].first_sector_new_track;
        /* indirection to allow arbitrary disk allocation order */
        allocation_order = calloc(num_files, sizeof(int));
        if(allocation_order == NULL) {
            fprintf(stderr, "ERROR: Memory allocation error\n");
            exit(-1);
        }
        for(int i = 0; i < num_files; i++) {
            allocation_order[i] = i;
        }
        if(allocation_strategy != 0) {
            optimize_transwarp_allocation(type, image, files, num_files, allocation_order);
        }
    }

    int transwarp_version = 100;

    for (int i = 0; i < num_files; i++) {
        imagefile *file = files + allocation_order[i];
        if ((file->mode & MODE_TRANSWARPBOOTFILE) != 0) {
            int fileSize = 0;

            struct stat st;
            if (stat((char*)file->alocalname, &st) == 0) {
                fileSize = (int)st.st_size;
            }

            int version_major;
            int version_minor;
            if (sscanf((char *) basename(file->alocalname), "transwarp v%d.%d", &version_major, &version_minor) == 2) {
                transwarp_version = (version_major * 100) + version_minor;
            }

            int num_blocks = (fileSize / 254) + ((fileSize % 254 == 0) ? 0 : 1);
            transwarp_bootfile_fits_on_dir_track = (num_blocks <= 17);

            break;
        }
    }

    for (int i = 0; i < num_files; i++) {
        imagefile *file = files + allocation_order[i];
        unsigned char track = 1; /* reset start search track for every file, as Transwarp and -r might leave free blocks below */

        if (type == IMAGE_D81) {
            file->sectorInterleave = 1; /* caught in command line parsing anyway, but does not hurt */
        }

        int file_usedirtrack = usedirtrack;
        int file_numdirblocks = numdirblocks;

        if ((file->mode & MODE_NOFILE)) {
            if (file->nrSectorsShown == -1) {
                file->nrSectorsShown = file->nrSectors;
            }
            file->track = 0;
            file->sector = 0;
            int entryOffset = linear_sector(type, dirtrack(type), file->direntrysector) * BLOCKSIZE + file->direntryoffset;
            image[entryOffset + FILETRACKOFFSET] = file->track;
            image[entryOffset + FILESECTOROFFSET] = file->sector;
            image[entryOffset + FILEBLOCKSLOOFFSET] = file->nrSectorsShown & 255;
            image[entryOffset + FILEBLOCKSHIOFFSET] = file->nrSectorsShown >> 8;
            if (shadowdirtrack > 0) {
                entryOffset = linear_sector(type, shadowdirtrack, file->direntrysector) * BLOCKSIZE + file->direntryoffset;
                image[entryOffset + FILETRACKOFFSET] = file->track;
                image[entryOffset + FILESECTOROFFSET] = file->sector;
                image[entryOffset + FILEBLOCKSLOOFFSET] = file->nrSectors & 255;
                image[entryOffset + FILEBLOCKSHIOFFSET] = file->nrSectors >> 8;
            }
        } else if (!(file->mode & MODE_LOOPFILE)) { /* loop files are handled later */

            int fileSize = 0;

            struct stat st;
            if (stat((char*)file->alocalname, &st) == 0) {
                fileSize = (int)st.st_size;
            }

            if ((file->mode & MODE_TRANSWARPBOOTFILE) != 0) {
                file_usedirtrack = true;
                file_numdirblocks = transwarp_bootfile_fits_on_dir_track ? 2 : 4;
                file->sectorInterleave = -4;
                file->mode = (file->mode & ~MODE_BEGINNING_SECTOR_MASK) | (10 + 1);
                file->first_sector_new_track = 10;
                track = DIRTRACK_D41_D71;
            }

            unsigned char* filedata = (unsigned char*)calloc(fileSize + ((file->filetype & FILETYPETRANSWARPMASK) ? (21 * TRANSWARPBLOCKSIZE) : 0), sizeof(unsigned char));
            if (filedata == NULL) {
                fprintf(stderr, "ERROR: Memory allocation error\n");

                exit(-1);
            }
            FILE* f = fopen((char*)file->alocalname, "rb");
            if (f == NULL) {
                fprintf(stderr, "ERROR: Could not open file \"%s\" for reading\n", file->alocalname);

                exit(-1);
            }
            if (fread(filedata, fileSize, 1, f) != 1) {
                fprintf(stderr, "ERROR: Unexpected filesize when reading %s\n", file->alocalname);
                exit(-1);
            }
            fclose(f);

            if ((!(file->filetype & FILETYPETRANSWARPMASK))
                    && ((file->mode & MODE_MIN_TRACK_MASK) > 0)) {
                int minTrack = (file->mode & MODE_MIN_TRACK_MASK) >> MODE_MIN_TRACK_SHIFT;
                if (lastMinTrack != minTrack) {
                    lastMinTrack = minTrack;
                    track = minTrack;
                    /* note that track may be smaller than lastTrack now */
                    if (track > image_num_tracks(type)) {
                        fprintf(stderr, "ERROR: Invalid minimum track %u for file %s (", track, file->alocalname);
                        print_filename(stderr, file->pfilename);
                        fprintf(stderr, ") specified\n");

                        exit(-1);
                    }
                    while ((!file_usedirtrack)
                            && ((track == dirtrack(type))
                                || (track == shadowdirtrack)
                                || ((type == IMAGE_D71) && (track == (D64NUMTRACKS + dirtrack(type)))))) { /* .d71 track 53 is usually empty except the extra BAM block */
                        ++track; /* skip dir track */
                    }
                    if (abs(((int) track) - lastTrack) > 1) {
                        /* previous file's last track and this file's beginning track have tracks in between */
                        sector = (type == IMAGE_D81) ? 0 : file->first_sector_new_track;
                    }
                }
            } else {
                lastMinTrack = 0;
            }

            if ((file->mode & MODE_BEGINNING_SECTOR_MASK) > 0) {
                sector = (file->mode & MODE_BEGINNING_SECTOR_MASK) - 1;
            }

            if ((!(file->filetype & FILETYPETRANSWARPMASK))
                    && (((file->mode & MODE_SAVETOEMPTYTRACKS) != 0)
                        || ((file->mode & MODE_FITONSINGLETRACK) != 0))) {

                /* find first empty track */
                int found = 0;
                while (!found) {
                    for (int s = 0; s < num_sectors(type, track); s++) {
                        if (is_sector_free(type, image, track, s, file_usedirtrack ? file_numdirblocks : 0, dir_sector_interleave)) {
                            if (s == (num_sectors(type, track) - 1)) {
                                found = 1;
                                /* In first pass, use sector as left by previous file (or as set by -b) to reach first file block quickly. */
                                /* Claus: according to Krill, on real HW tracks are not aligned anyway, so it does not make a difference. */
                                /* Emulators tend to reset the disk angle on track changes, so this should rather be 3. */
                                if (sector >= num_sectors(type, track)) {
                                    if ((file->mode & MODE_BEGINNING_SECTOR_MASK) > 0) {
                                        fprintf(stderr, "ERROR: Invalid beginning sector %u on track %u for file %s (", sector, track, file->alocalname);
                                        print_filename(stderr, file->pfilename);
                                        fprintf(stderr, ") specified\n");

                                        exit(-1);
                                    }

                                    sector %= num_sectors(type, track);
                                }
                            }
                        } else {
                            int prev_track = track;
                            if (file->mode & MODE_SAVECLUSTEROPTIMIZED) {
                                if (track > D64NUMTRACKS) {
                                    int next_track = track - D64NUMTRACKS + 1; /* to next track on first side */
                                    if (next_track < D64NUMTRACKS) {
                                        track = next_track;
                                    } else {
                                        ++track; /* disk full */
                                    }
                                } else {
                                    track += D64NUMTRACKS; /* to same track on second side */
                                }
                            } else {
                                ++track;
                            }
                            while ((!file_usedirtrack)
                                    && ((track == dirtrack(type))
                                        || (track == shadowdirtrack)
                                        || ((type == IMAGE_D71) && (track == D64NUMTRACKS + dirtrack(type))))) { /* .d71 track 53 is usually empty except the extra BAM block */
                                ++track; /* skip dir track */
                            }
                            if (file->mode & MODE_FITONSINGLETRACK) {
                                int file_size = fileSize;
                                int first_sector = -1;
                                for (int s = 0; s < num_sectors(type, prev_track); s++) {
                                    if (is_sector_free(type, image, prev_track, s, file_usedirtrack ? file_numdirblocks : 0, dir_sector_interleave)) {
                                        if (first_sector < 0) {
                                            first_sector = s;
                                        }
                                        file_size -= BLOCKSIZE + BLOCKOVERHEAD;
                                        if (file_size <= 0) {
                                            found = 1;
                                            track = prev_track;
                                            sector = first_sector;
                                            break;
                                        }
                                    }
                                }
                            }

                            if (track > image_num_tracks(type)) {
                                fprintf(stderr, "ERROR: Disk full, file %s (", file->alocalname);
                                print_filename(stderr, file->pfilename);
                                fprintf(stderr, ")\n");

                                exit(-1);
                            }
                            break;
                        }
                    } /* for each sector on track */

                    if ((track == (lastTrack + 2))
                            && ((file->mode & MODE_BEGINNING_SECTOR_MASK) == 0)) {
                        /* previous file's last track and this file's beginning track have tracks in between now */
                        sector = 0;
                    }
                } /* while not found */
            }

            if ((file->mode & MODE_BEGINNING_SECTOR_MASK) > 0) {
                if (sector != ((file->mode & MODE_BEGINNING_SECTOR_MASK) - 1)) {
                    fprintf(stderr, "ERROR: Specified beginning sector of file %s (", file->alocalname);
                    print_filename(stderr, file->pfilename);
                    fprintf(stderr, ") not free on track %u\n", track);

                    exit(-1);
                }
            }

            /* found start track, now save file */
            if (type == IMAGE_D81) {
                sector = 0;
            }

            int byteOffset = 0;
            int bytesLeft = fileSize;

            unsigned long long key0 = 0;
            if (file->filetype & FILETYPETRANSWARPMASK) {
                key0 = write_transwarp_file(type, image, file, filedata, &fileSize, transwarp_version, transwarp_bootfile_fits_on_dir_track);

                bytesLeft = 0;
            }

            while (bytesLeft > 0) {
                /* Find free track & sector, starting from current T/S forward one revolution, then the next track etc... skip dirtrack (unless -t is active) */
                /* If the file didn't fit before dirtrack then restart on dirtrack + 1 and try again (unless -t is active). */
                /* If the file didn't fit before track 36/41/71 then the disk is full. */

                int blockfound = 0;
                int findSector = 0;

                while (!blockfound) {
                    /* find spare block on the current track */
                    for (int s = sector; s < sector + num_sectors(type, track); s++) {
                        findSector = s % num_sectors(type, track);

                        if (is_sector_free(type, image, track, findSector, file_usedirtrack ? file_numdirblocks : 0, dir_sector_interleave)) {
                            blockfound = 1;
                            break;
                        }
                    }

                    if (!blockfound) {
                        /* find next track, use some magic to make up for track seek delay */
                        int seek_delay = 1;
                        if (file->mode & MODE_SAVECLUSTEROPTIMIZED) {
                            if (track > D64NUMTRACKS) {
                                track = track - D64NUMTRACKS + 1;
                            } else {
                                track += D64NUMTRACKS;
                                seek_delay = 0; /* switching to the other side, no head movement */
                            }
                        } else {
                            ++track;
                        }
                        if (type == IMAGE_D81) {
                            sector = 0;
                        } else if (file->first_sector_new_track < 0) {
                            sector -= file->first_sector_new_track;
                        } else if ((file->sectorInterleave < 0)
                                   && ((file->mode & MODE_TRANSWARPBOOTFILE) == 0)) {
                            sector += seek_delay;
                        } else {
                            sector = file->first_sector_new_track;
                        }
                        sector %= num_sectors(type, track);
                        findSector = sector;

                        while ((!file_usedirtrack)
                                && ((track == dirtrack(type))
                                    || (track == shadowdirtrack)
                                    || ((type == IMAGE_D71) && (track == D64NUMTRACKS + dirtrack(type))))) { /* .d71 track 53 is usually empty except the extra BAM block */
                            /* Delete old fragments and restart file */
                            if (!dirtracksplit) {
                                if (file->nrSectors > 0) {
                                    int deltrack = file->track;
                                    int delsector = file->sector;
                                    while (deltrack != 0) {
                                        int b = linear_sector(type, deltrack, delsector);
                                        if(b < 0) {
                                            break;
                                        }
                                        int offset = b * BLOCKSIZE;
                                        mark_sector(type, image, deltrack, delsector, 1 /* free */);
                                        deltrack = image[offset + 0];
                                        delsector = image[offset + 1];
                                        memset(image + offset, 0, BLOCKSIZE);
                                    }
                                }

                                bytesLeft = fileSize;
                                byteOffset = 0;
                                file->nrSectors = 0;
                            }
                            ++track;
                        }

                        if (track > image_num_tracks(type)) {
                            if (verbose) {
                                print_file_allocation(type, image, files, num_files);
                                check_bam(type, image);
                            }

                            fprintf(stderr, "ERROR: Disk full, file %s (", file->alocalname);
                            print_filename(stderr, file->pfilename);
                            fprintf(stderr, ")\n");
                            free(filedata);

                            exit(-1);
                        }
                    }
                } /* while not block found */

                sector = findSector;
                int offset = linear_sector(type, track, sector) * BLOCKSIZE;

                if (bytesLeft == fileSize) {
                    file->track = track;
                    file->sector = sector;
                    lastTrack = track;
                    lastSector = sector;
                    lastOffset = offset;
                } else {
                    image[lastOffset + 0] = track;
                    image[lastOffset + 1] = sector;
                }

                /* write sector */
                bytes_to_write = min(BLOCKSIZE - BLOCKOVERHEAD, bytesLeft);
                memset(image + offset + 2, 0, 254);
                memcpy(image + offset + 2, filedata + byteOffset, bytes_to_write);

                bytesLeft -= bytes_to_write;
                byteOffset += bytes_to_write;

                lastTrack = track;
                lastSector = sector;
                lastOffset = offset;

                mark_sector(type, image, track, sector, 0 /* not free */);

                if (num_sectors(type, track) <= abs(file->sectorInterleave)) {
                    fprintf(stderr, "ERROR: Invalid interleave %d on track %u (%d sectors), file %s (", file->sectorInterleave, track, num_sectors(type, track), file->alocalname);
                    print_filename(stderr, file->pfilename);
                    fprintf(stderr, ")\n");

                    exit(-1);
                }

                sector += abs(file->sectorInterleave);
                sector %= num_sectors(type, track);

                file->nrSectors++;
            } /* while bytes left */

            if (!(file->filetype & FILETYPETRANSWARPMASK)) {
                image[lastOffset + 0] = 0x00;
                image[lastOffset + 1] = bytes_to_write + 1;
            }

            /* update directory entry */
            int entryOffset = linear_sector(type, dirtrack(type), file->direntrysector) * BLOCKSIZE + file->direntryoffset;
            image[entryOffset + FILETRACKOFFSET] = file->track;
            image[entryOffset + FILESECTOROFFSET] = file->sector;

            if (file->filetype & FILETYPETRANSWARPMASK) {
                image[entryOffset + FILETRACKOFFSET] = 0;
                image[entryOffset + FILESECTOROFFSET] = 0;

                image[entryOffset + TRANSWARPSIGNATROFFSLO] = TRANSWARPSIGNATURELO;
                image[entryOffset + TRANSWARPSIGNATROFFSHI] = TRANSWARPSIGNATUREHI;

                image[entryOffset + TRANSWARPTRACKOFFSET] = file->track;

                image[entryOffset + FILEBLOCKSLOOFFSET] = file->nrSectors;
                image[entryOffset + FILEBLOCKSHIOFFSET] = (file->nrSectors >> 8);
                if (image[entryOffset + FILEBLOCKSHIOFFSET] > 0) {
                    fprintf(stderr, "ERROR: Transwarp file \"%s\" is %d > 255 blocks big\n", file->alocalname, file->nrSectors);

                    exit(-8);
                }

                int loadaddress = (filedata[1] << 8) | filedata[0];
                image[entryOffset + LOADADDRESSLOOFFSET] = loadaddress;
                image[entryOffset + LOADADDRESSHIOFFSET] = (loadaddress >> 8);
                int endaddress = loadaddress + fileSize - 2;
                image[entryOffset + ENDADDRESSLOOFFSET] = endaddress;
                image[entryOffset + ENDADDRESSHIOFFSET] = (endaddress >> 8);

                unsigned char file_checksum = 0xff;
                for (int i = 2; i < fileSize; ++i) {
                    file_checksum ^= filedata[i];
                    file_checksum = crc8(file_checksum);
                }
                image[entryOffset + FILECHECKSUMOFFSET] = file_checksum;

                image[entryOffset + DIRDATACHECKSUMOFFSET] = 0;
                image[entryOffset + DIRDATACHECKSUMOFFSET] = (0x0100 - transwarp_dirdata_checksum(image, entryOffset));
                unsigned char dirdata_checksum = transwarp_dirdata_checksum(image, entryOffset);
                if (dirdata_checksum != 0) {
                    if (dirdata_checksum == 1) {
                        --image[entryOffset + DIRDATACHECKSUMOFFSET];
                    }
                    dirdata_checksum = transwarp_dirdata_checksum(image, entryOffset);
                    if (dirdata_checksum != 0) {
                        fprintf(stderr, "ERROR: Encoding error with \"%s\", 0x%x\n", file->alocalname, dirdata_checksum);

                        exit(-9);
                    }
                }

                if (file->have_key != 0) {
                    for (int offset = DIRDATACHECKSUMOFFSET; offset <= FILEBLOCKSLOOFFSET; ++offset) {
                        image[entryOffset + offset] ^= key0;
                        key0 >>= 8;
                    }

                    file->nrSectors = image[entryOffset + FILEBLOCKSLOOFFSET];
                }
            }

            if (file->nrSectorsShown < 0) {
                file->nrSectorsShown = file->nrSectors;
            }
            image[entryOffset + FILEBLOCKSLOOFFSET] = file->nrSectorsShown & 255;
            image[entryOffset + FILEBLOCKSHIOFFSET] = file->nrSectorsShown >> 8;

            if (shadowdirtrack > 0) {
                entryOffset = linear_sector(type, shadowdirtrack, file->direntrysector) * BLOCKSIZE + file->direntryoffset;
                image[entryOffset + FILETRACKOFFSET] = file->track;
                image[entryOffset + FILESECTOROFFSET] = file->sector;

                image[entryOffset + FILEBLOCKSLOOFFSET] = file->nrSectors & 255;
                image[entryOffset + FILEBLOCKSHIOFFSET] = file->nrSectors >> 8;
            }

            free(filedata);
        }
    } /* for each file */

    /* Set track/sector of Transwarp file entries to Transwarp bootfile */
    int transwarp_boot_track = 0;
    int transwarp_boot_sector = 0;
    char *blockmap = calloc(image_num_blocks(type), sizeof(char));
    if(blockmap == NULL) {
        fprintf(stderr, "ERROR: Memory allocation error\n");
        exit(-1);
    }

    for (int i = 0; i < num_files; i++) {
        imagefile *file = files + allocation_order[i];
        if (file->filetype & FILETYPETRANSWARPMASK) {
            if (transwarp_boot_track == 0) {
                /* find Transwarp bootfile */

                int t = dirtrack(type);
                int s = (type == IMAGE_D81) ? 3 : 1;
                int o = 0;

                do {
                    int b = linear_sector(type, t, s) * BLOCKSIZE + o;
                    int filetype = image[b + FILETYPEOFFSET] & 0xf;
                    if (filetype == FILETYPEDEL) {
                        continue;
                    }

                    if (is_transwarp_file(image, b)) {
                        continue;
                    }

                    if (!is_transwarp_bootfile(image, b)) {
                        continue;
                    }

                    int filetrack = image[b + FILETRACKOFFSET];
                    int filesector = image[b + FILESECTOROFFSET];
                    if (filetrack == 0) {
                        continue;
                    }

                    if (verbose) {
                        printf("\nTranswarp bootfile at T%d/S%d\n", filetrack, filesector);
                    }

                    transwarp_boot_track = filetrack;
                    transwarp_boot_sector = filesector;

                    break;
                } while (next_dir_entry(type, image, &t, &s, &o, blockmap));
            }

            if (transwarp_boot_track == 0) {
                fprintf(stderr, "ERROR: No Transwarp bootfile provided\n");

                exit(-10);
            }

            int b = linear_sector(type, dirtrack(type), file->direntrysector) * BLOCKSIZE + file->direntryoffset;
            image[b + FILETRACKOFFSET] = transwarp_boot_track;
            image[b + FILESECTOROFFSET] = transwarp_boot_sector;
        }
    }
    free(blockmap);

    /* update loop files */
    for (int i = 0; i < num_files; i++) {
        imagefile *file = files + allocation_order[i];
        if (((file->filetype & 0xf) != FILETYPEDEL) && (file->mode & MODE_LOOPFILE)) {
            int track, sector, offset;
            int index;
            if (find_existing_file(type, image, file->plocalname, &index, &track, &sector, &offset)) {
                /* read track/sector and nrSectors from disk image */
                int b = linear_sector(type, track, sector) * BLOCKSIZE + offset;
                file->track = image[b + FILETRACKOFFSET];
                file->sector = image[b + FILESECTOROFFSET];
                file->nrSectors = image[b + FILEBLOCKSLOOFFSET] + (image[b + FILEBLOCKSHIOFFSET] << 8);

                /* update directory entry */
                b = linear_sector(type, dirtrack(type), file->direntrysector) * BLOCKSIZE + file->direntryoffset;
                image[b + FILETRACKOFFSET] = file->track;
                image[b + FILESECTOROFFSET] = file->sector;

                if (file->nrSectorsShown == -1) {
                    file->nrSectorsShown = file->nrSectors;
                }
                image[b + FILEBLOCKSLOOFFSET] = file->nrSectorsShown & 255;
                image[b + FILEBLOCKSHIOFFSET] = file->nrSectorsShown >> 8;

                if (shadowdirtrack > 0) {
                    b = linear_sector(type, shadowdirtrack, file->direntrysector) * BLOCKSIZE + file->direntryoffset;
                    image[b + FILETRACKOFFSET] = file->track;
                    image[b + FILESECTOROFFSET] = file->sector;

                    image[b + FILEBLOCKSLOOFFSET] = file->nrSectors & 255;
                    image[b + FILEBLOCKSHIOFFSET] = file->nrSectors >> 8;
                }

                for (int j = 0; j < num_files; j++) {
                    imagefile *other_file = files + allocation_order[j];
                    if ((i != j)
                            && (file->track == other_file->track)
                            && (file->sector == other_file->sector)) {
                        file->sectorInterleave = other_file->sectorInterleave;

                        break;
                    }
                }

                continue;
            } else {
                fprintf(stderr, "ERROR: Loop source file '%s' (%d) not found\n", file->alocalname, i + 1);
                exit(-1);
            }
        }
    }
    free(allocation_order);
}

/* Writes 16 bit value to file */
static size_t
write16(unsigned int value, FILE* f)
{
    char byte = value & 0xff;
    size_t bytes_written = fwrite(&byte, 1, 1, f);

    byte = (value >> 8) & 0xff;
    bytes_written += fwrite(&byte, 1, 1, f);

    return bytes_written;
}

/* Writes 32 bit value to file */
static size_t
write32(unsigned int value, FILE* f)
{
    size_t bytes_written = write16(value, f);
    bytes_written += write16(value >> 16, f);

    return bytes_written;
}

/* Writes image as G64 file */
static int
generate_uniformat_g64(unsigned char* image, const char *imagepath)
{
    FILE* f = fopen(imagepath, "wb");

    size_t filepos = 0;

    static const char signature[] = "GCR-1541";
    filepos += fwrite(signature, 1, sizeof signature - 1, f);

    const char version = 0;
    filepos += fwrite(&version, 1, 1, f);

    const char num_tracks = 35 * 2;
    filepos += fwrite(&num_tracks, 1, 1, f);

    const unsigned int track_size = 7692; /* = track_bytes on tracks 1..17 */
    filepos += write16(track_size, f);

    const unsigned int table_size = num_tracks * 4;
    const unsigned int tracks_offset = (int)filepos + (table_size * 2);

    for (int track = 0; track < num_tracks; ++track) {
        unsigned int track_offset = 0;

        if ((track & 1) == 0) {
            track_offset = tracks_offset + ((track >> 1) * (2 + track_size));
        }

        filepos += write32(track_offset, f);
    }

    for (int track = 0; track < num_tracks; ++track) {
        unsigned int bit_rate = 0;

        if ((track & 1) == 0) {
            switch (sectors_per_track[track >> 1]) {
            case 21:
                bit_rate = 3;
                break;
            case 19:
                bit_rate = 2;
                break;
            case 18:
                bit_rate = 1;
                break;
            case 17:
                bit_rate = 0;
                break;
            }
        }

        filepos += write32(bit_rate, f);
    }

    const unsigned char sync[] = { 0xff, 0xff, 0xff, 0xff, 0xff };
    const char gap[] = { 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55 };
    char header_gcr[10];

    const unsigned int block_size =
        (sizeof sync)
        + (sizeof header_gcr)
        + (sizeof gap)
        + (sizeof sync)
        + 325; /* data */

    const char id[3] = { '2', 'A', '\0' };

    bool is_uniform = true;

    for (int track = 0; track < (num_tracks >> 1); ++track) {
        int track_bytes = 0;
        int num_sectors = sectors_per_track[track];
        switch (num_sectors) {
        case 21:
            track_bytes = 7692;
            break; /* = track_size */
        case 19:
            track_bytes = 7142;
            break;
        case 18:
            track_bytes = 6666;
            break;
        case 17:
            track_bytes = 6250;
            break;
        }

        filepos += write16(track_bytes, f);
        size_t track_begin = filepos;

        int data_bytes = num_sectors * block_size;
        int gap_size = (track_bytes - data_bytes) / num_sectors;
        if (gap_size < 0) {
            printf("\nERROR: Track too small for G64 output\n");
            fclose(f);

            return -1;
        }

        float average_gap_remainder = (((float) (track_bytes - data_bytes)) / num_sectors) - gap_size;
        if (average_gap_remainder >= 1.0f) {
            average_gap_remainder = 0.0f; /* 0..1 */
        }

        float remainder = 0.0f;
        for (int sector = 0; sector < num_sectors; ++sector) {
            unsigned int gap_bytes = gap_size;
            remainder += average_gap_remainder;
            if (remainder >= 0.5f) {
                remainder -= 1.0f;
                ++gap_bytes;
            }

            filepos += fwrite(sync, 1, sizeof sync, f);
            char header[8] = {
                0x08, /* header ID */
                (char) (sector ^ (track + 1) ^ id[1] ^ id[0]), /* checksum */
                (char) sector,
                (char) (track + 1),
                id[1],
                id[0],
                0x0f, 0x0f
            };

            encode_4_bytes_gcr(header, header_gcr);
            encode_4_bytes_gcr(header + 4, header_gcr + 5);

            filepos += fwrite(header_gcr, 1, sizeof header_gcr, f);
            filepos += fwrite(gap, 1, sizeof gap, f);

            filepos += fwrite(sync, 1, sizeof sync, f);

            char group[5];

            char checksum = image[0] ^ image[1] ^ image[2];
            char data[4] = { 0x07, (char) image[0], (char) image[1], (char) image[2] };
            encode_4_bytes_gcr(data, group);
            filepos += fwrite(group, 1, sizeof group, f);
            for (int i = 0; i < 0x3f; ++i) {
                data[0] = image[(i * 4) + 3];
                data[1] = image[(i * 4) + 4];
                data[2] = image[(i * 4) + 5];
                data[3] = image[(i * 4) + 6];
                encode_4_bytes_gcr(data, group);
                filepos += fwrite(group, 1, sizeof group, f);
                checksum ^= (data[0] ^ data[1] ^ data[2] ^ data[3]);
            }
            data[0] = image[0xff];
            data[1] = data[0] ^ checksum;
            data[2] = 0;
            data[3] = 0;
            encode_4_bytes_gcr(data, group);
            filepos += fwrite(group, 1, sizeof group, f);

            for (int i = gap_bytes; i > 0; --i) {
                filepos += fwrite(gap, 1, 1, f);
            }

            image += 0x0100;
        } /* for each sector */

        size_t tail_gap = track_bytes - filepos + track_begin;
        if (tail_gap > 0) {
            for (size_t i = tail_gap; i > 0; --i) {
                filepos += fwrite(gap, 1, 1, f);
            }

            is_uniform = false;
        }

        for (int i = (track_size - track_bytes); i > 0; --i) {
            filepos += fwrite(sync, 1, 1, f);
        }
    } /* for each track */

    fclose(f);

    if (!is_uniform) {
        printf("\nWARNING: \"%s\" is not UniFormAt'ed\n", imagepath);
    }

    return 0;
}

/* Generates a unique filename, either based on the proposed name, or using track and sector. */
static void
generate_unique_filename(image_type type, unsigned char *image, unsigned char *name, int track, int sector, int start, char marker)
{
    int i, t, s, o;
    if(name[0] == 0xa0 || name[0] == 0) {
        /* no name provided, create one */
        name[0] = a2p('t');
        pputnum2(name+1, track);
        name[3] = a2p('s');
        pputnum2(name+4, sector);
        name[6] = a2p('$');
        pputhex(name+7, start);
        for(int pos = 11; pos < FILENAMEMAXSIZE; pos++) {
            name[pos] = 0xa0;
        }
    }
    int marker_pos = pstrlen(name);
    if(marker_pos > FILENAMEMAXSIZE-1) {
        marker_pos = FILENAMEMAXSIZE-1;
    }
    if(marker) {
        name[marker_pos] = marker;
    } else {
        marker_pos++;
    }
    int appendix = 1;
    int appendix_len = 2;
    int namelen = pstrlen(name);
    while(find_existing_file(type, image, name, &i, &t, &s, &o)) {
        marker_pos = namelen + appendix_len;
        if(marker_pos > FILENAMEMAXSIZE-1) {
            marker_pos = FILENAMEMAXSIZE-1;
            if(!marker) {
                marker_pos++;
            }
        }
        if(marker) {
            name[marker_pos] = marker;
        }
        int appendix_pos = marker_pos - appendix_len;
        name[appendix_pos] = '.';
        pputnum(name + appendix_pos + 1, appendix);
        appendix++;
        if(appendix == 10) {
            appendix_len++;
        }
        if(appendix == 100) {
            appendix_len++;
        }
    }
}

/* Count number of blocks in file, assumes valid t/s chain */
static int
count_blocks(image_type type, unsigned char *image, unsigned int track, int sector)
{
    int num = 0;
    while(track != 0) {
        int b = linear_sector(type, track, sector);
        if(b < 0) {
            break;
        }
        int block_offset = b * BLOCKSIZE;
        track = image[block_offset + TRACKLINKOFFSET];
        sector = image[block_offset + SECTORLINKOFFSET];
        num++;
    };
    return num;
}

/* Overwrites all blocks that are marked as potentially allocated with the given value */
static void
mark_sector_chain(image_type type, unsigned char *image, char* atab, unsigned int track, int sector, unsigned int last_track, int last_sector, int mark)
{
    if(track <= 0 || track > image_num_tracks(type) || sector < 0 || sector > num_sectors(type, track)) {
        return;
    }
    while(1) {
        int b = linear_sector(type, track, sector);
        atab[b] = mark;
        if(track == last_track && sector == last_sector) {
            break;
        }
        int block_offset = b * BLOCKSIZE;
        track = image[block_offset + TRACKLINKOFFSET];
        sector = image[block_offset + SECTORLINKOFFSET];
    };
}

/* Validates sector chain starting at track/sector, returns how and in which t/s the chain ends */
static int
validate_sector_chain(image_type type, unsigned char* image, char* atab, unsigned int track, int sector, unsigned int *last_track, int *last_sector)
{
    *last_track = track;
    *last_sector = sector;
    if(track == 0 || track > image_num_tracks(type)) {
        return FIRST_BROKEN;
    }
    if(sector >= num_sectors(type, track)) {
        return FIRST_BROKEN;
    }
    if(atab[linear_sector(type, track, sector)] != UNALLOCATED) {
        return FIRST_BROKEN;
    }
    while (1) {
        atab[linear_sector(type, track, sector)] = POTENTIALLYALLOCATED;
        int block_offset = linear_sector(type, track, sector) * BLOCKSIZE;
        unsigned int next_track = image[block_offset + TRACKLINKOFFSET];
        int next_sector = image[block_offset + SECTORLINKOFFSET];
        if (next_track == 0) {
            return VALID; /* end of valid chain */
        }
        if (next_track > image_num_tracks(type)) {
            return ILLEGAL_TRACK;
        }
        if (next_sector >= num_sectors(type, next_track)) {
            return ILLEGAL_SECTOR;
        }
        switch(atab[linear_sector(type, next_track, next_sector)]) {
        case POTENTIALLYALLOCATED:
            return LOOP;
        case ALLOCATED:
            return COLLISION;
        case FILESTART:
            return CHAINED;
        case FILESTART_TRUNCATED:
            return CHAINED_TRUNCATED;
        }
        track = next_track;
        sector = next_sector;
        *last_track = track;
        *last_sector = sector;
    }
}

/* Allocates and initialises a table to track block allocation */
static void
init_atab(image_type type, unsigned char* image, char* atab)
{
    int dt = dirtrack(type);
    int ds = (type == IMAGE_D81) ? 3 : 1;
    int offset = 0;
    char *blockmap = calloc(image_num_blocks(type), sizeof(char));
    if(blockmap == NULL) {
        fprintf(stderr, "ERROR: Memory allocation error\n");
        exit(-1);
    }

    do {
        int db = linear_sector(type, dt, ds);
        atab[db] = ALLOCATED;
        int filetype = image[db * BLOCKSIZE + offset + FILETYPEOFFSET] & 0xf;
        if(filetype != FILETYPEDEL) {
            int track = image[db * BLOCKSIZE + offset + FILETRACKOFFSET];
            int sector = image[db * BLOCKSIZE + offset + FILESECTOROFFSET];
            unsigned int last_track;
            int last_sector;
            int error = validate_sector_chain(type, image, atab, track, sector, &last_track, &last_sector);
            if(error != VALID) {
                printf("WARNING: file ");
                print_filename(stdout, &image[db * BLOCKSIZE + offset + FILENAMEOFFSET]);
                printf(" seems corrupt (%s)\n", error_name[error]);
            }
            mark_sector_chain(type, image, atab, track, sector, last_track, last_sector, ALLOCATED);
        }
    } while(next_dir_entry(type, image, &dt, &ds, &offset, blockmap));
    free(blockmap);
}

/* Write atab into BAM */
static void
write_atab(image_type type, unsigned char* image, char* atab)
{
    for(unsigned int t = 1; t <= image_num_tracks(type); t++) {
        for(int s = 0; s < num_sectors(type, t); s++) {
            int b = linear_sector(type, t, s);
            int free = (atab[b] == UNALLOCATED);
            if(!free) {
                /* keep allocated BAM entries, only add new ones */
                mark_sector(type, image, t, s, 0);
            }
        }
    }
}

/* Try to undelete a file given the directory entry, returns true if successful */
static bool
undelete_file(image_type type, unsigned char* image, int dt, int ds, int offset, char* atab, int level)
{
    unsigned int last_track;
    int last_sector;
    char marker = 0;
    int dirblock = linear_sector(type, dt, ds) * BLOCKSIZE;
    int track = image[dirblock + offset + FILETRACKOFFSET];
    int sector = image[dirblock + offset + FILESECTOROFFSET];
    int error = validate_sector_chain(type, image, atab, track, sector, &last_track, &last_sector);
    if(error == VALID || ((level == RESTORE_DIR_ONLY || level >= RESTORE_INVALID_FILES) && error != FIRST_BROKEN)) {
        unsigned char name[17];
        if(error != VALID) {
            if(level != RESTORE_DIR_ONLY) {
                /* terminate chain */
                int block_offset = linear_sector(type, last_track, last_sector) * BLOCKSIZE;
                image[block_offset + TRACKLINKOFFSET] = 0;
                marker = '<';
            }
        }
        /* restore dir entry */
        memcpy(&name, &image[dirblock + offset + FILENAMEOFFSET], 16);
        name[16] = 0;
        int b = linear_sector(type, track, sector);
        int address = image[b * BLOCKSIZE + 2] + 256 * image[b * BLOCKSIZE + 3];
        generate_unique_filename(type, image, name, track, sector, address, marker);
        memcpy(&image[dirblock + offset + FILENAMEOFFSET], &name, 16);
        image[dirblock + offset + FILETYPEOFFSET] = 0x82; /* original file type is lost, use closed PRG instead */
        mark_sector_chain(type, image, atab, track, sector, last_track, last_sector, ALLOCATED);
        if(level != RESTORE_DIR_ONLY) {
            int size = count_blocks(type, image, track, sector);
            image[dirblock + offset + FILEBLOCKSHIOFFSET] = size / 256;
            image[dirblock + offset + FILEBLOCKSLOOFFSET] = size % 256;
        }
        return true;
    }
    if(error != FIRST_BROKEN) {
        mark_sector_chain(type, image, atab, track, sector, last_track, last_sector, UNALLOCATED);
    }
    return false;
}

/* search for scratched directory entries and restore them */
static int
undelete(image_type type, unsigned char* image, char* atab, int level)
{
    int dt = dirtrack(type);
    int ds = (type == IMAGE_D81) ? 3 : 1;
    int nsectors = num_sectors(type, dt);
    int offset = 0;
    int num_undeleted = 0;
    int final_dt, final_ds; /* last linked directory sector and track */
    char *blockmap = calloc(image_num_blocks(type), sizeof(char));
    if(blockmap == NULL) {
        fprintf(stderr, "ERROR: Memory allocation error\n");
        exit(-1);
    }

    bool* searched = calloc(nsectors, sizeof(bool));
    if(searched == NULL) {
        fprintf(stderr, "ERROR: error allocating memory");
        exit(-1);
    }

    /* go through directory sector chain */
    do {
        if(dt == dirtrack(type)) {
            searched[ds] = true;
        }
        final_dt = dt;
        final_ds = ds;
        int db = linear_sector(type, dt, ds);
        mark_sector(type, image, dt, ds, 0); /* needs to be updated in BAM, so that dir allocation works correctly later */
        atab[db] = ALLOCATED;
        int dirblock = db * BLOCKSIZE;
        int filetype = image[dirblock + offset + FILETYPEOFFSET];
        if(filetype == FILETYPEDEL && image[dirblock + offset + FILENAMEOFFSET] != 0 && undelete_file(type, image, dt, ds, offset, atab, level)) { /* filename starting with 0 means that likely there was no file */
            num_undeleted++;
        }
    } while(next_dir_entry(type, image, &dt, &ds, &offset, blockmap));
    free(blockmap);

    /* search for unlinked dir sectors */
    dt = dirtrack(type);
    for(ds = (type == IMAGE_D81) ? 3 : 1; ds < nsectors; ds++) {
        if(!searched[ds]) {
            int found = 1;
            for(offset = 0; offset < DIRENTRIESPERBLOCK*DIRENTRYSIZE; offset += DIRENTRYSIZE) {
                if(!undelete_file(type, image, dt, ds, offset, atab, level)) {
                    found = 0;
                    break;
                }
            }
            if(found) {
                num_undeleted += 8;
                if(!quiet) {
                    printf("Relinking directory sector %d\n", ds);
                }
                /* link found dir sector */
                int block_offset = linear_sector(type, final_dt, final_ds) * BLOCKSIZE;
                image[block_offset + TRACKLINKOFFSET] = dt;
                image[block_offset + SECTORLINKOFFSET] = ds;
                /* terminate found dir sector */
                int db = linear_sector(type, dt, ds);
                block_offset = db * BLOCKSIZE;
                image[block_offset + TRACKLINKOFFSET] = 0;
                final_dt = dt;
                final_ds = ds;
                mark_sector(type, image, dt, ds, 0); /* needs to be updated in BAM, so that dir allocation works correctly later */
                atab[db] = ALLOCATED;
            }
        }
    }
    free(searched);
    write_atab(type, image, atab);
    return num_undeleted;
}

/* add new DIR entries for wild chains */
static void
add_wild_to_dir(image_type type, unsigned char* image, char* atab, imagefile files[])
{
    /* create a DIR entry for each FILESTART */
    for(unsigned int t = 1; t <= image_num_tracks(type); t++) {
        for(int s = 0; s < num_sectors(type, t); s++) {
            int b = linear_sector(type, t, s);
            if(atab[b] == FILESTART || atab[b] == FILESTART_TRUNCATED) {
                char marker = atab[b] == FILESTART ? 0 : '<';
                unsigned char name[17];
                name[0] = 0xa0;
                int dir_index, dir_sector, dir_offset;
                atab[b] = ALLOCATED;
                new_dir_slot(type, image, (type == IMAGE_D81 ? 1 : 3), 0, &dir_index, &dir_sector, &dir_offset, files); /* TODO: handle full directory more gracefully */
                int db = linear_sector(type, dirtrack(type), dir_sector);
                atab[db] = ALLOCATED; /* make sure that potentially new dir block is marked as used */
                int offset = db * BLOCKSIZE + dir_offset;
                image[offset + FILETYPEOFFSET] = 0x82; /* closed PRG */
                image[offset + FILETRACKOFFSET] = t;
                image[offset + FILESECTOROFFSET] = s;
                image[offset + FILENAMEOFFSET] = 0xa0; /* no proposed filename */
                int address = image[b * BLOCKSIZE + 2] + 256 * image[b * BLOCKSIZE + 3];
                generate_unique_filename(type, image, name, t, s, address, marker);
                memcpy(&image[offset + FILENAMEOFFSET], name, 16);
                int size = count_blocks(type, image, t, s);
                image[offset + FILEBLOCKSHIOFFSET] = size / 256;
                image[offset + FILEBLOCKSLOOFFSET] = size % 256;
            }
        }
    }
}

/* search for wild valid chains of unallocated sectors */
static int
undelete_wild(image_type type, unsigned char* image, char* atab, int level, imagefile files[])
{
    int num_undeleted = 0;
    int max_bam_sector = (type == IMAGE_D81) ? 2 : 0;
    unsigned int dt = dirtrack(type);
    /* search for well terminated sector chains */
    for(unsigned int t = 1; t <= image_num_tracks(type); t++) {
        for(int s = 0; s < num_sectors(type, t); s++) {
            int b = linear_sector(type, t, s);
            if(atab[b] == UNALLOCATED && (t != dt || s > max_bam_sector)) { /* ignore bam sectors */
                unsigned int last_track;
                int last_sector;
                int error = validate_sector_chain(type, image, atab, t, s, &last_track, &last_sector);
                int last_block = linear_sector(type, last_track, last_sector);
                if(error == CHAINED || error == CHAINED_TRUNCATED) {
                    mark_sector_chain(type, image, atab, t, s, last_track, last_sector, ALLOCATED);
                    atab[b] = (error == CHAINED) ? FILESTART : CHAINED_TRUNCATED;
                    int chained_track = image[last_block * BLOCKSIZE + TRACKLINKOFFSET];
                    int chained_sector = image[last_block * BLOCKSIZE + SECTORLINKOFFSET];
                    int chained_block = linear_sector(type, chained_track, chained_sector);
                    atab[chained_block] = ALLOCATED; /* overwrite FILESTART */
                } else if(error == VALID) {
                    if(last_track == t && last_sector == s) {
                        if(level == RESTORE_INVALID_SINGLES) {
                            /* single block files should have all 0 after file end */
                            bool valid = false;
                            int last_byte = image[last_block * BLOCKSIZE + SECTORLINKOFFSET];
                            if(image[last_block * BLOCKSIZE + TRACKLINKOFFSET] == 0 && last_byte >= 2) {
                                valid = true;
                                for(int i = last_byte + 1; i <= 255; i++) {
                                    if(image[last_block * BLOCKSIZE + i] != 0) {
                                        valid = false;
                                        break;
                                    }
                                }
                            }
                            if(valid) {
                                atab[b] = FILESTART;
                                num_undeleted++;
                            } else {
                                atab[b] = UNALLOCATED;
                            }
                            atab[b] = valid ? FILESTART : UNALLOCATED;
                        } else {
                            atab[b] = UNALLOCATED;
                        }
                    } else {
                        mark_sector_chain(type, image, atab, t, s, last_track, last_sector, ALLOCATED);
                        atab[b] = FILESTART;
                        image[last_block * BLOCKSIZE + TRACKLINKOFFSET] = 0;
                        num_undeleted++;
                    }
                } else {
                    if(error != FIRST_BROKEN) {
                        mark_sector_chain(type, image, atab, t, s, last_track, last_sector, UNALLOCATED);
                    }
                }
            }
        }
    }
    if(num_undeleted) {
        write_atab(type, image, atab);
        add_wild_to_dir(type, image, atab, files);
    }
    return num_undeleted;
}

/* search for wild invalid chains of unallocated sectors and fix them */
static int
undelete_fix_wild(image_type type, unsigned char* image, char* atab, imagefile files[])
{
    int num_undeleted = 0;
    int max_bam_sector = (type == IMAGE_D81) ? 2 : 0;
    unsigned int dt = dirtrack(type);
    /* search for broken sector chains */
    for(unsigned int t = 1; t <= image_num_tracks(type); t++) {
        for(int s = 0; s < num_sectors(type, t); s++) {
            int b = linear_sector(type, t, s);
            if(atab[b] == UNALLOCATED && (t != dt || s > max_bam_sector)) { /* ignore bam sectors */
                unsigned int last_track;
                int last_sector;
                int error = validate_sector_chain(type, image, atab, t, s, &last_track, &last_sector);
                int last_block = linear_sector(type, last_track, last_sector);
                if(error == CHAINED || error == CHAINED_TRUNCATED) {
                    mark_sector_chain(type, image, atab, t, s, last_track, last_sector, ALLOCATED);
                    atab[b] = (error == CHAINED) ? FILESTART : FILESTART_TRUNCATED;
                    int chained_track = image[last_block * BLOCKSIZE + TRACKLINKOFFSET];
                    int chained_sector = image[last_block * BLOCKSIZE + SECTORLINKOFFSET];
                    int chained_block = linear_sector(type, chained_track, chained_sector);
                    atab[chained_block] = ALLOCATED; /* overwrite FILESTART */
                } else if(t != last_track || s != last_sector) {
                    mark_sector_chain(type, image, atab, t, s, last_track, last_sector, ALLOCATED);
                    atab[b] = FILESTART_TRUNCATED;
                    image[last_block * BLOCKSIZE + TRACKLINKOFFSET] = 0;
                    image[last_block * BLOCKSIZE + SECTORLINKOFFSET] = 255;
                    num_undeleted++;
                } else {
                    if(error != FIRST_BROKEN) {
                        mark_sector_chain(type, image, atab, t, s, last_track, last_sector, UNALLOCATED);
                    }
                }
            }
        }
    }
    if(num_undeleted) {
        write_atab(type, image, atab);
        add_wild_to_dir(type, image, atab, files);
    }
    return num_undeleted;
}

/* Tries to restore any deleted or formatted files */
static void
restore(image_type type, unsigned char* image, int level, imagefile files[])
{
    int num_undeleted = 0;
    /* create block allocation table */
    char *atab = (char *)calloc(image_num_blocks(type), sizeof(char));
    if (atab == NULL) {
        fprintf(stderr, "ERROR: error allocating memory");
        exit(-1);
    }
    init_atab(type, image, atab);
    if(level == RESTORE_DIR_ONLY) {
        num_undeleted += undelete(type, image, atab, RESTORE_DIR_ONLY);
    } else {
        num_undeleted += undelete(type, image, atab, RESTORE_VALID_FILES);
        if(level >= RESTORE_VALID_CHAINS) {
            num_undeleted += undelete_wild(type, image, atab, RESTORE_VALID_CHAINS, files);
        }
        if(level >= RESTORE_INVALID_FILES) {
            num_undeleted += undelete(type, image, atab, RESTORE_INVALID_FILES);
        }
        if(level >= RESTORE_INVALID_CHAINS) {
            num_undeleted += undelete_fix_wild(type, image, atab, files);
        }
        if(level >= RESTORE_INVALID_SINGLES) {
            num_undeleted += undelete_wild(type, image, atab, RESTORE_INVALID_SINGLES, files);
        }
    }
    free(atab);
    if(num_undeleted) {
        modified = 1;
    }
    if(!quiet) {
        printf("%d files undeleted", num_undeleted);
        if(num_undeleted) {
            printf(", '<' at filename end marks truncated files");
        }
        printf("\n");
    }
}

/* Prints a command line to create dir art like the given image */
static void
convert_to_commandline(image_type type, unsigned char* image)
{
    char *blockmap = calloc(image_num_blocks(type), sizeof(char));
    if(blockmap == NULL) {
        fprintf(stderr, "ERROR: Memory allocation error\n");
        exit(-1);
    }
    printf("\nCommandline to create directory art: -n \"");
    unsigned int bam = linear_sector(type, dirtrack(type), 0) * BLOCKSIZE;
    print_filename_with_escapes(image + bam + get_header_offset(type), FILENAMEMAXSIZE);
    printf("\" -i \"");
    print_filename_with_escapes(image + bam + get_id_offset(type), 5);
    printf("\" ");

    int ds = (type == IMAGE_D81) ? 3 : 1;
    int dt = dirtrack(type);
    int offset = 0;
    do {
        int dirblock = linear_sector(type, dt, ds) * BLOCKSIZE + offset;
        int filetype = image[dirblock + FILETYPEOFFSET];
        if (filetype) {
            if(filetype != 0x82) {
                printf("-T %d ", filetype);
            }
            int size = image[dirblock + FILEBLOCKSLOOFFSET] + 256 * image[dirblock + FILEBLOCKSHIOFFSET];
            if(size != 0) {
                printf("-B %d ", size);
            }
            unsigned char *filename = (unsigned char *) image + dirblock + FILENAMEOFFSET;
            printf("-N -f \"");
            print_filename_with_escapes(filename, FILENAMEMAXSIZE);
            printf("\" -L ");
        }
    } while (next_dir_entry(type, image, &dt, &ds, &offset, blockmap));
    free(blockmap);
    printf("\n\n");
}

/* Performs strict CBM DOS validation on the image */
static void
validate(image_type type, unsigned char* image)
{
    /* create block allocation table */
    char *atab = (char *)calloc(image_num_blocks(type), sizeof(int));
    if (atab == NULL) {
        fprintf(stderr, "ERROR: error allocating memory");
        exit(-1);
    }
    /* check format specifier */
    int format = image[linear_sector(type, dirtrack(type), 0) * BLOCKSIZE + 2];
    if ((type == IMAGE_D64 || type == IMAGE_D71) && format != 0x41) {
        fprintf(stderr, "ERROR: validation failed, format specifier in directory (0x%02x) does not specify 1541/1570/1571 (0x41)\n", format);
        exit(-1);
    } else if(type == IMAGE_D81 && format != 0x44) {
        fprintf(stderr, "ERROR: validation failed, format specifier in directory (0x%02x) does not specify 1581 (0x44)\n", format);
        exit(-1);
    }
    /* check double sided flag */
    int doublesided = image[linear_sector(type, dirtrack(type), 0) * BLOCKSIZE + 3];
    if (type == IMAGE_D64 && doublesided != 0) {
        fprintf(stderr, "ERROR: validation failed, double sided flag in directory not zero (0x%02x) for D64\n", doublesided);
        exit(-1);
    }
    if (type == IMAGE_D71 && doublesided != 0x80) {
        fprintf(stderr, "ERROR: validation failed, double sided flag in directory not 0x80 (0x%02x) for D71\n", doublesided);
        exit(-1);
    }
    /* check each directory entry and set block allocation table */
    /* set BAM sector as allocated */
    atab[linear_sector(type, dirtrack(type), 0)] = ALLOCATED;
    if (type == IMAGE_D71) {
        /* mark full track of second BAM as allocated */
        for (int s = 0; s < num_sectors(type, dirtrack(type) + D64NUMTRACKS); s++) {
            atab[linear_sector(type, dirtrack(type) + D64NUMTRACKS, s)] = ALLOCATED;
        }
    }
    if(type == IMAGE_D81) {
        /* mark additional BAM sectors as allocated */
        atab[linear_sector(type, dirtrack(type), 1)] = ALLOCATED;
        atab[linear_sector(type, dirtrack(type), 2)] = ALLOCATED;
    }
    unsigned int dt = dirtrack(type);
    int dirsector = (type == IMAGE_D81) ? 3 : 1;
    unsigned int start_track = 1;
    while (start_track != 0) {
        atab[linear_sector(type, dt, dirsector)] = ALLOCATED;
        int dirblock = linear_sector(type, dt, dirsector) * BLOCKSIZE;
        for (int direntry = 0; direntry < DIRENTRIESPERBLOCK; direntry++) {
            int entryOffset = direntry * DIRENTRYSIZE;
            int filetype = image[dirblock + entryOffset + FILETYPEOFFSET] & 0xf;
            if (filetype > 4) {
                fprintf(stderr, "ERROR: validation failed, illegal file type (0x%02x) in directory\n", filetype);
                exit(-1);
            }
            if (filetype != 0) { /* skip deleted entries */
                start_track = image[dirblock + entryOffset + FILETRACKOFFSET];
                int start_sector = image[dirblock + entryOffset + FILESECTOROFFSET];
                if (start_track == 0 || start_track > image_num_tracks(type)) {
                    fprintf(stderr, "ERROR: validation failed, illegal track reference (%u) in directory\n", start_track);
                    exit(-1);
                }
                if (start_sector >= num_sectors(type, start_track)) {
                    fprintf(stderr, "ERROR: validation failed, illegal sector reference (track %u, sector %d) in directory\n", start_track, start_sector);
                    exit(-1);
                }
                if (atab[linear_sector(type, start_track, start_sector)] == ALLOCATED) {
                    fprintf(stderr, "ERROR: validation failed, file starts in the middle of another file (track %u, sector %d)\n", start_track, start_sector);
                    exit(-1);
                }
                if (atab[linear_sector(type, start_track, start_sector)] != FILESTART) { /* loop files are allowed */
                    unsigned int error_track;
                    int error_sector;
                    int result = validate_sector_chain(type, image, atab, start_track, start_sector, &error_track, &error_sector);
                    switch(result) {
                    case ILLEGAL_TRACK:
                        fprintf(stderr, "ERROR: validation failed, illegal track reference in file sector chain at track %d, sector %d\n", error_track, error_sector);
                        exit(-1);
                    case ILLEGAL_SECTOR:
                        fprintf(stderr, "ERROR: validation failed, illegal sector reference in file sector chain at track %d, sector %d\n", error_track, error_sector);
                        exit(-1);
                    case LOOP:
                        fprintf(stderr, "ERROR: validation failed, loop in file sector chain at track %d, sector %d\n", error_track, error_sector);
                        exit(-1);
                    case COLLISION:
                    case CHAINED:
                        fprintf(stderr, "ERROR: validation failed, collision with existing file in file sector chain at track %d, sector %d\n", error_track, error_sector);
                        exit(-1);
                    }
                    atab[linear_sector(type, start_track, start_sector)] = FILESTART;
                    mark_sector_chain(type, image, atab, start_track, start_sector, error_track, error_sector, ALLOCATED);
                }
            }
        }
        dt = image[dirblock + TRACKLINKOFFSET];
        dirsector = image[dirblock + SECTORLINKOFFSET];
        if (dt == 0) {
            break;
        }
    }
    /* check BAM for consistency with block allocation table */
    for (unsigned int t = 1; t <= image_num_tracks(type); t++) {
        unsigned char* bitmap = image + get_bam_offset(type, t);
        int num_free = 0;
        int exp_free;
        for (int s = 0; s < num_sectors(type, t); s++) {
            int atab_used = (atab[linear_sector(type, t, s)] != UNALLOCATED);
            int bam_used = ((bitmap[s >> 3] & (1 << (s & 7))) == 0);
            num_free += (1 - bam_used);
            if (bam_used != atab_used) {
                fprintf(stderr, "ERROR: validation failed, BAM (%s) is not consistent with files (%s) for track %u sector %d\n", bam_used ? "used" : "free", atab_used ? "used" : "free", t, s);
                exit(-1);
            }
        }
        if (type != IMAGE_D71 || t <= 35) {
            exp_free = *(bitmap - 1);
        } else {
            exp_free = image[linear_sector(type, dirtrack(type), 0) * BLOCKSIZE + 0xdd + t - D64NUMTRACKS - 1];
        }
        if (exp_free != num_free) {
            fprintf(stderr, "ERROR: validation failed, BAM number of free blocks (%d) is not consistent with bitmap (%#02x%#02x%#02x) for track %u\n", exp_free, *bitmap, *(bitmap + 1), *(bitmap + 2), t);
            exit(-1);
        }
    }
    free(atab);
    if(!quiet) {
        fprintf(stderr, "CBM DOS validation passed\n");
    }
}

/* Apply list of binary patches to image */
static void
apply_patches(image_type type, unsigned char* image, patch patches[], int num_patches)
{
    for(int p = 0; p < num_patches; p++) {
        unsigned int t = patches[p].track;
        int s = patches[p].sector;
        int o = patches[p].offset;
        const char *n = patches[p].filename;
        if(t < 1 || t >= image_num_tracks(type)) {
            fprintf(stderr, "ERROR: Illegal track for -p\n");
            exit(-1);
        }
        if(s < 0 || s >= num_sectors(type, t)) {
            fprintf(stderr, "ERROR: Illegal sector for -p\n");
            exit(-1);
        }
        if(o < 0 || o > 255) {
            fprintf(stderr, "ERROR: Illegal offset for -p\n");
            exit(-1);
        }
        int pos = linear_sector(type, t, s) * BLOCKSIZE + o;
        int maxread = image_size(type) - pos;
        FILE* f = fopen(n, "rb");
        if (f == NULL) {
            fprintf(stderr, "ERROR: Could not open patch file \"%s\" for reading\n", n);
            exit(-1);
        }
        int numread = fread(image + pos, 1, maxread, f);
        if(!feof(f)) {
            fprintf(stderr, "ERROR: Patch file \"%s\" overflows disk\n", n);
            exit(-1);
        }
        fclose(f);
        while(numread + o > 0)  {
            mark_sector(type, image, t, s++, 0);
            if(s >= num_sectors(type, t)) {
                s = 0;
                t++;
            }
            numread -= BLOCKSIZE;
        }
    }
}

/* Extract files according to patterns */
static void
extract_files(image_type type, unsigned char* image, char *patterns[], int num_patterns)
{
    bool found = false;
    char *blockmap = calloc(image_num_blocks(type), sizeof(char));
    if(blockmap == NULL) {
        fprintf(stderr, "ERROR: Memory allocation error\n");
        exit(-1);
    }

    int ds = (type == IMAGE_D81) ? 3 : 1;
    int dt = dirtrack(type);
    int offset = 0;
    do {
        int dirblock = linear_sector(type, dt, ds) * BLOCKSIZE + offset;
        int filetype = image[dirblock + FILETYPEOFFSET];

        if (filetype != FILETYPEDEL) {
            unsigned char *filename = (unsigned char *) image + dirblock + FILENAMEOFFSET;
            for(int p = 0; p < num_patterns; p++) {
                unsigned char petscii[FILENAMEMAXSIZE];
                evalhexescape((unsigned char*)patterns[p], petscii, FILENAMEMAXSIZE, FILENAMEEMPTYCHAR);
                if(match_filename(petscii, filename)) {
                    char afilename[3 * FILENAMEMAXSIZE + 4 + 1]; /* space for hexevals, extension and terminating 0 */
                    found = true;
                    petscii2ascii(filename, afilename, FILENAMEMAXSIZE, false);
                    int pext = strlen(afilename);
                    filetype &= 7;
                    if(filetype <= 5) {
                        afilename[pext] = '.';
                        strcpy(afilename + pext + 1, filetypename_lc[filetype]);
                    }
                    unsigned int track = *(image + dirblock + FILETRACKOFFSET);
                    int sector = *(image + dirblock + FILESECTOROFFSET);
                    unsigned int last_track;
                    int last_sector;
                    int result = validate_sector_chain(type, image, blockmap, track, sector, &last_track, &last_sector);
                    printf("\nExtracting %s", afilename);
                    if(result == FIRST_BROKEN) {
                        printf(", ERROR: directory entry invalid");
                    } else {
                        if(result != VALID) {
                            printf(", WARNING: file corrupt");
                        }
                        FILE *file = fopen(afilename, "wb");
                        if(file == NULL) {
                            fprintf(stderr, "\nERROR: Could not open output file %s\n", afilename);
                            exit(-1);
                        }
                        while(track != last_track || sector != last_sector) {
                            unsigned char *block = image + linear_sector(type, track, sector) * BLOCKSIZE;
                            fwrite(block + 2, 1, 254, file);
                            track = *block;
                            sector = *(block + 1);
                        }
                        unsigned char *block = image + linear_sector(type, track, sector) * BLOCKSIZE;
                        fwrite(block + 2, 1, *(block + 1) - 1, file);
                        fclose(file);
                    }
                    break;
                }
            }
        }
    } while (next_dir_entry(type, image, &dt, &ds, &offset, blockmap));
    if(found) {
        printf("\n");
    }
    free(blockmap);
}

int
main(int argc, char* argv[])
{
    imagefile files[MAXNUMFILES_D81];
    memset(files, 0, sizeof files);
    patch patches[MAXNUMPATCHES];
    memset(patches, 0, sizeof patches);
    int num_patches = 0;
    char *extractions[MAXNUMEXTRACTIONS];
    memset(extractions, 0, sizeof extractions);
    int num_extractions = 0;
    image_type type = IMAGE_D64;
    char* imagepath = NULL;
    char* filename_g64 = NULL;
    unsigned char* header  = (unsigned char*)"cc1541";
    unsigned char* id      = (unsigned char*)"00#a02a";
    unsigned char* bam_message = NULL;
    int dirtracksplit = 1;
    int usedirtrack = 0;
    unsigned int shadowdirtrack = 0;

    int default_first_sector_new_track = 0;
    int first_sector_new_track = 0;
    int defaultSectorInterleave = 10;
    int sectorInterleave = 0;
    int dir_sector_interleave = 3;
    int numdirblocks = 2;
    int nrSectorsShown = -1;
    unsigned char* filename = NULL;
    int set_header = 0;
    int nooverwrite = 0;
    int dovalidate = 0;
    int restore_level = -1;
    int ignore_collision = 0;
    int filetype = 0x82; /* default is closed PRG */
    bool filetype_set = false;
    bool print_art_commandline = false;
    int allocation_strategy = 1;

    /* flags to detect illegal settings */
    int transwarp_set = 0;
    int sector_interleave_set = 0;
    int default_sector_interleave_set = 0;
    int file_start_sector_set = 0;
    int new_track_start_sector_set = 0;
    int cluster_optimized = 0;

    int retval = 0;

    int i, j;

    if (argc == 1 || strcmp(argv[argc-1], "-h") == 0) {
        usage();
    }

    /* read environment variable for unicode printout */
    char *unicode_env = getenv("CC1541UNICODE");
    if(unicode_env != NULL) {
        unicode = atoi(unicode_env);
        if(unicode < 0 || unicode > 2) {
            unicode = 0;
        }
    }

    for (j = 1; j < argc - 1; j++) {
        if (strcmp(argv[j], "-n") == 0) {
            if (argc < j + 2) {
                fprintf(stderr, "ERROR: Error parsing argument for -n\n");
                return -1;
            }
            header = (unsigned char*)argv[++j];
            set_header = 1;
            modified = 1;
        } else if (strcmp(argv[j], "-i") == 0) {
            if (argc < j + 2) {
                fprintf(stderr, "ERROR: Error parsing argument for -i\n");
                return -1;
            }
            id = (unsigned char*)argv[++j];
            set_header = 1;
            modified = 1;
        } else if (strcmp(argv[j], "-H") == 0) {
            if (argc < j + 2) {
                fprintf(stderr, "ERROR: Error parsing argument for -H\n");
                return -1;
            }
            bam_message = (unsigned char*)argv[++j];
            set_header = 1;
            modified = 1;
        } else if (strcmp(argv[j], "-M") == 0) {
            if ((argc < j + 2) || !sscanf(argv[++j], "%d", &max_hash_length)) {
                fprintf(stderr, "ERROR: Error parsing argument for -M\n");
                return -1;
            }
            if ((max_hash_length < 1) || (max_hash_length > FILENAMEMAXSIZE)) {
                fprintf(stderr, "ERROR: Hash computation maximum filename length %d specified\n", max_hash_length);
                return -1;
            }
        } else if (strcmp(argv[j], "-m") == 0) {
            ignore_collision = 1;
        } else if (strcmp(argv[j], "-F") == 0) {
            if ((argc < j + 2) || !sscanf(argv[++j], "%d", &first_sector_new_track)) {
                fprintf(stderr, "ERROR: Error parsing argument for -F\n");
                return -1;
            }
            new_track_start_sector_set = 1;
        } else if (strcmp(argv[j], "-S") == 0) {
            if ((argc < j + 2) || !sscanf(argv[++j], "%d", &defaultSectorInterleave)) {
                fprintf(stderr, "ERROR: Error parsing argument for -S\n");
                return -1;
            }
            if(defaultSectorInterleave < 1 || defaultSectorInterleave > 21) {
                fprintf(stderr, "ERROR: Illegal value for -S\n");
                return -1;
            }
            default_sector_interleave_set = 1;
        } else if (strcmp(argv[j], "-s") == 0) {
            if ((argc < j + 2) || !sscanf(argv[++j], "%d", &sectorInterleave)) {
                fprintf(stderr, "ERROR: Error parsing argument for -s\n");
                return -1;
            }
            if(sectorInterleave < 1 || sectorInterleave > 21) {
                fprintf(stderr, "ERROR: Illegal value for -s\n");
                return -1;
            }
            sector_interleave_set = 1;
        } else if (strcmp(argv[j], "-f") == 0) {
            if (argc < j + 2) {
                fprintf(stderr, "ERROR: Error parsing argument for -f\n");
                return -1;
            }
            filename = (unsigned char*)argv[++j];
        } else if (strcmp(argv[j], "-A") == 0) {
            if ((argc < j + 2) || !sscanf(argv[++j], "%d", &allocation_strategy)) {
                fprintf(stderr, "ERROR: Error parsing argument for -A\n");
                return -1;
            }
            if(allocation_strategy < 0 || allocation_strategy > 1) {
                fprintf(stderr, "ERROR: Illegal value for -A\n");
                return -1;
            }
        } else if (strcmp(argv[j], "-e") == 0) {
            files[num_files].mode |= MODE_SAVETOEMPTYTRACKS;
        } else if (strcmp(argv[j], "-E") == 0) {
            files[num_files].mode |= MODE_FITONSINGLETRACK;
        } else if (strcmp(argv[j], "-r") == 0) {
            if ((argc < j + 2) || !sscanf(argv[++j], "%d", &i)) {
                fprintf(stderr, "ERROR: Error parsing argument for -r\n");
                return -1;
            }
            if ((i < 1) || (((i << MODE_MIN_TRACK_SHIFT) & MODE_MIN_TRACK_MASK) != (i << MODE_MIN_TRACK_SHIFT))) {
                fprintf(stderr, "ERROR: Invalid minimum track %d specified\n",  i);
                return -1;
            }
            files[num_files].mode = (files[num_files].mode & ~MODE_MIN_TRACK_MASK) | (i << MODE_MIN_TRACK_SHIFT);
        } else if (strcmp(argv[j], "-b") == 0) {
            if ((argc < j + 2) || !sscanf(argv[++j], "%d", &i)) {
                fprintf(stderr, "ERROR: Error parsing argument for -b\n");
                return -1;
            }
            if ((i < 0) || (i >= num_sectors(IMAGE_D64, 1))) { /* same for all applicable images, so simply use D64 */
                fprintf(stderr, "ERROR: Invalid beginning sector %d specified\n", i);
                return -1;
            }
            files[num_files].mode = (files[num_files].mode & ~MODE_BEGINNING_SECTOR_MASK) | (i + 1);
            file_start_sector_set = 1;
        } else if (strcmp(argv[j], "-c") == 0) {
            files[num_files].mode |= MODE_SAVECLUSTEROPTIMIZED;
            cluster_optimized = 1;
        } else if (strcmp(argv[j], "-o") == 0) {
            nooverwrite = 1;
        } else if (strcmp(argv[j], "-V") == 0) {
            dovalidate = 1;
        } else if (strcmp(argv[j], "-T") == 0) {
            if (argc < j + 2) {
                fprintf(stderr, "ERROR: Error parsing argument for -T\n");
                return -1;
            }
            if (strcmp(argv[j + 1], "DEL") == 0) {
                filetype = (filetype & 0xf0) | FILETYPEDEL;
            } else if (strcmp(argv[j + 1], "SEQ") == 0) {
                filetype = (filetype & 0xf0) | FILETYPESEQ;
            } else if (strcmp(argv[j + 1], "PRG") == 0) {
                filetype = (filetype & 0xf0) | FILETYPEPRG;
            } else if (strcmp(argv[j + 1], "USR") == 0) {
                filetype = (filetype & 0xf0) | FILETYPEUSR;
            } else if (strcmp(argv[j + 1], "REL") == 0) {
                filetype = (filetype & 0xf0) | FILETYPEREL;
            } else {
                char* dummy;
                filetype = strtol(argv[j + 1], &dummy, 10);
                if(dummy == argv[j + 1] || *dummy != 0 || filetype < 0 || filetype > 255) {
                    fprintf(stderr, "ERROR: Error parsing argument for -T\n");
                    return -1;
                }
            }
            filetype_set = true;
            j++;
        } else if (strcmp(argv[j], "-O") == 0) {
            filetype &= 0x7f;
        } else if (strcmp(argv[j], "-P") == 0) {
            filetype |= 0x40;
        } else if (strcmp(argv[j], "-N") == 0) {
            files[num_files].force_new = 1;
            ignore_collision = 1;
        } else if (strcmp(argv[j], "-K") == 0) {
            if (argc < j + 2) {
                fprintf(stderr, "ERROR: Error parsing argument for -K\n");
                return -1;
            }
            evalhexescape((unsigned char *) argv[++j], files[num_files].key, TRANSWARPKEYSIZE, 0);
            files[num_files].have_key = true;
        } else if ((strcmp(argv[j], "-w") == 0)
                   || (strcmp(argv[j], "-W") == 0)) {
            if (argc < j + 2) {
                fprintf(stderr, "ERROR: Error parsing argument for %s\n", argv[j]);
                return -1;
            }
            files[num_files].alocalname = (unsigned char*)argv[j + 1];
            if (filename == NULL) {
                ascii2petscii(basename(files[num_files].alocalname), files[num_files].pfilename, FILENAMEMAXSIZE); /* do not eval escapes when converting the filename, as the local filename could contain the escape char */
            } else {
                evalhexescape(filename, files[num_files].pfilename, FILENAMEMAXSIZE, FILENAMEEMPTYCHAR);
            }
            files[num_files].sectorInterleave = sectorInterleave ? sectorInterleave : defaultSectorInterleave;
            files[num_files].first_sector_new_track = first_sector_new_track;
            files[num_files].nrSectorsShown = nrSectorsShown;
            files[num_files].filetype = filetype;
            files[num_files].direntryindex = -1;

            if (strcmp(argv[j], "-W") == 0) {
                if(nrSectorsShown != -1) {
                    fprintf(stderr, "ERROR: -B cannot be used for Transwarp files\n");
                    return -1;
                }
                if(file_start_sector_set) {
                    fprintf(stderr, "ERROR: -b cannot be used for Transwarp files\n");
                    return -1;
                }
                if(sector_interleave_set) {
                    fprintf(stderr, "ERROR: -s cannot be used for Transwarp files\n");
                    return -1;
                }
                transwarp_set = true;
                if(!filetype_set && files[num_files].have_key) {
                    files[num_files].filetype = (filetype & 0xf0) | FILETYPEUSR | FILETYPETRANSWARPMASK;
                } else {
                    files[num_files].filetype = filetype | FILETYPETRANSWARPMASK;
                }
                files[num_files].sectorInterleave = 1;
            }

            first_sector_new_track = default_first_sector_new_track;
            filename = NULL;
            sectorInterleave = 0;
            nrSectorsShown = -1;
            filetype = 0x82;
            filetype_set = false;
            num_files++;
            modified = 1;
            j++;
        } else if (strcmp(argv[j], "-l") == 0) {
            if (argc < j + 2) {
                fprintf(stderr, "ERROR: Error parsing argument for -l\n");
                return -1;
            }
            files[num_files].alocalname = (unsigned char*)argv[j + 1];
            evalhexescape(files[num_files].alocalname, files[num_files].plocalname, FILENAMEMAXSIZE, FILENAMEEMPTYCHAR);
            if (filename == NULL) {
                fprintf(stderr, "ERROR: Loop files require a filename set with -f\n");
                return -1;
            }
            evalhexescape(filename, files[num_files].pfilename, FILENAMEMAXSIZE, FILENAMEEMPTYCHAR);
            if(memcmp(files[num_files].pfilename, files[num_files].plocalname, FILENAMEMAXSIZE) == 0 && !files[num_files].force_new) {
                fprintf(stderr, "ERROR: Loop file cannot have the same name as the file they refer to, unless with -N\n");
                return -1;
            }
            files[num_files].mode |= MODE_LOOPFILE;
            files[num_files].sectorInterleave = 0;
            files[num_files].first_sector_new_track = first_sector_new_track;
            first_sector_new_track = default_first_sector_new_track;
            files[num_files].nrSectorsShown = nrSectorsShown;
            files[num_files].filetype = filetype;
            files[num_files].direntryindex = -1;
            filename = NULL;
            sectorInterleave = 0;
            nrSectorsShown = -1;
            filetype = 0x82;
            filetype_set = false;
            num_files++;
            modified = 1;
            j++;
        } else if (strcmp(argv[j], "-L") == 0) {
            if (filename == NULL) {
                fprintf(stderr, "ERROR: Writing no file using -L requires disk filename set with -f\n");
                return -1;
            }
            evalhexescape(filename, files[num_files].pfilename, FILENAMEMAXSIZE, FILENAMEEMPTYCHAR);
            files[num_files].nrSectorsShown = nrSectorsShown;
            files[num_files].filetype = filetype;
            files[num_files].direntryindex = -1;
            files[num_files].mode |= MODE_NOFILE;

            first_sector_new_track = default_first_sector_new_track;
            filename = NULL;
            sectorInterleave = 0;
            nrSectorsShown = -1;
            filetype = 0x82;
            filetype_set = false;
            num_files++;
            modified = 1;
        } else if (strcmp(argv[j], "-x") == 0) {
            dirtracksplit = 0;
        } else if (strcmp(argv[j], "-t") == 0) {
            usedirtrack = 1;
        } else if (strcmp(argv[j], "-d") == 0) {
            if ((argc < j + 2) || !sscanf(argv[++j], "%u", &shadowdirtrack)) {
                fprintf(stderr, "ERROR: Error parsing argument for -d\n");
                return -1;
            }
            modified = 1;
        } else if (strcmp(argv[j], "-u") == 0) {
            if ((argc < j + 2) || !sscanf(argv[++j], "%d", &numdirblocks)) {
                fprintf(stderr, "ERROR: Error parsing argument for -u\n");
                return -1;
            }
        } else if (strcmp(argv[j], "-B") == 0) {
            if ((argc < j + 2) || !sscanf(argv[++j], "%d", &nrSectorsShown)) {
                fprintf(stderr, "ERROR: Error parsing argument for -B\n");
                return -1;
            }
            if (nrSectorsShown < 0 || nrSectorsShown > 65535) {
                fprintf(stderr, "ERROR: Argument must be between 0 and 65535 for -B\n");
                return -1;
            }
        } else if (strcmp(argv[j], "-4") == 0) {
            type = IMAGE_D64_EXTENDED_SPEED_DOS;
            modified = 1;
        } else if (strcmp(argv[j], "-R") == 0) {
            if ((argc < j + 2) || !sscanf(argv[++j], "%d", &restore_level)) {
                fprintf(stderr, "ERROR: Error parsing argument for -R\n");
                return -1;
            }
            if(restore_level < 0 || restore_level > 5) {
                fprintf(stderr, "ERROR: Argument must be between 0 and 5 for -R\n");
                return -1;
            }
        } else if (strcmp(argv[j], "-5") == 0) {
            type = IMAGE_D64_EXTENDED_DOLPHIN_DOS;
            modified = 1;
        } else if (strcmp(argv[j], "-a") == 0) {
            print_art_commandline = true;
        } else if(strcmp(argv[j], "-g") == 0) {
            if (argc < j + 2) {
                fprintf(stderr, "ERROR: Error parsing argument for -g\n");
                return -1;
            }
            filename_g64 = argv[++j];
        } else if (strcmp(argv[j], "-U") == 0) {
            if ((argc < j + 2) || !sscanf(argv[++j], "%d", &unicode)) {
                fprintf(stderr, "ERROR: Error parsing argument for -U\n");
                return -1;
            }
            if(unicode < 0 || unicode > 2) {
                fprintf(stderr, "ERROR: Argument must be between 0 and 2 for -U\n");
                return -1;
            }
        } else if (strcmp(argv[j], "-p") == 0) {
            if(num_patches >= MAXNUMPATCHES) {
                fprintf(stderr, "ERROR: Too many patches, maximum supported number is %d\n", MAXNUMPATCHES);
                return -1;
            }
            if (argc < j + 2) {
                fprintf(stderr, "ERROR: Error parsing argument for -p\n");
                return -1;
            }
            int numparsed;
            if((sscanf(argv[j + 1], "%d,%d,%d%n", &(patches[num_patches].track), &(patches[num_patches].sector), &(patches[num_patches].offset), &numparsed) != 3)
                    || (argv[j + 1][numparsed] != ',')) {
                fprintf(stderr, "ERROR: Error parsing arguments for -p\n");
                return -1;
            }
            patches[num_patches++].filename = argv[j + 1] + numparsed + 1;
            modified = 1;
            j++;
        } else if (strcmp(argv[j], "-X") == 0) {
            if(num_extractions >= MAXNUMEXTRACTIONS) {
                fprintf(stderr, "ERROR: Too many file extraction patterns, maximum supported number is %d\n", MAXNUMPATCHES);
                return -1;
            }
            if (argc < j + 2) {
                fprintf(stderr, "ERROR: Error parsing argument for -X\n");
                return -1;
            }
            extractions[num_extractions++] = argv[j + 1];
            j++;
        } else if (strcmp(argv[j], "-q") == 0) {
            quiet = 1;
        } else if (strcmp(argv[j], "-v") == 0) {
            verbose = 1;
        } else if (strcmp(argv[j], "-h") == 0) {
            usage();
        } else {
            fprintf(stderr, "ERROR: Error parsing command line at \"%s\"\n", argv[j]);
            printf("Use -h for help.\n");
            return -1;
        }
    }
    if (j >= argc) {
        fprintf(stderr, "ERROR: No image file provided, or misparsed last option\n");
        return -1;
    }
    imagepath = argv[argc-1];

    if (strlen(imagepath) >= 4) {
        if (strcmp(imagepath + strlen(imagepath) - 4, ".d71") == 0) {
            if ((type == IMAGE_D64_EXTENDED_SPEED_DOS) || (type == IMAGE_D64_EXTENDED_DOLPHIN_DOS)) {
                fprintf(stderr, "ERROR: Extended .d71 images are not supported\n");
                return -1;
            }
            type = IMAGE_D71;
        } else if (strcmp(imagepath + strlen(imagepath) - 4, ".d81") == 0) {
            if ((type == IMAGE_D64_EXTENDED_SPEED_DOS) || (type == IMAGE_D64_EXTENDED_DOLPHIN_DOS)) {
                fprintf(stderr, "ERROR: Extended .d81 images are not supported\n");
                return -1;
            }
            type = IMAGE_D81;
            dir_sector_interleave = 1;
        }
    }

    if(type != IMAGE_D71 && cluster_optimized) {
        fprintf(stderr, "ERROR: -c switch is only supported for D71 images\n");
        return -1;
    }

    if(bam_message != NULL && type != IMAGE_D64 && type != IMAGE_D64_EXTENDED_SPEED_DOS) {
        fprintf(stderr, "ERROR: Bam message only supported for D64 and SPEED DOS images\n");
        return -1;
    }

    if(shadowdirtrack > image_num_tracks(type) || (int)shadowdirtrack == dirtrack(type) || (type == IMAGE_D71 && (int)shadowdirtrack == dirtrack(type) + D64NUMTRACKS)) {
        fprintf(stderr, "ERROR: Invalid shadow directory track\n");
        return -1;
    }

    if(!transwarp_set) {
        allocation_strategy = 0;
    }

    if (type != IMAGE_D64) {
        if (transwarp_set
                && (type != IMAGE_D64_EXTENDED_SPEED_DOS)
                && (type != IMAGE_D64_EXTENDED_DOLPHIN_DOS)) {
            fprintf(stderr, "ERROR: Transwarp encoding is not supported for non-D64 images\n");
            return -1;
        }

        if (filename_g64 != NULL) {
            fprintf(stderr, "ERROR: G64 output is only supported for non-extended D64 images\n");
            return -1;
        }
    }

    /* Check for unsupported settings for D81 */
    if (type == IMAGE_D81) {
        if (default_sector_interleave_set) {
            fprintf(stderr, "ERROR: -S is not supported for D81 images\n");
            return -1;
        }
        if (sector_interleave_set) {
            fprintf(stderr, "ERROR: -s is not supported for D81 images\n");
            return -1;
        }
        if (new_track_start_sector_set) {
            fprintf(stderr, "ERROR: -F is not supported for D81 images\n");
            return -1;
        }
        if (file_start_sector_set) {
            fprintf(stderr, "ERROR: -b is not supported for D81 images\n");
            return -1;
        }
    }

    /* Change locale from C to default to allow unicode printouts */
    if(unicode != 0) {
        setlocale(LC_ALL, "");
    }

    /* quiet has precedence over verbose */
    if(quiet) {
        verbose = 0;
    }

    /* open image */
    unsigned int imagesize = image_size(type);
    unsigned char* image = (unsigned char*)calloc(imagesize, sizeof(unsigned char));
    if (image == NULL) {
        fprintf(stderr, "ERROR: Memory allocation error\n");
        return -1;
    }
    FILE* f = fopen(imagepath, "rb");
    if (f == NULL) {
        modified = 1;
        if (!quiet) {
            printf("Adding %d files to new image %s\n", num_files, basename((unsigned char*)imagepath));
        }
        initialize_directory(type, image, header, id, bam_message, shadowdirtrack, usedirtrack);
    } else {
        if (!quiet) {
            printf("Adding %d files to existing image %s\n", num_files, basename((unsigned char*)imagepath));
        }
        size_t read_size = fread(image, 1, imagesize, f);
        fclose(f);
        if (read_size != imagesize) {
            if (((type == IMAGE_D64_EXTENDED_SPEED_DOS) || (type == IMAGE_D64_EXTENDED_DOLPHIN_DOS)) && (read_size == D64SIZE)) {
                /* Clear extra tracks */
                memset(image + image_size(IMAGE_D64), 0, image_size(type) - image_size(IMAGE_D64));

                /* Mark all extra sectors unused */
                for (unsigned int t = D64NUMTRACKS + 1; t <= image_num_tracks(type); t++) {
                    for (int s = 0; s < num_sectors(type, t); s++) {
                        mark_sector(type, image, t, s, 1 /* free */);
                    }
                }
            } else {
                fprintf(stderr, "ERROR: Wrong filesize: expected to read %u bytes, but read %u bytes\n", imagesize, (unsigned int) read_size);
                return -1;
            }
        }
        if (dovalidate) {
            validate(type, image);
        }
        if (restore_level >= 0) {
            restore(type, image, restore_level, files);
        }
        if (set_header) {
            update_directory(type, image, header, id, bam_message, shadowdirtrack);
        }
    }

    /* Print command line before adding anything to the image */
    if(print_art_commandline) {
        convert_to_commandline(type, image);
    }

    /* Extract files */
    extract_files(type, image, extractions, num_extractions);

    /* Apply patches */
    apply_patches(type, image, patches, num_patches);

    /* Create directory entries */
    create_dir_entries(type, image, files, num_files, dir_sector_interleave, shadowdirtrack, nooverwrite);

    /* Write files and mark sectors in BAM */
    write_files(type, image, files, num_files, usedirtrack, dirtracksplit, shadowdirtrack, numdirblocks, dir_sector_interleave, allocation_strategy);

    /* Print allocation info */
    if (verbose) {
        print_file_allocation(type, image, files, num_files);
    }
    int blocks_free = check_bam(type, image);

    /* Print directory */
    if (!quiet) {
        print_directory(type, image, blocks_free);
    }

    /* Show directory issues if present */
    if(dir_error != DIR_OK) {
        fprintf(stdout, "WARNING: %s\n", dir_error_string[dir_error]);
    }

    /* Save image */
    if(modified) {
        f = fopen(imagepath, "wb");
        if (f == NULL || fwrite(image, imagesize, 1, f) != 1) {
            fprintf(stderr, "ERROR: Failed to write %s\n", imagepath);
            retval = -1;
        }
        if (f != NULL) {
            fclose(f);
        }
    }

    /* Save optional g64 image */
    if (filename_g64 != NULL) {
        /* retval might be set to -1 already.  Thus we need to take its
        previous state and OR it with the following return value. */
        retval |= generate_uniformat_g64(image, filename_g64);
    }

    if (!ignore_collision && check_hashes(type, image)) {
        fprintf(stderr, "\nERROR: Filename hash collision detected; image is not compatible with Krill's loader. Use -m to ignore this error.\n");
        retval = -1;
    }

    free(image);

    return retval;
}
