/* This file is part of jpcsp. Jpcsp is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Jpcsp is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Jpcsp. If not, see <http://www.gnu.org/licenses/>. */ package jpcsp.filesystems.umdiso; import static jpcsp.filesystems.umdiso.UmdIsoFile.sectorLength; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.io.RandomAccessFile; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import jpcsp.Emulator; import jpcsp.filesystems.umdiso.iso9660.Iso9660Directory; import jpcsp.filesystems.umdiso.iso9660.Iso9660File; import jpcsp.filesystems.umdiso.iso9660.Iso9660Handler; import jpcsp.settings.Settings; import jpcsp.util.Utilities; /** * * @author gigaherz, gid15 */ public class UmdIsoReader implements IBrowser { public static final int startSector = 16; public static final int startSectorJoliet = 17; private static final int headerLength = 24; private ISectorDevice sectorDevice; private IBrowser browser; private final HashMap<String, Iso9660File> fileCache = new HashMap<String, Iso9660File>(); private final HashMap<String, Iso9660Directory> dirCache = new HashMap<String, Iso9660Directory>(); private int numSectors; private static boolean doIsoBuffering = false; private boolean hasJolietExtension; public UmdIsoReader(String umdFilename) throws IOException, FileNotFoundException { init(umdFilename, doIsoBuffering); } public UmdIsoReader(String umdFilename, boolean doIsoBuffering) throws IOException, FileNotFoundException { init(umdFilename, doIsoBuffering); } private void init(String umdFilename, boolean doIsoBuffering) throws IOException, FileNotFoundException { if (umdFilename == null && doIsoBuffering) { sectorDevice = null; } else { RandomAccessFile fileReader = new RandomAccessFile(umdFilename, "r"); byte[] header = new byte[headerLength]; fileReader.seek(0); fileReader.read(header); fileReader.seek(0); if (header[0] == 'C' && header[1] == 'I' && header[2] == 'S' && header[3] == 'O') { sectorDevice = new CSOFileSectorDevice(fileReader, header); } else if (header[0] == 0 && header[1] == 'P' && header[2] == 'B' && header[3] == 'P') { sectorDevice = new PBPFileSectorDevice(fileReader); } else { sectorDevice = new ISOFileSectorDevice(fileReader); } } if (doIsoBuffering) { String tmp = Settings.getInstance().getTmpDirectory(); sectorDevice = new BufferedFileSectorDevice(new RandomAccessFile(tmp + "umdbuffer.toc", "rw"), new RandomAccessFile(tmp + "umdbuffer.iso", "rw"), sectorDevice); } numSectors = sectorDevice.getNumSectors(); setBrowser(); if (browser == null && !hasIsoHeader()) { throw new IOException(String.format("Unsupported file format or corrupted file '%s'.", umdFilename)); } } public UmdIsoReader(ISectorDevice sectorDevice) throws IOException { this.sectorDevice = sectorDevice; numSectors = sectorDevice.getNumSectors(); setBrowser(); } private void setBrowser() { if (sectorDevice instanceof IBrowser) { browser = (IBrowser) sectorDevice; } else { browser = null; } } public void close() throws IOException { sectorDevice.close(); } private boolean hasIsoHeader() throws IOException { if (numSectors <= 0) { return false; } UmdIsoFile f = new UmdIsoFile(this, startSector, sectorLength, null, null); byte[] header = new byte[6]; int length = f.read(header); f.close(); if (length < header.length) { return false; } if (header[1] != 'C' || header[2] != 'D' || header[3] != '0' || header[4] != '0' || header[5] != '1') { return false; } hasJolietExtension = false; f = new UmdIsoFile(this, startSectorJoliet, sectorLength, null, null); length = f.read(header); f.close(); if (length == header.length) { if (header[0] == 2 && header[1] == 'C' && header[2] == 'D' && header[3] == '0' && header[4] == '0' && header[5] == '1') { hasJolietExtension = true; } } return true; } public boolean hasJolietExtension() { return hasJolietExtension; } public int getNumSectors() { return numSectors; } /** * Read sequential sectors into a byte array * * @param sectorNumber - the first sector to be read * @param numberSectors - the number of sectors to be read * @param buffer - the byte array where to write the sectors * @param offset - offset into the byte array where to start writing * @return the number of sectors read * @throws IOException */ public int readSectors(int sectorNumber, int numberSectors, byte[] buffer, int offset) throws IOException { if (sectorNumber < 0 || (sectorNumber + numberSectors) > numSectors) { Arrays.fill(buffer, offset, offset + numberSectors * sectorLength, (byte) 0); Emulator.log.warn(String.format("Sectors start=%d, end=%d out of ISO (numSectors=%d)", sectorNumber, sectorNumber + numberSectors, numSectors)); return numberSectors; } return sectorDevice.readSectors(sectorNumber, numberSectors, buffer, offset); } /** * Read one sector into a byte array * * @param sectorNumber - the sector number to be read * @param buffer - the byte array where to write * @param offset - offset into the byte array where to start writing * @throws IOException */ public void readSector(int sectorNumber, byte[] buffer, int offset) throws IOException { if (sectorNumber < 0 || sectorNumber >= numSectors) { Arrays.fill(buffer, offset, offset + sectorLength, (byte) 0); Emulator.log.warn(String.format("Sector number %d out of ISO (numSectors=%d)", sectorNumber, numSectors)); return; } sectorDevice.readSector(sectorNumber, buffer, offset); } /** * Read one sector * * @param sectorNumber - the sector number to be read * @return a new byte array of size sectorLength containing the sector * @throws IOException */ public byte[] readSector(int sectorNumber) throws IOException { return readSector(sectorNumber, null); } /** * Read one sector * * @param sectorNumber - the sector number to be read * @param buffer - try to reuse this buffer if possible * @return a new byte array of size sectorLength containing the sector or * the buffer if it could be reused. * @throws IOException */ public byte[] readSector(int sectorNumber, byte[] buffer) throws IOException { if (buffer == null || buffer.length != sectorLength) { buffer = new byte[sectorLength]; } readSector(sectorNumber, buffer, 0); return buffer; } private int removePath(String[] path, int index, int length) { if (index < 0 || index >= length) { return length; } for (int i = index + 1; i < length; i++) { path[i - 1] = path[i]; } return length - 1; } private Iso9660File getFileEntry(String filePath) throws IOException, FileNotFoundException { Iso9660File info; info = fileCache.get(filePath); if (info != null) { return info; } int parentDirectoryIndex = filePath.lastIndexOf('/'); if (parentDirectoryIndex >= 0) { String parentDirectory = filePath.substring(0, parentDirectoryIndex); Iso9660Directory dir = dirCache.get(parentDirectory); if (dir != null) { int index = dir.getFileIndex(filePath.substring(parentDirectoryIndex + 1)); info = dir.getEntryByIndex(index); if (info != null) { fileCache.put(filePath, info); return info; } } } Iso9660Directory dir = new Iso9660Handler(this); String[] path = filePath.split("[\\/]"); // First convert the path to a canonical path by removing all the // occurrences of "." and "..". int pathLength = path.length; for (int i = 0; i < pathLength;) { if (path[i].equals(".")) { // Remove "." pathLength = removePath(path, i, pathLength); } else if (path[i].equals("..")) { // Remove ".." and its parent pathLength = removePath(path, i, pathLength); pathLength = removePath(path, i - 1, pathLength); } else { i++; } } // walk through the canonical path for (int i = 0; i < pathLength;) { int index = dir.getFileIndex(path[i]); info = dir.getEntryByIndex(index); if (isDirectory(info)) { dir = new Iso9660Directory(this, info.getLBA(), info.getSize()); StringBuilder dirPath = new StringBuilder(path[0]); for (int j = 1; j <= i; j++) { dirPath.append("/").append(path[j]); } dirCache.put(dirPath.toString(), dir); } i++; } if (info != null) { fileCache.put(filePath, info); } return info; } public UmdIsoFile getFile(String filePath) throws IOException, FileNotFoundException { if (numSectors == 0) { throw new FileNotFoundException(filePath); } int fileStart; long fileLength; Date timestamp; String fileName = null; if (filePath != null && filePath.startsWith("sce_lbn")) { // // Direct sector access on UMD is using the following file name syntax: // sce_lbnSSSS_sizeLLLL // where SSSS is the index of the first sector (in hexadecimal) // LLLL is the length in bytes (in hexadecimal) // The prefix "0x" before each hexadecimal value is optional. // // E.g. // disc0:/sce_lbn0x5fa0_size0x1428 // disc0:/sce_lbn7050_sizeee850 // // Remark: SSSS and LLLL can be followed by any non-hex characters. // These additional characters are simply ignored. // filePath = filePath.substring(7); int sep = filePath.indexOf("_size"); fileStart = (int) Utilities.parseHexLong(filePath.substring(0, sep), true); fileLength = Utilities.parseHexLong(filePath.substring(sep + 5), true); timestamp = new Date(); fileName = null; if (fileStart < 0 || fileStart >= numSectors) { throw new IOException("File '" + filePath + "': Invalid Start Sector"); } } else if (filePath != null && filePath.length() == 0) { fileStart = 0; fileLength = ((long) numSectors) * sectorLength; timestamp = new Date(); } else { Iso9660File info = getFileEntry(filePath); if (info != null && isDirectory(info)) { info = null; } if (info == null) { throw new FileNotFoundException("File '" + filePath + "' not found or not a file."); } fileStart = info.getLBA(); fileLength = info.getSize(); timestamp = info.getTimestamp(); fileName = info.getFileName(); } return new UmdIsoFile(this, fileStart, fileLength, timestamp, fileName); } public boolean hasFile(String filePath) { try { UmdIsoFile umdIsoFile = getFile(filePath); if (umdIsoFile != null) { umdIsoFile.close(); return true; } } catch (FileNotFoundException e) { } catch (IOException e) { } return false; } public String resolveSectorPath(int start, long length) { String fileName = null; // Scroll back through the sectors until the file's start sector is reached // and it's name can be obtained. while ((fileName == null) || (start <= startSector)) { fileName = getFileName(start); start--; } return fileName; } public String[] listDirectory(String filePath) throws IOException, FileNotFoundException { Iso9660Directory dir = null; if (filePath.length() == 0) { dir = new Iso9660Handler(this); } else { Iso9660File info = getFileEntry(filePath); if (info != null && isDirectory(info)) { dir = new Iso9660Directory(this, info.getLBA(), info.getSize()); } } if (dir == null) { throw new FileNotFoundException("File '" + filePath + "' not found or not a directory."); } return dir.getFileList(); } public int getFileProperties(String filePath) throws IOException, FileNotFoundException { if (filePath.length() == 0) { return 2; } Iso9660File info = getFileEntry(filePath); if (info == null) { throw new FileNotFoundException("File '" + filePath + "' not found."); } return info.getProperties(); } public boolean isDirectory(String filePath) throws IOException, FileNotFoundException { return ((getFileProperties(filePath) & 2) == 2); } public boolean isDirectory(Iso9660File file) { return (file.getProperties() & 2) == 2; } private String getFileNameRecursive(int fileStartSector, String path, String[] files) throws FileNotFoundException, IOException { for (String file : files) { String filePath = path + "/" + file; Iso9660File info = null; if (path.length() == 0) { filePath = file; } else { info = getFileEntry(filePath); if (info != null) { if (info.getLBA() == fileStartSector) { return info.getFileName(); } } } if ((info == null || isDirectory(info)) && !file.equals(".") && !file.equals("\01")) { try { String[] childFiles = listDirectory(filePath); String fileName = getFileNameRecursive(fileStartSector, filePath, childFiles); if (fileName != null) { return fileName; } } catch (FileNotFoundException e) { // Continue } } } return null; } public String getFileName(int fileStartSector) { try { String[] files = listDirectory(""); return getFileNameRecursive(fileStartSector, "", files); } catch (FileNotFoundException e) { // Ignore Exception } catch (IOException e) { // Ignore Exception } return null; } public long dumpIndexRecursive(PrintWriter out, String path, String[] files) throws IOException { long size = 0; for (String file : files) { String filePath = path + "/" + file; Iso9660File info; int fileStart = 0; long fileLength = 0; if (path.length() == 0) { filePath = file; } info = getFileEntry(filePath); if (info != null) { fileStart = info.getLBA(); fileLength = info.getSize(); size += (fileLength + 0x7FF) & ~0x7FF; } // "." isn't a directory (throws an exception) // "\01" claims to be a directory but ends up in an infinite loop // ignore them here as they do not contribute much to the listing if (file.equals(".") || file.equals("\01")) { continue; } if (info == null || isDirectory(info)) { out.println(String.format("D %08X %10d %s", fileStart, fileLength, filePath)); String[] childFiles = listDirectory(filePath); size += dumpIndexRecursive(out, filePath, childFiles); } else { out.println(String.format(" %08X %10d %s", fileStart, fileLength, filePath)); } } return size; } public void dumpIndexFile(String filename) throws IOException, FileNotFoundException { PrintWriter out = new PrintWriter(new FileOutputStream(filename)); out.println(" Start Size Name"); String[] files = listDirectory(""); long totalSize = dumpIndexRecursive(out, "", files); long imageSize = ((long) numSectors) * sectorLength; out.println(String.format("Total Size %10d", totalSize)); out.println(String.format("Image Size %10d", imageSize)); out.println(String.format("Missing %10d (%d sectors)", imageSize - totalSize, numSectors - (totalSize / sectorLength))); out.close(); } public static void setDoIsoBuffering(boolean doIsoBuffering) { UmdIsoReader.doIsoBuffering = doIsoBuffering; } private byte[] readFile(String fileName) throws IOException { if (!hasFile(fileName)) { return null; } UmdIsoFile file = getFile(fileName); int length = (int) file.length(); byte[] buffer = new byte[length]; int read = file.read(buffer); if (read < 0) { return null; } // Read less than expected? if (read < length) { // Shrink the buffer to the read size byte[] newBuffer = new byte[read]; System.arraycopy(buffer, 0, newBuffer, 0, read); buffer = newBuffer; } return buffer; } @Override public byte[] readParamSFO() throws IOException { if (browser != null) { return browser.readParamSFO(); } return readFile("PSP_GAME/PARAM.SFO"); } @Override public byte[] readIcon0() throws IOException { if (browser != null) { return browser.readIcon0(); } return readFile("PSP_GAME/ICON0.PNG"); } @Override public byte[] readIcon1() throws IOException { if (browser != null) { return browser.readIcon1(); } return readFile("PSP_GAME/ICON1.PNG"); } @Override public byte[] readPic0() throws IOException { if (browser != null) { return browser.readPic0(); } return readFile("PSP_GAME/PIC0.PNG"); } @Override public byte[] readPic1() throws IOException { if (browser != null) { return browser.readPic1(); } return readFile("PSP_GAME/PIC1.PNG"); } @Override public byte[] readSnd0() throws IOException { if (browser != null) { return browser.readSnd0(); } return readFile("PSP_GAME/SND0.AT3"); } @Override public byte[] readPspData() throws IOException { if (browser != null) { return browser.readPspData(); } return null; } @Override public byte[] readPsarData() throws IOException { if (browser != null) { return browser.readPsarData(); } return null; } }