|
1 | 1 | #!/usr/bin/env lua |
2 | 2 |
|
3 | | -H=[[This script shows what blocks are allocated to what files on a FAT12/16 formatted floppy disk image (.IMA). |
| 3 | +--[[ Implicit global variables: |
| 4 | + Z = Whether to zero out free clusters or not |
| 5 | +]] |
| 6 | + |
| 7 | +-- H = Help string |
| 8 | +H = [[This script shows what blocks are allocated to what files on a FAT12/16 formatted floppy disk image (.IMA). |
4 | 9 | The -z option will shred unallocated clusters. Backup any file before using the -z option on it.]] |
5 | | -if #arg<1 then print(arg[-1].." "..arg[0].." [-z] [IMA...]"..'\n\n'..H)os.exit(1)end |
6 | | -function R16(s,o)local a,b=s:byte(o,o+1)return a+b*256 end |
7 | | -function R32(s,o)local a,b,c,d=s:byte(o,o+3)return a+b*256+c*65536+d*16777216 end |
8 | | -function F12(f,o,s)f:seek("set",o)local fd,fat,mc=f:read(s),{},math.floor(s*8/12) |
9 | | - function E(i)local c=math.floor(i*1.5)local a,b=fd:byte(c+1)or 0,fd:byte(c+2)or 0 if i%2==0 then return a+(b&0x0F)*256 else return (a>>4)+b*16 end end |
10 | | - for i=2,mc-1 do fat[i]=E(i)end return fat end |
11 | | -function F16(f,o,s) |
12 | | - f:seek("set",o)local fd,fat=f:read(s),{} |
13 | | - for i=2,#fd-1,2 do local lo,hi=fd:byte(i),fd:byte(i+1)fat[((i-2)//2)+2]=lo+hi*256 end return fat end |
14 | | -function FD(ts,rs,nf,sf,rd,sc) |
15 | | - local c,r=math.floor((ts-rs-(nf*sf)-rd)/sc)r=c<4085 and 12 or c<65525 and 16 or 32 return r,c end |
16 | | -function CC(fat,sc) |
17 | | - local h,c={},sc |
18 | | - while c>=2 and c<0xFF8 do table.insert(h,c)c=fat[c] end return h end |
| 10 | + |
| 11 | +if #arg < 1 then -- Print help and exit when there's no arguments |
| 12 | + print(arg[-1] .. " " .. arg[0] .. " [-z] [IMA...]" .. '\n\n' .. H) |
| 13 | + os.exit(1) |
| 14 | +end |
| 15 | + |
| 16 | +---Read 16 bits from an offset of a string |
| 17 | +---@param s string The string to read from |
| 18 | +---@param o number The offset in the string to read at |
| 19 | +---@return number The 16-bit value at the offset of the string |
| 20 | +function R16(s, o) |
| 21 | + local a, b = s:byte(o, o + 1) |
| 22 | + return a + b * 256 |
| 23 | +end |
| 24 | + |
| 25 | +---Read 32 bits from an offset of a string |
| 26 | +---@param s string The string to read from |
| 27 | +---@param o number The offset in the string to read at |
| 28 | +---@return number The 32-bit value at the offset of the string |
| 29 | +function R32(s, o) |
| 30 | + local a, b, c, d = s:byte(o, o + 3) |
| 31 | + return a + b * 256 + c * 65536 + d * 16777216 |
| 32 | +end |
| 33 | + |
| 34 | +---Read the allocation table of a FAT-12 partition |
| 35 | +---@param f file The file the partition is on |
| 36 | +---@param o number Offset relative to the file where the FAT partition starts |
| 37 | +---@param s number Size of the FAT partition |
| 38 | +---@return table A list containing each clusters allocation state |
| 39 | +function F12(f, o, s) |
| 40 | + f:seek("set", o) |
| 41 | + local fd, fat, mc = f:read(s), {}, math.floor(s * 8 / 12) |
| 42 | + |
| 43 | + ---Extract the FAT-12 cluster value |
| 44 | + ---@param i number Number of the cluster to read |
| 45 | + ---@return number The 12-bit cluster number |
| 46 | + function E(i) |
| 47 | + local c = math.floor(i * 1.5) |
| 48 | + local a, b = fd:byte(c + 1) or 0, fd:byte(c + 2) or 0 |
| 49 | + if i % 2 == 0 then |
| 50 | + return a + (b & 0x0F) * 256 |
| 51 | + else |
| 52 | + return (a >> 4) + b * 16 |
| 53 | + end |
| 54 | + end |
| 55 | + |
| 56 | + for i = 2, mc - 1 do |
| 57 | + fat[i] = E(i) |
| 58 | + end |
| 59 | + |
| 60 | + return fat |
| 61 | +end |
| 62 | + |
| 63 | +---Read the allocation table of a FAT-16 partition |
| 64 | +---@param f file The file the partition is on |
| 65 | +---@param o number Offset relative to the file where the FAT partition starts |
| 66 | +---@param s number Size of the FAT partition |
| 67 | +---@return table A list containing each clusters allocation state |
| 68 | +function F16(f, o, s) |
| 69 | + f:seek("set", o) |
| 70 | + local fd, fat = f:read(s), {} |
| 71 | + for i = 2, #fd - 1, 2 do |
| 72 | + local lo, hi = fd:byte(i), fd:byte(i + 1) |
| 73 | + fat[((i - 2) // 2) + 2] = lo + hi * 256 |
| 74 | + end |
| 75 | + return fat |
| 76 | +end |
| 77 | + |
| 78 | +---FAT Determine: Determine if the FAT type (12, 16 or 32) and total cluster count |
| 79 | +---@param ts number Total number of sectors on the disk |
| 80 | +---@param rs number Reserved sectors before the FAT header |
| 81 | +---@param nf number Number of FATs |
| 82 | +---@param sf number Sectors per FAT |
| 83 | +---@param rd number Root directory sectors |
| 84 | +---@param sc number Sectors per cluster |
| 85 | +---@return number, number The fat type as a number (12, 16, 32) and the cluster count |
| 86 | +function FD(ts, rs, nf, sf, rd, sc) |
| 87 | + local c, r = math.floor((ts - rs - (nf * sf) - rd) / sc) |
| 88 | + r = c < 4085 and 12 or c < 65525 and 16 or 32 |
| 89 | + return r, c |
| 90 | +end |
| 91 | + |
| 92 | +---Cluster Chain: Parse all clusters that are allocated to a single file |
| 93 | +---@param fat table The FAT table |
| 94 | +---@param sc number The starting cluster of the file |
| 95 | +---@return table List of all applicable clusters |
| 96 | +function CC(fat, sc) |
| 97 | + local h, c = {}, sc |
| 98 | + while c >= 2 and c < 0xFF8 do |
| 99 | + table.insert(h, c) |
| 100 | + c = fat[c] |
| 101 | + end |
| 102 | + return h |
| 103 | +end |
| 104 | + |
| 105 | +---Format Ranges: Sort and group cluster ranges together for easy reading by humans |
| 106 | +---@param c table A list of clusters representing a file allocation |
| 107 | +---@return table A list strings representing the same clusters in a sorted and ranged order |
19 | 108 | function FR(c) |
20 | | - table.sort(c)local ranges,i={},1 |
21 | | - while i<=#c do |
22 | | - local s,j,f=c[i],i while j+1<=#c and c[j+1]==c[j]+1 do j=j+1 end f=c[j] |
23 | | - if s==f then table.insert(ranges,string.format("<%d>",s)) |
24 | | - else table.insert(ranges,string.format("<%d-%d>",s,f))end |
25 | | - i=j+1 end return table.concat(ranges," ")end |
26 | | -function DE(f,o,e,fat) |
27 | | - f:seek("set",o) |
28 | | - for _=1,e do |
29 | | - local en,fb=f:read(32)if not en then break end fb=en:byte(1)if fb==0 then break end |
30 | | - if fb~=229 and(en:byte(12)&8)==0 then |
31 | | - local n,x=en:sub(1,8):gsub("+$",""),en:sub(9,11):gsub("+$","") |
32 | | - io.write(string.format("\t%8s %3s %s\n",n,x,FR(CC(fat,R16(en,27)))))end end end |
33 | | -function FC(fa,f,st,s,uc) |
34 | | - local fr,z={},string.rep("\0",s)io.write("\n\tFree Space ") |
35 | | - for i=2,uc+1 do if fa[i]==0 then table.insert(fr,i)if Z then f:seek("set",st+(i-2)*s)f:write(z)end end end print(FR(fr))end |
36 | | -F={}for _,v in ipairs(arg)do if v=="-z" then Z=1 else table.insert(F,v)end end for _,fn in ipairs(F)do |
37 | | -local f=io.open(fn,(Z and"r+b"or"rb"))if f then |
38 | | - print(fn..":") |
39 | | - local b,o=f:read(512)o=b:sub(1,1):byte()if o==235 or o==233 then |
40 | | - local bs,sc,rs,nf,re,ts,sf=R16(b,12),b:byte(14),R16(b,15),b:byte(17),R16(b,18),R16(b,20),R16(b,23)if ts==0 then ts=R32(b,32)end |
41 | | - local rd,fo,fs,ro,ft,uc=math.ceil(re*32/bs),rs*bs,sf*bs,(rs+nf*sf)*bs ft,uc=FD(ts,rs,nf,sf,rd,sc) |
42 | | - print(string.format("\t FAT type: FAT%s\n\t Bytes/sector: %d\n\tSectors/cluster: %d\n\n",ft,bs,sc)) |
43 | | - local fat=ft==12 and F12(f,fo,fs)or ft==16 and F16(f,fo,fs)or error("Non-FAT type") |
44 | | - DE(f,ro,re,fat)FC(fat,f,(rs+(nf*sf)+rd)*bs,sc*bs,uc)end f:close()print()end end |
| 109 | + table.sort(c) |
| 110 | + local ranges, i = {}, 1 |
| 111 | + while i <= #c do |
| 112 | + local s, j, f = c[i], i |
| 113 | + while j + 1 <= #c and c[j + 1] == c[j] + 1 do |
| 114 | + j = j + 1 |
| 115 | + end |
| 116 | + f = c[j] |
| 117 | + if s == f then |
| 118 | + table.insert(ranges, string.format("<%d>", s)) |
| 119 | + else |
| 120 | + table.insert(ranges, string.format("<%d-%d>", s, f)) |
| 121 | + end |
| 122 | + i = j + 1 |
| 123 | + end |
| 124 | + return table.concat(ranges, " ") |
| 125 | +end |
| 126 | + |
| 127 | +---Display Entries: print to the screen what entries belong to what files |
| 128 | +---@param f file The file the fat partition is on |
| 129 | +---@param o number The offset the directory entries begin at |
| 130 | +---@param e number The maximum entries to read |
| 131 | +---@param fat table The file allocation table |
| 132 | +function DE(f, o, e, fat) |
| 133 | + f:seek("set", o) |
| 134 | + for _ = 1, e do |
| 135 | + local en, fb = f:read(32) |
| 136 | + if not en then |
| 137 | + break |
| 138 | + end |
| 139 | + fb = en:byte(1) |
| 140 | + if fb == 0 then |
| 141 | + break |
| 142 | + end |
| 143 | + if fb ~= 229 and (en:byte(12) & 8) == 0 then |
| 144 | + local n, x = en:sub(1, 8):gsub("+$", ""), en:sub(9, 11):gsub("+$", "") |
| 145 | + io.write(string.format("\t%8s %3s %s\n", n, x, FR(CC(fat, R16(en, 27))))) |
| 146 | + end |
| 147 | + end |
| 148 | +end |
| 149 | + |
| 150 | +---Free Clusters: Find and optionally zero out all free clusters |
| 151 | +---@param fa table File allocation table |
| 152 | +---@param f file File the allocation table is on |
| 153 | +---@param st number Starting area of the data area |
| 154 | +---@param s number The size of a cluster in bytes |
| 155 | +---@param uc number The number of usable clusters |
| 156 | +function FC(fa, f, st, s, uc) |
| 157 | + local fr, z = {}, string.rep("\0", s) |
| 158 | + io.write("\n\tFree Space ") |
| 159 | + for i = 2, uc + 1 do |
| 160 | + if fa[i] == 0 then |
| 161 | + table.insert(fr, i) |
| 162 | + if Z then |
| 163 | + f:seek("set", st + (i - 2) * s) |
| 164 | + f:write(z) |
| 165 | + end |
| 166 | + end |
| 167 | + end |
| 168 | + print(FR(fr)) |
| 169 | +end |
| 170 | + |
| 171 | +F = {} -- List of files to parse |
| 172 | + |
| 173 | +for _, v in ipairs(arg) do -- Parse all arguments |
| 174 | + if v == "-z" then -- '-z' found, enable zeroing out of unallocated clusters |
| 175 | + Z = 1 |
| 176 | + else |
| 177 | + table.insert(F, v) |
| 178 | + end |
| 179 | +end |
| 180 | + |
| 181 | +for _, fn in ipairs(F) do -- Iterate over each file |
| 182 | + local f = io.open(fn, (Z and "r+b" or "rb")) -- Read only unless argument '-z' was found |
| 183 | + if f then |
| 184 | + print(fn .. ":") |
| 185 | + |
| 186 | + -- b = The first 512 bytes of the file |
| 187 | + -- o = The first byte of the file |
| 188 | + local b, o = f:read(512) |
| 189 | + o = b:sub(1, 1):byte() |
| 190 | + if o == 235 or o == 233 then -- The boot sector has a valid jump instruction to a FAT boot sector |
| 191 | + |
| 192 | + -- bs = Bytes per sector |
| 193 | + -- sc = Sector per cluster |
| 194 | + -- rs = Reserved sectors |
| 195 | + -- nf = Number of FATs |
| 196 | + -- re = Root directory entries |
| 197 | + -- ts = Total sectors |
| 198 | + -- sf = Sectors per FAT |
| 199 | + local bs, sc, rs, nf, re, ts, sf = R16(b, 12), b:byte(14), R16(b, 15), b:byte(17), R16(b, 18), R16(b, 20), R16(b, 23) |
| 200 | + |
| 201 | + if ts == 0 then -- Use the larger 32-bit number of sectors |
| 202 | + ts = R32(b, 32) |
| 203 | + end |
| 204 | + |
| 205 | + -- rd = Root directory sectors |
| 206 | + -- fo = FAT offset |
| 207 | + -- fs = FAT size in bytes |
| 208 | + -- ro = Root directory offset |
| 209 | + local rd, fo, fs, ro, ft, uc = math.ceil(re * 32 / bs), rs * bs, sf * bs, (rs + nf * sf) * bs |
| 210 | + |
| 211 | + -- ft = FAT type |
| 212 | + -- uc = Usable cluster count |
| 213 | + ft, uc = FD(ts, rs, nf, sf, rd, sc) |
| 214 | + |
| 215 | + -- Print some basic information about the FAT table |
| 216 | + print(string.format("\t FAT type: FAT%s\n\t Bytes/sector: %d\n\tSectors/cluster: %d\n\n", ft, bs, sc)) |
| 217 | + |
| 218 | + -- Parse the FAT table |
| 219 | + local fat = ft == 12 and F12(f, fo, fs) or ft == 16 and F16(f, fo, fs) or error("Non-FAT type") |
| 220 | + |
| 221 | + -- Print state of each cluster |
| 222 | + DE(f, ro, re, fat) |
| 223 | + FC(fat, f, (rs + (nf * sf) + rd) * bs, sc * bs, uc) |
| 224 | + end |
| 225 | + f:close() |
| 226 | + print() |
| 227 | + end |
| 228 | +end |
0 commit comments