/*
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.HLE.VFS.local;
import static jpcsp.HLE.kernel.types.SceKernelErrors.ERROR_MEMSTICK_DEVCTL_BAD_PARAMS;
import static jpcsp.HLE.modules.IoFileMgrForUser.PSP_O_CREAT;
import static jpcsp.HLE.modules.IoFileMgrForUser.PSP_O_EXCL;
import static jpcsp.HLE.modules.IoFileMgrForUser.PSP_O_RDWR;
import static jpcsp.HLE.modules.IoFileMgrForUser.PSP_O_TRUNC;
import static jpcsp.HLE.modules.IoFileMgrForUser.PSP_O_WRONLY;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Map;
import jpcsp.Memory;
import jpcsp.HLE.Modules;
import jpcsp.HLE.SceKernelErrorException;
import jpcsp.HLE.TPointer;
import jpcsp.HLE.VFS.AbstractVirtualFileSystem;
import jpcsp.HLE.VFS.IVirtualFile;
import jpcsp.HLE.kernel.types.SceIoDirent;
import jpcsp.HLE.kernel.types.SceIoStat;
import jpcsp.HLE.kernel.types.SceKernelErrors;
import jpcsp.HLE.kernel.types.SceKernelThreadInfo;
import jpcsp.HLE.kernel.types.ScePspDateTime;
import jpcsp.HLE.modules.IoFileMgrForUser;
import jpcsp.HLE.modules.ThreadManForUser;
import jpcsp.HLE.modules.IoFileMgrForUser.IoOperation;
import jpcsp.HLE.modules.IoFileMgrForUser.IoOperationTiming;
import jpcsp.filesystems.SeekableRandomFile;
import jpcsp.hardware.MemoryStick;
public class LocalVirtualFileSystem extends AbstractVirtualFileSystem {
protected final String localPath;
private final boolean useDirExtendedInfo;
// modeStrings indexed by [0, PSP_O_RDONLY, PSP_O_WRONLY, PSP_O_RDWR]
// SeekableRandomFile doesn't support write only: take "rw",
private final static String[] modeStrings = {"r", "r", "rw", "rw"};
/**
* Get the file name as returned from the memory stick.
* In some cases, the name is uppercased.
*
* The following cases have been tested:
* - "a" => "A"
* - "B" => "B"
* - "b.txt" => "B.TXT"
* - "cC" => "cC"
* - "LongFileName.txt" => "LongFileName.txt"
* - "aaaaaaaa" => "AAAAAAAA"
* - "aaaaaaaa.aaa" => "AAAAAAAA.AAA"
* - "aaaaaaaaa" => "aaaaaaaaa"
* - "aaaaaaaa.aaaa" => "aaaaaaaa.aaaa"
*
* It seems that file names in the format 8.3 only containing lowercase characters
* are converted to uppercase characters.
*/
public static String getMsFileName(String fileName) {
if (fileName == null) {
return fileName;
}
if (fileName.matches("[^A-Z]{1,8}(\\.[^A-Z]{1,3})?")) {
return fileName.toUpperCase();
}
return fileName;
}
public LocalVirtualFileSystem(String localPath, boolean useDirExtendedInfo) {
this.localPath = localPath;
this.useDirExtendedInfo = useDirExtendedInfo;
}
protected File getFile(String fileName) {
return new File(localPath + fileName);
}
protected static String getMode(int mode) {
return modeStrings[mode & PSP_O_RDWR];
}
@Override
public IVirtualFile ioOpen(String fileName, int flags, int mode) {
File file = getFile(fileName);
if (file.exists() && hasFlag(flags, PSP_O_CREAT) && hasFlag(flags, PSP_O_EXCL)) {
if (log.isDebugEnabled()) {
log.debug("hleIoOpen - file already exists (PSP_O_CREAT + PSP_O_EXCL)");
}
throw new SceKernelErrorException(SceKernelErrors.ERROR_ERRNO_FILE_ALREADY_EXISTS);
}
// When PSP_O_CREAT is specified, create the parent directories
// if they do not yet exist.
if (!file.exists() && hasFlag(flags, PSP_O_CREAT)) {
String parentDir = file.getParent();
new File(parentDir).mkdirs();
}
SeekableRandomFile raf;
try {
raf = new SeekableRandomFile(file, getMode(flags));
} catch (FileNotFoundException e) {
return null;
}
LocalVirtualFile localVirtualFile = new LocalVirtualFile(raf);
if (hasFlag(flags, PSP_O_WRONLY) && hasFlag(flags, PSP_O_TRUNC)) {
// When writing, PSP_O_TRUNC truncates the file at the position of the first write.
// E.g.:
// open(PSP_O_TRUNC)
// seek(0x1000)
// write() -> truncates the file at the position 0x1000 before writing
localVirtualFile.setTruncateAtNextWrite(true);
}
return localVirtualFile;
}
@Override
public int ioGetstat(String fileName, SceIoStat stat) {
File file = getFile(fileName);
if (!file.exists()) {
return SceKernelErrors.ERROR_ERRNO_FILE_NOT_FOUND;
}
// Set attr (dir/file) and copy into mode
int attr = 0;
if (file.isDirectory()) {
attr |= 0x10;
}
if (file.isFile()) {
attr |= 0x20;
}
int mode = (file.canRead() ? 4 : 0) + (file.canWrite() ? 2 : 0) + (file.canExecute() ? 1 : 0);
// Octal extend into user and group
mode = mode + (mode << 3) + (mode << 6);
mode |= attr << 8;
// Java can't see file create/access time
ScePspDateTime ctime = ScePspDateTime.fromUnixTime(file.lastModified());
ScePspDateTime atime = ScePspDateTime.fromUnixTime(0);
ScePspDateTime mtime = ScePspDateTime.fromUnixTime(file.lastModified());
stat.init(mode, attr, file.length(), ctime, atime, mtime);
return 0;
}
@Override
public int ioRemove(String name) {
File file = getFile(name);
if (!file.delete()) {
return IO_ERROR;
}
return 0;
}
@Override
public String[] ioDopen(String dirName) {
File file = getFile(dirName);
if (!file.isDirectory()) {
if (file.exists()) {
log.warn(String.format("ioDopen file '%s' is not a directory", dirName));
} else {
log.warn(String.format("ioDopen directory '%s' not found", dirName));
}
return null;
}
String files[] = file.list();
if (files != null) {
for (int i = 0; i < files.length; i++) {
files[i] = getMsFileName(files[i]);
}
}
return files;
}
@Override
public int ioDread(String dirName, SceIoDirent dir) {
if (dir != null) {
// Use ExtendedInfo for the MemoryStick
dir.setUseExtendedInfo(useDirExtendedInfo);
}
return super.ioDread(dirName, dir);
}
@Override
public int ioMkdir(String name, int mode) {
File file = getFile(name);
if (file.exists()) {
return SceKernelErrors.ERROR_ERRNO_FILE_ALREADY_EXISTS;
}
if (!file.mkdir()) {
return IO_ERROR;
}
return 0;
}
@Override
public int ioRmdir(String name) {
File file = getFile(name);
if (!file.exists()) {
return SceKernelErrors.ERROR_ERRNO_FILE_NOT_FOUND;
}
if (!file.delete()) {
return IO_ERROR;
}
return 0;
}
@Override
public int ioChstat(String fileName, SceIoStat stat, int bits) {
File file = getFile(fileName);
int mode = stat.mode;
boolean successful = true;
if ((bits & 0x0001) != 0) { // Others execute permission
if (!file.isDirectory() && !file.setExecutable((mode & 0x0001) != 0)) {
successful = false;
}
}
if ((bits & 0x0002) != 0) { // Others write permission
if (!file.setWritable((mode & 0x0002) != 0)) {
successful = false;
}
}
if ((bits & 0x0004) != 0) { // Others read permission
if (!file.setReadable((mode & 0x0004) != 0)) {
successful = false;
}
}
if ((bits & 0x0040) != 0) { // User execute permission
if (!file.setExecutable((mode & 0x0040) != 0, true)) {
successful = false;
}
}
if ((bits & 0x0080) != 0) { // User write permission
if (!file.setWritable((mode & 0x0080) != 0, true)) {
successful = false;
}
}
if ((bits & 0x0100) != 0) { // User read permission
if (!file.setReadable((mode & 0x0100) != 0, true)) {
successful = false;
}
}
return successful ? 0 : IO_ERROR;
}
@Override
public int ioRename(String oldFileName, String newFileName) {
File oldFile = getFile(oldFileName);
File newFile = getFile(newFileName);
if (log.isDebugEnabled()) {
log.debug(String.format("ioRename: renaming file '%s' to '%s'", oldFileName, newFileName));
}
if (!oldFile.renameTo(newFile)) {
log.warn(String.format("ioRename failed: '%s' to '%s'", oldFileName, newFileName));
return IO_ERROR;
}
return 0;
}
@Override
public int ioDevctl(String deviceName, int command, TPointer inputPointer, int inputLength, TPointer outputPointer, int outputLength) {
int result;
switch (command) {
// Register memorystick insert/eject callback (fatms0).
case 0x02415821: {
log.debug("sceIoDevctl register memorystick insert/eject callback (fatms0)");
ThreadManForUser threadMan = Modules.ThreadManForUserModule;
if (!deviceName.equals("fatms0:")) {
result = ERROR_MEMSTICK_DEVCTL_BAD_PARAMS;
} else if (inputPointer.isAddressGood() && inputLength == 4) {
int cbid = inputPointer.getValue32();
final int callbackType = SceKernelThreadInfo.THREAD_CALLBACK_MEMORYSTICK_FAT;
if (threadMan.hleKernelRegisterCallback(callbackType, cbid)) {
// Trigger the registered callback immediately.
// Only trigger this one callback, not all the MS callbacks.
threadMan.hleKernelNotifyCallback(callbackType, cbid, MemoryStick.getStateFatMs());
result = 0; // Success.
} else {
result = SceKernelErrors.ERROR_ERRNO_INVALID_ARGUMENT;
}
} else {
result = SceKernelErrors.ERROR_ERRNO_INVALID_ARGUMENT;
}
break;
}
// Unregister memorystick insert/eject callback (fatms0).
case 0x02415822: {
log.debug("sceIoDevctl unregister memorystick insert/eject callback (fatms0)");
ThreadManForUser threadMan = Modules.ThreadManForUserModule;
if (!deviceName.equals("fatms0:")) {
result = ERROR_MEMSTICK_DEVCTL_BAD_PARAMS;
} else if (inputPointer.isAddressGood() && inputLength == 4) {
int cbid = inputPointer.getValue32();
threadMan.hleKernelUnRegisterCallback(SceKernelThreadInfo.THREAD_CALLBACK_MEMORYSTICK_FAT, cbid);
result = 0; // Success.
} else {
result = SceKernelErrors.ERROR_ERRNO_INVALID_ARGUMENT;
}
break;
}
// Set if the device is assigned/inserted or not (fatms0).
case 0x02415823: {
log.debug("sceIoDevctl set assigned device (fatms0)");
if (!deviceName.equals("fatms0:")) {
result = ERROR_MEMSTICK_DEVCTL_BAD_PARAMS;
} else if (inputPointer.isAddressGood() && inputLength >= 4) {
// 0 - Device is not assigned (callback not registered).
// 1 - Device is assigned (callback registered).
MemoryStick.setStateFatMs(inputPointer.getValue32());
result = 0;
} else {
result = IO_ERROR;
}
break;
}
// Check if the device is write protected (fatms0).
case 0x02425824: {
log.debug("sceIoDevctl check write protection (fatms0)");
if (!deviceName.equals("fatms0:") && !deviceName.equals("ms0:")) { // For this command the alias "ms0:" is also supported.
result = ERROR_MEMSTICK_DEVCTL_BAD_PARAMS;
} else if (outputPointer.isAddressGood()) {
// 0 - Device is not protected.
// 1 - Device is protected.
outputPointer.setValue32(0);
result = 0;
} else {
result = IO_ERROR;
}
break;
}
// Get MS capacity (fatms0).
case 0x02425818: {
log.debug("sceIoDevctl get MS capacity (fatms0)");
int sectorSize = 0x200;
int sectorCount = MemoryStick.getSectorSize() / sectorSize;
int maxClusters = (int) ((MemoryStick.getFreeSize() * 95L / 100) / (sectorSize * sectorCount));
int freeClusters = maxClusters;
int maxSectors = maxClusters;
if (inputPointer.isAddressGood() && inputLength >= 4) {
int addr = inputPointer.getValue32();
if (Memory.isAddressGood(addr)) {
log.debug("sceIoDevctl refer ms free space");
Memory mem = Memory.getInstance();
mem.write32(addr, maxClusters);
mem.write32(addr + 4, freeClusters);
mem.write32(addr + 8, maxSectors);
mem.write32(addr + 12, sectorSize);
mem.write32(addr + 16, sectorCount);
result = 0;
} else {
log.warn("sceIoDevctl 0x02425818 bad save address " + String.format("0x%08X", addr));
result = IO_ERROR;
}
} else {
log.warn("sceIoDevctl 0x02425818 bad param address " + String.format("0x%08X", inputPointer) + " or size " + inputLength);
result = IO_ERROR;
}
break;
}
// Check if the device is assigned/inserted (fatms0).
case 0x02425823: {
log.debug("sceIoDevctl check assigned device (fatms0)");
if (!deviceName.equals("fatms0:")) {
result = ERROR_MEMSTICK_DEVCTL_BAD_PARAMS;
} else if (outputPointer.isAddressGood() && outputLength >= 4) {
// 0 - Device is not assigned (callback not registered).
// 1 - Device is assigned (callback registered).
outputPointer.setValue32(MemoryStick.getStateFatMs());
result = 0;
} else {
result = IO_ERROR;
}
break;
}
case 0x00005802: {
if (!"flash1:".equals(deviceName) || inputLength != 0 || outputLength != 0) {
result = IO_ERROR;
} else {
result = 0;
}
break;
}
default: {
result = super.ioDevctl(deviceName, command, inputPointer, inputLength, outputPointer, outputLength);
}
}
return result;
}
@Override
public Map<IoOperation, IoOperationTiming> getTimings() {
return IoFileMgrForUser.noDelayTimings;
}
}