rewrite
This commit is contained in:
177
Src/iso.lua
Executable file
177
Src/iso.lua
Executable file
@@ -0,0 +1,177 @@
|
||||
-- Minimal ISO9660 parser
|
||||
-- Provides: ISO.open(path) -> iso object
|
||||
-- Methods: :close(), :readPVD(), :listRoot(), :readFile(path)
|
||||
|
||||
local M = {}
|
||||
|
||||
local SECTOR_SIZE = 2048
|
||||
|
||||
local function readBytes(f, pos, len)
|
||||
f:seek("set", pos)
|
||||
return f:read(len)
|
||||
end
|
||||
|
||||
local function u8(s, i)
|
||||
return string.byte(s, i)
|
||||
end
|
||||
|
||||
local function u16le(s, i)
|
||||
local b1 = string.byte(s, i)
|
||||
local b2 = string.byte(s, i+1)
|
||||
return b1 + b2 * 256
|
||||
end
|
||||
|
||||
local function u32le(s, i)
|
||||
local b1 = string.byte(s, i)
|
||||
local b2 = string.byte(s, i+1)
|
||||
local b3 = string.byte(s, i+2)
|
||||
local b4 = string.byte(s, i+3)
|
||||
return b1 + b2*256 + b3*65536 + b4*16777216
|
||||
end
|
||||
|
||||
local function trimVersion(name)
|
||||
-- remove ;1 version and trailing dots
|
||||
local n = name:gsub(";1$", "")
|
||||
n = n:gsub("%.$", "")
|
||||
return n
|
||||
end
|
||||
|
||||
local ISO = {}
|
||||
ISO.__index = ISO
|
||||
|
||||
function M.open(path)
|
||||
local f, err = io.open(path, "rb")
|
||||
if not f then return nil, err end
|
||||
local self = setmetatable({ f = f, path = path }, ISO)
|
||||
return self
|
||||
end
|
||||
|
||||
function ISO:close()
|
||||
if self.f then self.f:close(); self.f = nil end
|
||||
end
|
||||
|
||||
function ISO:readSector(n)
|
||||
local pos = n * SECTOR_SIZE
|
||||
return readBytes(self.f, pos, SECTOR_SIZE)
|
||||
end
|
||||
|
||||
function ISO:readPVD()
|
||||
-- Primary Volume Descriptor at sector 16 (index 16)
|
||||
local pvd = self:readSector(16)
|
||||
if not pvd or #pvd < SECTOR_SIZE then return nil, "cannot read PVD" end
|
||||
local typecode = u8(pvd,1)
|
||||
local id = pvd:sub(2,6)
|
||||
if typecode ~= 1 or id ~= "CD001" then return nil, "no primary volume descriptor" end
|
||||
|
||||
local volumeIdentifier = pvd:sub(41,72):gsub('%z+$','')
|
||||
-- root directory record starts at offset 157 (1-based index)
|
||||
local rdOffset = 157
|
||||
local rdLen = u8(pvd, rdOffset)
|
||||
local extent = u32le(pvd, rdOffset + 2)
|
||||
local dataLen = u32le(pvd, rdOffset + 10)
|
||||
|
||||
self.pvd = {
|
||||
volumeIdentifier = volumeIdentifier,
|
||||
root = { extent = extent, size = dataLen }
|
||||
}
|
||||
return self.pvd
|
||||
end
|
||||
|
||||
local function parseDirRecords(buffer, bytesToRead)
|
||||
local records = {}
|
||||
local offset = 1
|
||||
local total = bytesToRead or #buffer
|
||||
while offset <= total do
|
||||
local len = string.byte(buffer, offset)
|
||||
if not len or len == 0 then
|
||||
-- padding to sector boundary, move to next byte
|
||||
offset = offset + 1
|
||||
else
|
||||
local rec = {}
|
||||
rec.len = len
|
||||
rec.extAttrLen = string.byte(buffer, offset+1)
|
||||
rec.extent = u32le(buffer, offset+2)
|
||||
rec.size = u32le(buffer, offset+10)
|
||||
rec.recordingDate = buffer:sub(offset+18, offset+24)
|
||||
rec.flags = string.byte(buffer, offset+25)
|
||||
rec.fileUnitSize = string.byte(buffer, offset+26)
|
||||
rec.interleaveGapSize = string.byte(buffer, offset+27)
|
||||
rec.volumeSeq = u16le(buffer, offset+29)
|
||||
local fiLen = string.byte(buffer, offset+32)
|
||||
local idStart = offset + 33
|
||||
local idEnd = idStart + fiLen - 1
|
||||
local fileId = buffer:sub(idStart, idEnd)
|
||||
-- strip trailing ;1 and dots
|
||||
rec.fileIdentifier = fileId
|
||||
rec.fileName = trimVersion(fileId)
|
||||
rec.isDirectory = (rec.flags & 0x02) ~= 0
|
||||
table.insert(records, rec)
|
||||
offset = offset + len
|
||||
end
|
||||
end
|
||||
return records
|
||||
end
|
||||
|
||||
function ISO:listRoot()
|
||||
if not self.pvd then self:readPVD() end
|
||||
local root = self.pvd.root
|
||||
local extent = root.extent
|
||||
local size = root.size
|
||||
local pos = extent * SECTOR_SIZE
|
||||
local buffer = readBytes(self.f, pos, size)
|
||||
local records = parseDirRecords(buffer, size)
|
||||
-- filter out '.' and '..' and entries with empty name
|
||||
local out = {}
|
||||
for _, r in ipairs(records) do
|
||||
local name = r.fileName
|
||||
if name and name ~= "" and name ~= "\0" and name ~= "\1" and name ~= "." and name ~= ".." then
|
||||
table.insert(out, { name = name, isDirectory = r.isDirectory, extent = r.extent, size = r.size })
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
local function splitPath(path)
|
||||
local parts = {}
|
||||
for part in path:gmatch('[^/\\]+') do parts[#parts+1]=part end
|
||||
return parts
|
||||
end
|
||||
|
||||
function ISO:findEntryByPath(path)
|
||||
if not self.pvd then self:readPVD() end
|
||||
local parts = splitPath(path)
|
||||
local curExtent = self.pvd.root.extent
|
||||
local curSize = self.pvd.root.size
|
||||
for i, part in ipairs(parts) do
|
||||
-- read directory entries for curExtent
|
||||
local buffer = readBytes(self.f, curExtent * SECTOR_SIZE, curSize)
|
||||
local records = parseDirRecords(buffer, curSize)
|
||||
local found = nil
|
||||
for _, r in ipairs(records) do
|
||||
local name = trimVersion(r.fileIdentifier)
|
||||
if name:lower() == part:lower() then
|
||||
found = r; break
|
||||
end
|
||||
end
|
||||
if not found then return nil, "path not found: " .. part end
|
||||
if i < #parts then
|
||||
if not found.isDirectory then return nil, "not a directory: "..part end
|
||||
curExtent = found.extent
|
||||
curSize = found.size
|
||||
else
|
||||
return found
|
||||
end
|
||||
end
|
||||
return nil, "empty path"
|
||||
end
|
||||
|
||||
function ISO:readFile(path)
|
||||
local rec, err = self:findEntryByPath(path)
|
||||
if not rec then return nil, err end
|
||||
if rec.isDirectory then return nil, "path is a directory" end
|
||||
local pos = rec.extent * SECTOR_SIZE
|
||||
return readBytes(self.f, pos, rec.size)
|
||||
end
|
||||
|
||||
-- return module
|
||||
return M
|
||||
Reference in New Issue
Block a user