Details | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
9357 | Boppan | 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(" |
||
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(" |
||
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(" |
||
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(" |
||
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(" |
||
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(" |
||
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>><>>>>>>>>>>>>>>>>>>>>>>><>><>><>=>>>> |