Subversion Repositories Kolibri OS

Rev

Blame | Last modification | View Log | Download | RSS feed

  1. #!/usr/bin/env python3
  2. #
  3. # makeflop.py
  4. # Version 3
  5. # Brad Smith, 2019
  6. # http://rainwarrior.ca
  7. #
  8. # Simple file operations for a FAT12 floppy disk image.
  9. # Public domain.
  10.  
  11. import sys
  12. assert sys.version_info[0] == 3, "Python 3 required."
  13.  
  14. import struct
  15. import datetime
  16. import os
  17.  
  18. class Floppy:
  19.     """
  20.    Simple file operations for a FAT12 floppy disk image.
  21.  
  22.    Floppy() - creates a blank DOS formatted 1.44MB disk.
  23.    Floppy(data) - parses a disk from a given array of bytes.
  24.    .open(filename) - loads a file and returns a Floppy from its data.
  25.    .save(filename) - saves the disk image to a file.
  26.    .flush() - Updates the .data member with any pending changes.
  27.    .data - A bytearray of the disk image.
  28.  
  29.    .files() - Returns a list of strings, each is a file or directory. Directories end with a backslash.
  30.    .delete_path(path) - Deletes a file or directory (recursive).
  31.    .add_dir_path(path) - Creates a new empty directory, if it does not already exist (recursive). Returns cluster of directory, or -1 if failed.
  32.    .add_file_path(path,data) - Creates a new file (creating directory if needed) with the given data. Returns False if failed.
  33.    .extract_file_path(path) - Returns a bytearray of the file at the given path, None if not found.
  34.    .set_volume_id(id=None) - Sets the 32-bit volume ID. Use with no arguments to generate ID from current time.
  35.    .set_volume_label(label) - Sets the 11-character volume label.
  36.  
  37.    .boot_info() - Returns a string displaying boot sector information.
  38.    .fat_info() - Returns a very long string of all 12-bit FAT entries.
  39.    .files_info() - Returns a string displaying the files() list.
  40.  
  41.    .add_all(path,prefix) - Adds all files from local path to disk (uppercased). Use prefix to specify a target directory. Returns False if any failed.
  42.    .extract_all(path) - Dumps entire contents of disk to local path.
  43.  
  44.    .find_path(path) - Returns a Floppy.FileEntry.
  45.    FileEntry.info() - Returns and information string about a FileEntry.
  46.  
  47.    This class provides some basic interface to a FAT12 floppy disk image.
  48.    Some things are fragile, in particular filenames that are too long,
  49.    or references to clusters outside the disk may cause exceptions.
  50.    The FAT can be accessed directly with some internal functions (see implementation)
  51.    and changes will be applied to the stored .data image with .flush().
  52.  
  53.    Example:
  54.        f = Floppy.open("disk.img")
  55.        print(f.boot_info()) # list boot information about disk
  56.        print(f.file_info()) # list files and directories
  57.        f.extract_all("disk_dump\\")
  58.        f.delete_path("DIR\\FILE.TXT") # delete a file
  59.        f.delete_path("DIR") # delete an entire directory
  60.        f.add_file_path("DIR\\NEW.TXT",open("new.txt","rb").read()) # add a file, directory automatically created
  61.        f.add_dir_path("NEWDIR") # creates a new directory
  62.        f.add_all("add\\","ADDED\\") # adds all files from a local directory to to a specified floppy directory.
  63.        f.set_volume_id() # generates a new volume ID
  64.        f.set_volume_label("MY DISK") # changes the volume label
  65.        f.save("new.img")
  66.    """
  67.  
  68.     EMPTY = 0xE5 # incipit value for an empty directory entry
  69.  
  70.     def _filestring(s, length):
  71.         """Creates an ASCII string, padded to length with spaces ($20)."""
  72.         b = bytearray(s.encode("ASCII"))
  73.         b = b + bytearray([0x20] * (length - len(b)))
  74.         if len(b) != length:
  75.             raise self.Error("File string '%s' too long? (%d != %d)" % (s,len(b),length))
  76.         return b
  77.  
  78.     class Error(Exception):
  79.         """Floppy has had an error."""
  80.         pass
  81.    
  82.     class FileEntry:
  83.         """A directory entry for a file."""
  84.  
  85.         def __init__(self, data=None, dir_cluster=-1, dir_index=-1):
  86.             """Unpacks a 32 byte directory entry into a FileEntry structure."""
  87.             if data == None:
  88.                 data = bytearray([Floppy.EMPTY]+([0]*31))
  89.             self.data = bytearray(data)
  90.             self.path = ""
  91.             self.incipit = data[0]
  92.             if self.incipit != 0x00 and self.incipit != Floppy.EMPTY:
  93.                 filename = data[0:8].decode("cp866")
  94.                 filename = filename.rstrip(" ")
  95.                 extension = data[8:11].decode("cp866")
  96.                 extension = extension.rstrip(" ")
  97.                 self.path = filename
  98.                 if len(extension) > 0:
  99.                     self.path = self.path + "." + extension
  100.             block = struct.unpack("<BHHHHHHHHL",data[11:32])
  101.             self.attributes = block[0]
  102.             self.write_time = block[6]
  103.             self.write_date = block[7]
  104.             self.cluster = block[8]
  105.             self.size = block[9]
  106.             self.dir_cluster = dir_cluster
  107.             self.dir_index = dir_index
  108.  
  109.         def compile(self):
  110.             """Commits any changed data to the entry, rebuilds and returns the 32 byte structure."""
  111.             filename = ""
  112.             extension = ""
  113.             period = self.path.find(".")
  114.             if (period >= 0):
  115.                 filename = self.path[0:period]
  116.                 extension = self.path[period+1:]
  117.             else:
  118.                 filename = self.path
  119.             if self.incipit != 0x00 and self.incipit != 0xEF:
  120.                 self.data[0:8] = Floppy._filestring(filename,8)
  121.                 self.data[8:11] = Floppy._filestring(extension,3)
  122.             else:
  123.                 self.data[0] = self.incipit
  124.             self.data[11] = self.attributes
  125.             self.data[22:32] = bytearray(struct.pack("<HHHL",
  126.                 self.write_time,
  127.                 self.write_date,
  128.                 self.cluster,
  129.                 self.size))
  130.             return bytearray(self.data)
  131.  
  132.         def info(self):
  133.             """String of information about a FileEntry."""
  134.             s = ""
  135.             s += "Path: [%s]\n" % self.path
  136.             s += "Incipit: %02X\n" % self.incipit
  137.             s += "Attributes: %02X\n" % self.attributes
  138.             s += "Write: %04X %04X\n" % (self.write_date, self.write_time)
  139.             s += "Cluster: %03X\n" % self.cluster
  140.             s += "Size: %d bytes\n" % self.size
  141.             s += "Directory: %03X, %d\n" % (self.dir_cluster, self.dir_index)
  142.             return s
  143.  
  144.         def fat_time(year, month, day, hour, minute, second):
  145.             """Builds a FAT12 date/time entry."""
  146.             date = ((year - 1980) << 9) | ((month) << 5) | day
  147.             time = (hour << 11) | (minute << 5) | (second >> 1)
  148.             return (date, time)
  149.  
  150.         def fat_time_now():
  151.             """Builds the current time as a FAT12 date/time entry."""
  152.             now = datetime.datetime.now()
  153.             return Floppy.FileEntry.fat_time(now.year, now.month, now.day, now.hour, now.minute, now.second)
  154.  
  155.         def set_name(self,s):
  156.             """Sets the filename and updates incipit."""
  157.             self.path = s
  158.             self.incipit = Floppy._filestring(s,12)[0]
  159.  
  160.         def set_now(self):
  161.             """Updates modified date/time to now."""
  162.             (date,time) = Floppy.FileEntry.fat_time_now()
  163.             self.write_date = date
  164.             self.write_time = time
  165.            
  166.         def new_file(name):
  167.             """Generate a new file entry."""
  168.             e = Floppy.FileEntry()
  169.             e.set_name(name)
  170.             e.set_now()
  171.             e.attributes = 0x00
  172.             return e
  173.  
  174.         def new_dir(name):
  175.             """Generate a new subdirectory entry."""
  176.             e = Floppy.FileEntry()
  177.             e.set_name(name)
  178.             e.set_now()
  179.             e.attributes = 0x10
  180.             return e
  181.  
  182.         def new_volume(name):
  183.             """Generate a new volume entry."""
  184.             e = Floppy.FileEntry()
  185.             if len(name) > 8:
  186.                 name = name[0:8] + "." + name[8:]
  187.             e.set_name(name)
  188.             e.set_now()
  189.             e.attributes = 0x08
  190.             return e
  191.  
  192.         def new_terminal():
  193.             """Generate a new directory terminating entry."""
  194.             e = Floppy.FileEntry()
  195.             e.incipit = 0x00
  196.             return e
  197.            
  198.     # blank formatted 1.44MB MS-DOS 5.0 non-system floppy
  199.     blank_floppy = [ # boot sector, 2xFAT, "BLANK" volume label, F6 fill
  200.         0xEB,0x3C,0x90,0x4D,0x53,0x44,0x4F,0x53,0x35,0x2E,0x30,0x00,0x02,0x01,0x01,0x00,
  201.         0x02,0xE0,0x00,0x40,0x0B,0xF0,0x09,0x00,0x12,0x00,0x02,0x00,0x00,0x00,0x00,0x00,
  202.         0x00,0x00,0x00,0x00,0x00,0x00,0x29,0xE3,0x16,0x53,0x1B,0x42,0x4C,0x41,0x4E,0x4B,
  203.         0x20,0x20,0x20,0x20,0x20,0x20,0x46,0x41,0x54,0x31,0x32,0x20,0x20,0x20,0xFA,0x33,
  204.         0xC0,0x8E,0xD0,0xBC,0x00,0x7C,0x16,0x07,0xBB,0x78,0x00,0x36,0xC5,0x37,0x1E,0x56,
  205.         0x16,0x53,0xBF,0x3E,0x7C,0xB9,0x0B,0x00,0xFC,0xF3,0xA4,0x06,0x1F,0xC6,0x45,0xFE,
  206.         0x0F,0x8B,0x0E,0x18,0x7C,0x88,0x4D,0xF9,0x89,0x47,0x02,0xC7,0x07,0x3E,0x7C,0xFB,
  207.         0xCD,0x13,0x72,0x79,0x33,0xC0,0x39,0x06,0x13,0x7C,0x74,0x08,0x8B,0x0E,0x13,0x7C,
  208.         0x89,0x0E,0x20,0x7C,0xA0,0x10,0x7C,0xF7,0x26,0x16,0x7C,0x03,0x06,0x1C,0x7C,0x13,
  209.         0x16,0x1E,0x7C,0x03,0x06,0x0E,0x7C,0x83,0xD2,0x00,0xA3,0x50,0x7C,0x89,0x16,0x52,
  210.         0x7C,0xA3,0x49,0x7C,0x89,0x16,0x4B,0x7C,0xB8,0x20,0x00,0xF7,0x26,0x11,0x7C,0x8B,
  211.         0x1E,0x0B,0x7C,0x03,0xC3,0x48,0xF7,0xF3,0x01,0x06,0x49,0x7C,0x83,0x16,0x4B,0x7C,
  212.         0x00,0xBB,0x00,0x05,0x8B,0x16,0x52,0x7C,0xA1,0x50,0x7C,0xE8,0x92,0x00,0x72,0x1D,
  213.         0xB0,0x01,0xE8,0xAC,0x00,0x72,0x16,0x8B,0xFB,0xB9,0x0B,0x00,0xBE,0xE6,0x7D,0xF3,
  214.         0xA6,0x75,0x0A,0x8D,0x7F,0x20,0xB9,0x0B,0x00,0xF3,0xA6,0x74,0x18,0xBE,0x9E,0x7D,
  215.         0xE8,0x5F,0x00,0x33,0xC0,0xCD,0x16,0x5E,0x1F,0x8F,0x04,0x8F,0x44,0x02,0xCD,0x19,
  216.         0x58,0x58,0x58,0xEB,0xE8,0x8B,0x47,0x1A,0x48,0x48,0x8A,0x1E,0x0D,0x7C,0x32,0xFF,
  217.         0xF7,0xE3,0x03,0x06,0x49,0x7C,0x13,0x16,0x4B,0x7C,0xBB,0x00,0x07,0xB9,0x03,0x00,
  218.         0x50,0x52,0x51,0xE8,0x3A,0x00,0x72,0xD8,0xB0,0x01,0xE8,0x54,0x00,0x59,0x5A,0x58,
  219.         0x72,0xBB,0x05,0x01,0x00,0x83,0xD2,0x00,0x03,0x1E,0x0B,0x7C,0xE2,0xE2,0x8A,0x2E,
  220.         0x15,0x7C,0x8A,0x16,0x24,0x7C,0x8B,0x1E,0x49,0x7C,0xA1,0x4B,0x7C,0xEA,0x00,0x00,
  221.         0x70,0x00,0xAC,0x0A,0xC0,0x74,0x29,0xB4,0x0E,0xBB,0x07,0x00,0xCD,0x10,0xEB,0xF2,
  222.         0x3B,0x16,0x18,0x7C,0x73,0x19,0xF7,0x36,0x18,0x7C,0xFE,0xC2,0x88,0x16,0x4F,0x7C,
  223.         0x33,0xD2,0xF7,0x36,0x1A,0x7C,0x88,0x16,0x25,0x7C,0xA3,0x4D,0x7C,0xF8,0xC3,0xF9,
  224.         0xC3,0xB4,0x02,0x8B,0x16,0x4D,0x7C,0xB1,0x06,0xD2,0xE6,0x0A,0x36,0x4F,0x7C,0x8B,
  225.         0xCA,0x86,0xE9,0x8A,0x16,0x24,0x7C,0x8A,0x36,0x25,0x7C,0xCD,0x13,0xC3,0x0D,0x0A,
  226.         0x4E,0x6F,0x6E,0x2D,0x53,0x79,0x73,0x74,0x65,0x6D,0x20,0x64,0x69,0x73,0x6B,0x20,
  227.         0x6F,0x72,0x20,0x64,0x69,0x73,0x6B,0x20,0x65,0x72,0x72,0x6F,0x72,0x0D,0x0A,0x52,
  228.         0x65,0x70,0x6C,0x61,0x63,0x65,0x20,0x61,0x6E,0x64,0x20,0x70,0x72,0x65,0x73,0x73,
  229.         0x20,0x61,0x6E,0x79,0x20,0x6B,0x65,0x79,0x20,0x77,0x68,0x65,0x6E,0x20,0x72,0x65,
  230.         0x61,0x64,0x79,0x0D,0x0A,0x00,0x49,0x4F,0x20,0x20,0x20,0x20,0x20,0x20,0x53,0x59,
  231.         0x53,0x4D,0x53,0x44,0x4F,0x53,0x20,0x20,0x20,0x53,0x59,0x53,0x00,0x00,0x55,0xAA,
  232.         ] + \
  233.         [0xF0,0xFF,0xFF] + ([0]*(0x1200-3)) + \
  234.         [0xF0,0xFF,0xFF] + ([0]*(0x1200-3)) + [
  235.         0x42,0x4C,0x41,0x4E,0x4B,0x20,0x20,0x20,0x20,0x20,0x20,0x28,0x00,0x00,0x00,0x00,
  236.         0x00,0x00,0x00,0x00,0x00,0x00,0xEE,0x7C,0x24,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
  237.         ] + ([0] * (0x4200-0x2620)) + \
  238.         ([0xF6] * (0x168000-0x4200))
  239.  
  240.     def __init__(self,data=blank_floppy):
  241.         """Create Floppy() instance from image bytes, or pre-formatted blank DOS floppy by default."""
  242.         self.data = bytearray(data)
  243.         self._boot_open()
  244.         self._fat_open()
  245.  
  246.     def _boot_open(self):
  247.         """Parses the boot sector."""
  248.         if (len(self.data) < 38):
  249.             raise self.Error("Not enough data in image for boot sector. (%d bytes)" % len(data))
  250.         boot = struct.unpack("<HBHBHHBHHH",self.data[11:28])
  251.         self.sector_size = boot[0]
  252.         self.cluster_sects = boot[1]
  253.         self.reserved_sects = boot[2]
  254.         self.fat_count = boot[3]
  255.         self.root_max = boot[4]
  256.         self.sectors = boot[5]
  257.         self.fat_sects = boot[7]
  258.         self.track_sects = boot[8]
  259.         self.heads = boot[9]
  260.         self.volume_id = 0
  261.         self.volume_label = ""
  262.         if self.data[38] == 0x29 and len(self.data) >= 54:
  263.             self.volume_id = struct.unpack("<L",self.data[39:43])[0]
  264.             self.volume_label = self.data[43:54].decode("ASCII").rstrip(" ")
  265.         if (self.sectors * self.sector_size) > len(self.data):
  266.             raise self.Error("Not enough data to contain %d x %d byte sectors? (%d bytes)" %
  267.                              (self.sectors, self.sector_size, len(self.data)))
  268.         self.root = self.sector_size * (self.reserved_sects + (self.fat_count * self.fat_sects))
  269.         root_sectors = ((self.root_max * 32) + (self.sector_size-1)) // self.sector_size # round up to fill sector
  270.         self.cluster2 = self.root + (self.sector_size * root_sectors)
  271.  
  272.     def _boot_flush(self):
  273.         """Commits changes to the boot sector."""
  274.         boot = struct.pack("<HBHBHHBHHH",
  275.             self.sector_size,
  276.             self.cluster_sects,
  277.             self.reserved_sects,
  278.             self.fat_count,
  279.             self.root_max,
  280.             self.sectors,
  281.             self.data[21],
  282.             self.fat_sects,
  283.             self.track_sects,
  284.             self.heads)
  285.         self.data[11:28] = bytearray(boot)
  286.         if self.data[38] == 0x29 and len(self.data) >= 54:
  287.             self.data[39:43] = bytearray(struct.pack("<L",self.volume_id))
  288.             self.data[43:54] = Floppy._filestring(self.volume_label,11)
  289.  
  290.     def boot_info(self):
  291.         """String of information about the boot sector."""
  292.         s = ""
  293.         s += "Volume Label: [%s]\n" % self.volume_label
  294.         s += "Volume ID: %04X\n" % self.volume_id
  295.         s += "Sector size: %d bytes\n" % self.sector_size
  296.         s += "Cluster size: %d sectors\n" % self.cluster_sects
  297.         s += "Reserved sectors: %d\n" % self.reserved_sects
  298.         s += "Number of FATs: %d\n" % self.fat_count
  299.         s += "Maximum root entries: %d\n" % self.root_max
  300.         s += "Total sectors: %d\n" % self.sectors
  301.         s += "FAT size: %d sectors\n" % self.fat_sects
  302.         s += "Track size: %d sectors\n" % self.track_sects
  303.         s += "Heads: %d\n" % self.heads
  304.         s += "Root directory: %08X\n" % self.root
  305.         s += "Cluster 2: %08X\n" % self.cluster2
  306.         return s
  307.  
  308.     def _fat_open(self):
  309.         """Parses the FAT table."""
  310.         fat_start = self.reserved_sects * self.sector_size
  311.         fat_sects = self.fat_sects * self.sector_size
  312.         fat_end = fat_start + fat_sects
  313.         # make sure they're in the image
  314.         if (self.sectors < (self.reserved_sects + (self.fat_count * self.fat_sects))):
  315.             raise self.Error("Not enough sectors to contain %d + %d x %d FAT tables? (%d sectors)" %
  316.                             (self.reserved_sects, self.fat_count, self.fat_sects, self.sectors))
  317.         if (self.fat_count < 1) or (self.fat_sects < 1):
  318.             raise self.Error("No FAT tables? (%d x %d FAT sectors)" %
  319.                              (self.fat_count, self.fat_sects))        
  320.         # verify FAT tables match
  321.         for i in range(1,self.fat_count):
  322.             fat2_start = fat_start + (fat_sects * i)
  323.             fat2_end = fat2_start + fat_sects
  324.             if self.data[fat_start:fat_end] != self.data[fat2_start:fat2_end]:
  325.                 raise self.Error("FAT mismatch in table %d." % i)
  326.         # read FAT 0
  327.         self.fat = []
  328.         e = fat_start
  329.         while (e+2) <= fat_end:
  330.             entry = 0
  331.             if (len(self.fat) & 1) == 0:
  332.                 entry = self.data[e+0] | ((self.data[e+1] & 0x0F) << 8)
  333.                 e += 1
  334.             else:
  335.                 entry = ((self.data[e+0] & 0xF0) >> 4) | (self.data[e+1] << 4)
  336.                 e += 2
  337.             self.fat.append(entry)
  338.  
  339.     def _fat_flush(self):
  340.         """Commits changes to the FAT table."""
  341.         fat_start = self.reserved_sects * self.sector_size
  342.         fat_sects = self.fat_sects * self.sector_size
  343.         fat_end = fat_start + fat_sects
  344.         # build FAT 0
  345.         e = self.reserved_sects * self.sector_size
  346.         for i in range(len(self.fat)):
  347.             entry = self.fat[i]
  348.             if (i & 1) == 0:
  349.                 self.data[e+0] = entry & 0xFF
  350.                 self.data[e+1] = (self.data[e+1] & 0xF0) | ((entry >> 8) & 0x0F)
  351.                 e += 1
  352.             else:
  353.                 self.data[e+0] = (self.data[e+0] & 0x0F) | ((entry << 4) & 0xF0)
  354.                 self.data[e+1] = (entry >> 4) & 0xFF
  355.                 e += 2
  356.         # copy to all tables
  357.         for i in range(1,self.fat_count):
  358.             fat2_start = fat_start + (fat_sects * i)
  359.             fat2_end = fat2_start + fat_sects
  360.             self.data[fat2_start:fat2_end] = self.data[fat_start:fat_end]
  361.  
  362.     def fat_info(self):
  363.         """String of information about the FAT."""
  364.         per_line = 32
  365.         s = ""
  366.         for i in range(len(self.fat)):
  367.             if (i % per_line) == 0:
  368.                 s += "%03X: " % i
  369.             if (i % per_line) == (per_line // 2):
  370.                 s += "  " # extra space to mark 16
  371.             s += " %03X" % self.fat[i]
  372.             if (i % per_line) == (per_line - 1):
  373.                 s += "\n"
  374.         return s
  375.  
  376.     def _cluster_offset(self, cluster):
  377.         """Image offset of a FAT indexed logical cluster."""
  378.         if cluster < 2:
  379.             return self.root
  380.         return self.cluster2 + (self.sector_size * self.cluster_sects * (cluster-2))
  381.  
  382.     def _read_chain(self, cluster, size):
  383.         data = bytearray()
  384.         """Returns up to size bytes from a chain of clusters starting at cluster."""
  385.         while cluster < 0xFF0:
  386.             offset = self._cluster_offset(cluster)
  387.             #print("read_chain(%03X,%d) at %08X" % (cluster,size,offset))
  388.             if cluster < 2:
  389.                 return data + bytearray(self.data[self.root:self.root+size]) # root directory
  390.             read = min(size,(self.sector_size * self.cluster_sects))
  391.             data = data + bytearray(self.data[offset:offset+read])
  392.             size -= read
  393.             if (size < 1):
  394.                 return data
  395.             cluster = self.fat[cluster]
  396.         return data
  397.  
  398.     def _read_dir_chain(self, cluster):
  399.         """Reads an entire directory chain starting at the given cluster (0 for root)."""
  400.         if cluster < 2:
  401.             # root directory is contiguous
  402.             return self.data[self.root:self.root+(self.root_max*32)]
  403.         # directories just occupy as many clusters as in their FAT chain, using a dummy max size
  404.         return self._read_chain(cluster, self.sector_size*self.sectors//self.cluster_sects)
  405.  
  406.     def _delete_chain(self, cluster):
  407.         """Deletes a FAT chain."""
  408.         while cluster < 0xFF0 and cluster >= 2:
  409.             link = self.fat[cluster]
  410.             self.fat[cluster] = 0
  411.             cluster = link
  412.  
  413.     def _add_chain(self,data):
  414.         """Adds a block of data to the disk and creates its FAT chain. Returns start cluster, or -1 for failure."""
  415.         cluster_size = self.sector_size * self.cluster_sects
  416.         clusters = (len(data) + cluster_size-1) // cluster_size
  417.         if clusters < 1:
  418.             clusters = 1
  419.         # find a chain of free clusters
  420.         chain = []
  421.         for i in range(2,len(self.fat)):
  422.             if self.fat[i] == 0:
  423.                 chain.append(i)
  424.             if len(chain) >= clusters:
  425.                 break
  426.         if len(chain) < clusters:
  427.             return -1 # out of space
  428.         # store the FAT chain
  429.         start_cluster = chain[0]
  430.         for i in range(0,len(chain)-1):
  431.             self.fat[chain[i]] = chain[i+1]
  432.         self.fat[chain[len(chain)-1]] = 0xFFF
  433.         # store the data in the given clusters
  434.         data = bytearray(data)
  435.         c = 0
  436.         while len(data) > 0:
  437.             write = min(len(data),cluster_size)
  438.             offset = self._cluster_offset(chain[c])
  439.             self.data[offset:offset+write] = data[0:write]
  440.             c += 1
  441.             data = data[write:]
  442.         # done
  443.         return start_cluster
  444.        
  445.     def _files_dir(self, cluster, path):
  446.         """Returns a list of files in the directory starting at the given cluster. Recursive."""
  447.         #print("files_dir(%03X,'%s')" % (cluster, path))
  448.         entries = []
  449.         directory = self._read_dir_chain(cluster)
  450.         for i in range(len(directory) // 32):
  451.             e = self.FileEntry(directory[(i*32):(i*32)+32],cluster,i)
  452.             #print(("entry %d (%08X)\n"%(i,self._dir_entry_offset(cluster,i)))+e.info())
  453.             if e.incipit == 0x00: # end of directory
  454.                 return entries
  455.             if e.incipit == Floppy.EMPTY: # empty
  456.                 continue
  457.             if (e.attributes & 0x08) != 0: # volume label
  458.                 continue
  459.             if (e.attributes & 0x10) != 0: # subdirectory
  460.                 if e.path == "." or e.path == "..":
  461.                     continue
  462.                 subdir = path + e.path + "\\"
  463.                 entries.append(subdir)
  464.                 entries = entries + self._files_dir(e.cluster,subdir)
  465.             else:
  466.                 entries.append(path + e.path)
  467.         return entries
  468.  
  469.     def files(self):
  470.         """Returns a list of files in the image."""
  471.         root = self.sector_size * (self.reserved_sects + (self.fat_count * self.fat_sects))
  472.         return self._files_dir(0,"")
  473.  
  474.     def files_info(self):
  475.         """String of the file list."""
  476.         s = ""
  477.         for path in self.files():
  478.             s += path + "\n"
  479.         return s
  480.  
  481.     def _dir_entry_offset(self,cluster,dir_index):
  482.         """Find the offset in self.data to a particular directory entry."""
  483.         if (cluster < 2):
  484.             return self.root + (32 * dir_index)
  485.         if (cluster >= 0xFF0):
  486.             raise self.Error("Directory entry %d not in its cluster chain?" % dir_index)
  487.         per_cluster = (self.sector_size*self.cluster_sects)//32
  488.         if (dir_index < per_cluster): # within this cluster
  489.             return self._cluster_offset(cluster) + (32 * dir_index)
  490.         # continue to next cluster
  491.         return self._dir_entry_offset(self.fat[cluster],dir_index-per_cluster)
  492.  
  493.     def delete_file(self, entry):
  494.         """Deletes a FileEntry."""
  495.         self._delete_chain(entry.cluster) # delete its FAT chain
  496.         offset = self._dir_entry_offset(entry.dir_cluster,entry.dir_index)
  497.         self.data[offset+0] = Floppy.EMPTY # empty this entry
  498.  
  499.     def _find_path_dir(self, cluster, path):
  500.         """Recursive find path, breaking out subdirectories progressively."""
  501.         #print("_find_path_dir(%03X,'%s')" % (cluster,path))
  502.         separator = path.find("\\")
  503.         path_seek = path
  504.         path_next = ""
  505.         if separator >= 0:
  506.             path_seek = path[0:separator]
  507.             path_next = path[separator+1:]
  508.         directory = self._read_dir_chain(cluster)
  509.         for i in range(len(directory) // 32):
  510.             e = self.FileEntry(directory[(i*32):(i*32)+32],cluster,i)
  511.             if e.incipit == 0x00: # end of directory
  512.                 return None
  513.             if e.incipit == Floppy.EMPTY: # empty
  514.                 continue
  515.             if (e.attributes & 0x08) != 0: # volume label
  516.                 continue
  517.             if (e.attributes & 0x10) != 0: # subdirectory
  518.                 if e.path == "." or e.path == "..":
  519.                     continue
  520.                 if e.path == path_seek:
  521.                     if (len(path_next) > 0):
  522.                         return self._find_path_dir(e.cluster, path_next)
  523.                     else:
  524.                         return e
  525.             elif e.path == path_seek and path_next == "":
  526.                 return e
  527.         return None
  528.  
  529.     def find_path(self, path):
  530.         """Finds a FileEntry for a given path."""
  531.         return self._find_path_dir(0,path)
  532.  
  533.     def _delete_tree(self,de):
  534.         """Recursively deletes directory entries."""
  535.         #print("_delete_tree\n" + de.info())
  536.         directory = self._read_dir_chain(de.cluster)
  537.         for i in range(len(directory) // 32):
  538.             e = self.FileEntry(directory[(i*32):(i*32)+32],de.cluster,i)
  539.             #print(e.info())
  540.             if e.incipit == 0x00: # end of directory
  541.                 return
  542.             if e.incipit == Floppy.EMPTY: # empty
  543.                 continue
  544.             if (e.attributes & 0x08) != 0: # volume label
  545.                 continue
  546.             if (e.attributes & 0x10) != 0: # subdirectory
  547.                 if e.path == "." or e.path == "..":
  548.                     continue
  549.                 self._delete_tree(e) # recurse
  550.                 self.delete_file(e)
  551.             else:
  552.                 self.delete_file(e)
  553.  
  554.     def delete_path(self, path):
  555.         """Finds a file or directory and deletes it (recursive), if it exists. Returns True if successful."""
  556.         e = self.find_path(path)
  557.         if (e == None):
  558.             return False
  559.         if (e.attributes & 0x10) != 0:
  560.             self._delete_tree(e)
  561.         self.delete_file(e)
  562.         return True
  563.  
  564.     def _add_entry(self, dir_cluster, entry):
  565.         """
  566.        Adds an entry to a directory starting at the given cluster, appending a new cluster if needed.
  567.        Sets entry.dir_cluster and entry.dir_index to match their new directory.
  568.        Returns False if out of space.
  569.        """
  570.         #print(("_add_entry(%d):\n"%dir_cluster) + entry.info())
  571.         directory = self._read_dir_chain(dir_cluster)
  572.         dir_len = len(directory)//32
  573.         i = 0
  574.         terminal = False
  575.         while i < dir_len:
  576.             e = self.FileEntry(directory[(i*32):(i*32)+32],dir_cluster,i)
  577.             #print(e.info())
  578.             if e.incipit == 0x00:
  579.                 terminal = True # make sure to add another terminal entry after this one
  580.                 break
  581.             if e.incipit == Floppy.EMPTY:
  582.                 break
  583.             i += 1
  584.         # extend directory if out of room
  585.         if i >= dir_len:
  586.             if dir_cluster < 2:
  587.                 return False # no room in root
  588.             # add a zero-filled page to the end of this directory's FAT chain
  589.             chain = self._add_chain(bytearray([0]*(self.sector_size*self.cluster_sects)))
  590.             if (chain < 0):
  591.                 return False # no free clusters
  592.             tail = dir_cluster
  593.             while self.fat[tail] < 0xFF0:
  594.                 tail = self.fat[tail]
  595.             self.fat[tail] = chain
  596.             self.fat[chain] = 0xFFF
  597.         # insert entry
  598.         entry.dir_cluster = dir_cluster
  599.         entry.dir_index = i
  600.         offset = self._dir_entry_offset(dir_cluster,i)
  601.         self.data[offset:offset+32] = entry.compile()
  602.         # add a new terminal if needed
  603.         if terminal:
  604.             i += 1
  605.             if i < dir_len: # if it was the last entry, no new terminal is needed
  606.                 offset = self._dir_entry_offset(dir_cluster,i)
  607.                 self.data[offset:offset+32] = Floppy.FileEntry.new_terminal().compile()
  608.                
  609.         # success!
  610.         return True
  611.  
  612.     def _add_dir_recursive(self, cluster, path):
  613.         """Recursively creates directory, returns cluster of created dir, -1 if failed."""
  614.         #print("_add_dir_recursive(%03X,'%s')"%(cluster,path))
  615.         separator = path.find("\\")
  616.         path_seek = path
  617.         path_next = ""
  618.         if separator >= 0:
  619.             path_seek = path[0:separator]
  620.             path_next = path[separator+1:]
  621.         directory = self._read_dir_chain(cluster)
  622.         for i in range(len(directory) // 32):
  623.             e = self.FileEntry(directory[(i*32):(i*32)+32],cluster,i)
  624.             #print(e.info())
  625.             if e.incipit == 0x00: # end of directory
  626.                 break
  627.             if e.incipit == Floppy.EMPTY: # empty
  628.                 continue
  629.             if (e.attributes & 0x10) != 0: # subdirectory
  630.                 if e.path == path_seek: # already exists
  631.                     if len(path_next) < 1:
  632.                         return e.cluster # return existing directory
  633.                     else:
  634.                         return self._add_dir_recursive(e.cluster,path_next) # keep descending
  635.         # not found: create the directory
  636.         dp0 = Floppy.FileEntry.new_dir("_") # .
  637.         dp1 = Floppy.FileEntry.new_dir("__") # ..
  638.         dp1.cluster = cluster
  639.         dir_block = dp0.compile() + dp1.compile() + bytearray([0] * ((self.sector_size*self.cluster_sects)-64))
  640.         new_cluster = self._add_chain(dir_block)
  641.         if new_cluster < 0:
  642.             return -1 # out of space
  643.         # fix up "." to point to itself
  644.         dp0.cluster = new_cluster
  645.         offset = self._dir_entry_offset(new_cluster,0)
  646.         self.data[offset:offset+32] = dp0.compile()
  647.         # fix special directory names (FileEntry.compile() is incapable of . or ..)
  648.         self.data[offset+ 0:offset+11] = bytearray(".          ".encode("ASCII"))
  649.         self.data[offset+32:offset+43] = bytearray("..         ".encode("ASCII"))
  650.         # create entry to point to new directory cluster
  651.         new_dir = Floppy.FileEntry.new_dir(path_seek)
  652.         new_dir.cluster = new_cluster
  653.         if not self._add_entry(cluster, new_dir):
  654.             self._delete_chain(new_cluster)
  655.             return -1 # out of space
  656.         # return the entry if tail is reached, or keep descending
  657.         if len(path_next) < 1:
  658.             return new_cluster
  659.         else:
  660.             return self._add_dir_recursive(new_cluster,path_next)
  661.  
  662.     def add_dir_path(self, path):
  663.         """
  664.        Recursively ensures that the given directory path exists, creating it if necessary.
  665.        Path should not end in backslash.
  666.        Returns cluster of directory at path, or -1 if failed.
  667.        """
  668.         if (len(path) < 1):
  669.             return 0 # root
  670.         return self._add_dir_recursive(0,path)
  671.  
  672.     def add_file_path(self, path, data):
  673.         """
  674.        Adds the given data as a file at the given path.
  675.        Will automatically create directories to complete the path.
  676.        Returns False if failed.
  677.        """
  678.         self.delete_path(path) # remove file if it already exists
  679.         dir_path = ""
  680.         file_path = path
  681.         separator = path.rfind("\\")
  682.         if (separator >= 0):
  683.             dir_path = path[0:separator]
  684.             file_path = path[separator+1:]
  685.         dir_cluster = self.add_dir_path(dir_path)
  686.         if dir_cluster < 0:
  687.             return False # couldn't find or create directory
  688.         cluster = self._add_chain(data)
  689.         if (cluster < 0):
  690.             return False # out of space
  691.         entry = Floppy.FileEntry.new_file(file_path)
  692.         entry.cluster = cluster
  693.         entry.size = len(data)
  694.         if not self._add_entry(dir_cluster, entry):
  695.             self._delete_chain(cluster)
  696.             return False # out of space for directory entry
  697.         offset = self._dir_entry_offset(entry.dir_cluster, entry.dir_index)
  698.         self.data[offset:offset+32] = entry.compile()
  699.         return True
  700.  
  701.     def extract_file_path(self, path):
  702.         """Finds a file and returns all of its data. None on failure."""
  703.         e = self.find_path(path)
  704.         if e == None:
  705.             return None
  706.         return self._read_chain(e.cluster, e.size)
  707.        
  708.     def set_volume_id(self, value=None):
  709.         """Sets the volume ID. None for time-based."""
  710.         if (value == None):
  711.             (date,time) = Floppy.FileEntry.fat_time_now()
  712.             value = time | (date << 16)
  713.         self.volume_id = value & 0xFFFFFFFF
  714.  
  715.     def set_volume_label(self, label):
  716.         """Sets the volume label. Creates one if necessary."""
  717.         self.data[38] = 0x29
  718.         self.volume_label = label
  719.         self.data[54:62] = Floppy._filestring("FAT12",8)
  720.         # adjust existing volume entry in root
  721.         directory = self._read_dir_chain(0)
  722.         for i in range(len(directory) // 32):
  723.             e = self.FileEntry(directory[(i*32):(i*32)+32],0,i)
  724.             if e.incipit == 0x00: # end of directory
  725.                 break
  726.             if e.incipit == Floppy.EMPTY: # empty
  727.                 continue
  728.             if (e.attributes & 0x08) != 0: # existing volume label
  729.                 offset = self._dir_entry_offset(0,i)
  730.                 self.data[offset:offset+11] = Floppy._filestring(label,11)
  731.                 return
  732.         # volume entry does not exist in root, add one
  733.         self._add_entry(0,Floppy.FileEntry.new_volume(label))
  734.         return
  735.  
  736.     def open(filename):
  737.         """Opens a floppy image file and creates a Floppy() instance from it."""
  738.         return Floppy(open(filename,"rb").read())
  739.  
  740.     def flush(self):
  741.         """Commits all unfinished changes to self.data image."""
  742.         self._fat_flush()
  743.         self._boot_flush()
  744.  
  745.     def save(self, filename):
  746.         """Saves image (self.data) to file. Implies flush()."""
  747.         self.flush()
  748.         open(filename,"wb").write(self.data)
  749.  
  750.     def extract_all(self, out_directory):
  751.         """Extracts all files from image to specified directory."""
  752.         for in_path in self.files():
  753.             out_path = os.path.join(out_directory,in_path)
  754.             out_dir = os.path.dirname(out_path)
  755.             if not os.path.exists(out_dir):
  756.                 try:
  757.                     os.makedirs(out_dir)
  758.                 except OSError as e:
  759.                     if e.errono != errno.EEXIST:
  760.                         raise
  761.             if not in_path.endswith("\\"):
  762.                 open(out_path,"wb").write(self.extract_file_path(in_path))
  763.                 print(out_path)
  764.             else:
  765.                 print(out_path)
  766.  
  767.     def add_all(self, in_directory, prefix=""):
  768.         """
  769.        Adds all files from specified directory to image.
  770.        Files will be uppercased. Long filenames are not checked and will cause an exception.
  771.        prefix can be used to prefix a directory path (ending with \) to the added files.
  772.        """
  773.         result = True
  774.         def dospath(s):
  775.             s = s.upper()
  776.             s = s.replace("/","\\")
  777.             return s
  778.         if len(in_directory) < 1:
  779.             in_directory = "."
  780.         in_directory = os.path.normpath(in_directory) + os.sep
  781.         for (root, dirs, files) in os.walk(in_directory):
  782.             base = root[len(in_directory):]
  783.             for d in dirs:
  784.                 dir_path = prefix + dospath(os.path.join(base,d))
  785.                 result = result and (self.add_dir_path(dir_path) >= 0)
  786.                 print(dir_path + "\\")
  787.             for f in files:
  788.                 file_path = prefix + dospath(os.path.join(base,f))
  789.                 data = open(os.path.join(root,f),"rb").read()
  790.                 result = result and self.add_file_path(file_path,data)
  791.                 print(file_path + " (%d bytes)" % len(data))
  792.         return result
  793.