Fixed bugs in the mass-storage implementation. Directories can be mounted via drag/drop, file information is now property reported including dates, and now large files >128k work correctly.

This commit is contained in:
Brendan Robert 2015-12-20 12:45:56 -06:00
parent cf87f30e35
commit 4021af3ac6
7 changed files with 111 additions and 78 deletions

View File

@ -123,7 +123,7 @@ public class JaceUIController {
return null; return null;
} }
for (File f : files) { for (File f : files) {
if (f.isFile()) return f; if (f.exists()) return f;
} }
return null; return null;
} }

View File

@ -30,11 +30,14 @@ import java.util.logging.Logger;
/** /**
* Prodos directory node * Prodos directory node
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com *
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/ */
public class DirectoryNode extends DiskNode implements FileFilter { public class DirectoryNode extends DiskNode implements FileFilter {
// public static int FILE_ENTRY_SIZE = 38; // public static int FILE_ENTRY_SIZE = 38;
public static int FILE_ENTRY_SIZE = 0x027; public static int FILE_ENTRY_SIZE = 0x027;
public DirectoryNode(ProdosVirtualDisk ownerFilesystem, File physicalDir, int baseBlock) throws IOException { public DirectoryNode(ProdosVirtualDisk ownerFilesystem, File physicalDir, int baseBlock) throws IOException {
setBaseBlock(baseBlock); setBaseBlock(baseBlock);
init(ownerFilesystem, physicalDir); init(ownerFilesystem, physicalDir);
@ -44,7 +47,6 @@ public class DirectoryNode extends DiskNode implements FileFilter {
init(ownerFilesystem, physicalDir); init(ownerFilesystem, physicalDir);
} }
private void init(ProdosVirtualDisk ownerFilesystem, File physicalFile) throws IOException { private void init(ProdosVirtualDisk ownerFilesystem, File physicalFile) throws IOException {
setPhysicalFile(physicalFile); setPhysicalFile(physicalFile);
setType(EntryType.SUBDIRECTORY); setType(EntryType.SUBDIRECTORY);
@ -78,7 +80,8 @@ public class DirectoryNode extends DiskNode implements FileFilter {
@Override @Override
/** /**
* Checks contents of subdirectory for changes as well as directory itself (super class) * Checks contents of subdirectory for changes as well as directory itself
* (super class)
*/ */
public boolean checkFile() throws IOException { public boolean checkFile() throws IOException {
boolean success = true; boolean success = true;
@ -90,7 +93,7 @@ public class DirectoryNode extends DiskNode implements FileFilter {
for (File f : realFileList) { for (File f : realFileList) {
realFiles.add(f.getName()); realFiles.add(f.getName());
} }
for (Iterator<DiskNode> i = getChildren().iterator(); i.hasNext(); ) { for (Iterator<DiskNode> i = getChildren().iterator(); i.hasNext();) {
DiskNode node = i.next(); DiskNode node = i.next();
if (realFiles.contains(node.getPhysicalFile().getName())) { if (realFiles.contains(node.getPhysicalFile().getName())) {
realFiles.remove(node.getPhysicalFile().getName()); realFiles.remove(node.getPhysicalFile().getName());
@ -119,14 +122,15 @@ public class DirectoryNode extends DiskNode implements FileFilter {
checkFile(); checkFile();
if (block == 0) { if (block == 0) {
generateHeader(buffer); generateHeader(buffer);
for (int i=0; i < 12 && i < children.size(); i++) for (int i = 0; i < 12 && i < children.size(); i++) {
generateFileEntry(buffer, 4 + (i+1) * FILE_ENTRY_SIZE, i); generateFileEntry(buffer, 4 + (i + 1) * FILE_ENTRY_SIZE, i);
}
} else { } else {
int start = (block * 13) - 1; int start = (block * 13) - 1;
int end = start + 13; int end = start + 13;
int offset = 4; int offset = 4;
for (int i=start; i < end && i < children.size(); i++) { for (int i = start; i < end && i < children.size(); i++) {
// TODO: Add any parts that are not file entries. // TODO: Add any parts that are not file entries.
generateFileEntry(buffer, offset, i); generateFileEntry(buffer, offset, i);
offset += FILE_ENTRY_SIZE; offset += FILE_ENTRY_SIZE;
@ -136,7 +140,9 @@ public class DirectoryNode extends DiskNode implements FileFilter {
@Override @Override
public boolean accept(File file) { public boolean accept(File file) {
if (file.getName().endsWith("~")) return false; if (file.getName().endsWith("~")) {
return false;
}
char c = file.getName().charAt(0); char c = file.getName().charAt(0);
if (c == '.' || c == '~') { if (c == '.' || c == '~') {
return false; return false;
@ -146,24 +152,27 @@ public class DirectoryNode extends DiskNode implements FileFilter {
/** /**
* Generate the directory header found in the base block of a directory * Generate the directory header found in the base block of a directory
*
* @param buffer where to write data * @param buffer where to write data
*/ */
@SuppressWarnings("static-access") @SuppressWarnings("static-access")
private void generateHeader(byte[] buffer) { private void generateHeader(byte[] buffer) {
// System.out.println("Generating directory header"); // System.out.println("Generating directory header");
// Previous block = 0 // Previous block = 0
generateWord(buffer, 0,0); generateWord(buffer, 0, 0);
// Next block // Next block
int nextBlock = 0; int nextBlock = 0;
if (!additionalNodes.isEmpty()) if (!additionalNodes.isEmpty()) {
nextBlock = additionalNodes.get(0).baseBlock; nextBlock = additionalNodes.get(0).baseBlock;
}
generateWord(buffer, 0x02, nextBlock); generateWord(buffer, 0x02, nextBlock);
// Directory header + name length // Directory header + name length
// Volumme header = 0x0f0; Subdirectory header = 0x0e0 // Volumme header = 0x0f0; Subdirectory header = 0x0e0
buffer[4]= (byte) ((baseBlock == 0x02 ? 0x0f0 : 0x0E0) + getName().length()); buffer[4] = (byte) ((baseBlock == 0x02 ? 0x0f0 : 0x0E0) + getName().length());
generateName(buffer, 5, this); generateName(buffer, 5, this);
for (int i=0x014 ; i <= 0x01b; i++) for (int i = 0x014; i <= 0x01b; i++) {
buffer[i] = 0; buffer[i] = 0;
}
generateTimestamp(buffer, 0x01c, getPhysicalFile().lastModified()); generateTimestamp(buffer, 0x01c, getPhysicalFile().lastModified());
// Prodos 1.9 // Prodos 1.9
buffer[0x020] = 0x019; buffer[0x020] = 0x019;
@ -185,6 +194,7 @@ public class DirectoryNode extends DiskNode implements FileFilter {
/** /**
* Generate the entry of a directory * Generate the entry of a directory
*
* @param buffer where to write data * @param buffer where to write data
* @param offset starting offset in buffer to write * @param offset starting offset in buffer to write
* @param fileNumber number of file (indexed in Children array) to write * @param fileNumber number of file (indexed in Children array) to write
@ -192,17 +202,17 @@ public class DirectoryNode extends DiskNode implements FileFilter {
private void generateFileEntry(byte[] buffer, int offset, int fileNumber) throws IOException { private void generateFileEntry(byte[] buffer, int offset, int fileNumber) throws IOException {
// System.out.println("Generating entry for "+children.get(fileNumber).getName()); // System.out.println("Generating entry for "+children.get(fileNumber).getName());
DiskNode child = children.get(fileNumber); DiskNode child = children.get(fileNumber);
child.allocate();
// Entry Type and length // Entry Type and length
buffer[offset] = (byte) ((child.getType().code << 4) + child.getName().length()); buffer[offset] = (byte) ((child.getType().code << 4) + child.getName().length());
// Name // Name
generateName(buffer, offset+1, child); generateName(buffer, offset + 1, child);
// File type // File type
buffer[offset + 0x010] = (byte) ((child instanceof DirectoryNode) ? 0x0f : ((FileNode) child).fileType); buffer[offset + 0x010] = (byte) ((child instanceof DirectoryNode) ? 0x0f : ((FileNode) child).fileType);
// Key pointer // Key pointer
generateWord(buffer, offset + 0x011, child.getBaseBlock()); generateWord(buffer, offset + 0x011, child.getBaseBlock());
// Blocks used -- will report only one unless file is actually allocated // Blocks used -- will report only one unless file is actually allocated
// child.allocate(); generateWord(buffer, offset + 0x013, child.additionalNodes.size());
generateWord(buffer, offset + 0x013, 1 + child.additionalNodes.size());
// EOF // EOF
// TODO: Verify this is the right thing to do -- is EOF total length or a modulo? // TODO: Verify this is the right thing to do -- is EOF total length or a modulo?
int length = ((int) child.physicalFile.length()) & 0x0ffffff; int length = ((int) child.physicalFile.length()) & 0x0ffffff;
@ -214,11 +224,12 @@ public class DirectoryNode extends DiskNode implements FileFilter {
buffer[offset + 0x01c] = 0x19; buffer[offset + 0x01c] = 0x19;
// Minimum version = 0 // Minimum version = 0
buffer[offset + 0x01d] = 0; buffer[offset + 0x01d] = 0;
// Access = all granted // Access = Read-only
buffer[offset + 0x01e] = (byte) 0x0ff; buffer[offset + 0x01e] = (byte) 0x001;
// AUX type // AUX type
if (child instanceof FileNode) if (child instanceof FileNode) {
generateWord(buffer, offset + 0x01f, ((FileNode) child).loadAddress); generateWord(buffer, offset + 0x01f, ((FileNode) child).loadAddress);
}
// Modification date // Modification date
generateTimestamp(buffer, offset + 0x021, child.physicalFile.lastModified()); generateTimestamp(buffer, offset + 0x021, child.physicalFile.lastModified());
// Key pointer for directory // Key pointer for directory
@ -231,26 +242,22 @@ public class DirectoryNode extends DiskNode implements FileFilter {
// yyyyyyym mmmddddd - Byte 0,1 // yyyyyyym mmmddddd - Byte 0,1
// ---hhhhh --mmmmmm - Byte 2,3 // ---hhhhh --mmmmmm - Byte 2,3
// buffer[offset+1] = (byte) (((c.get(Calendar.YEAR) - 1990) << 1) + ((c.get(Calendar.MONTH)>> 3) & 1)); buffer[offset+1] = (byte) (((c.get(Calendar.YEAR) - 2000) << 1) | ((c.get(Calendar.MONTH)+1)>> 3));
buffer[offset+0] = 0;
buffer[offset+1] = 0;
buffer[offset+2] = 0;
buffer[offset+3] = 0;
// buffer[offset+2] = (byte) ((c.get(Calendar.MONTH)>> 3) & 1); // buffer[offset+2] = (byte) ((c.get(Calendar.MONTH)>> 3) & 1);
// buffer[offset+3] = (byte) (((c.get(Calendar.MONTH)&7) + c.get(Calendar.DAY_OF_MONTH)) & 0x0ff); buffer[offset+0] = (byte) (((((c.get(Calendar.MONTH)+1)&7)<<5) | c.get(Calendar.DAY_OF_MONTH)) & 0x0ff);
// buffer[offset+0] = (byte) c.get(Calendar.HOUR_OF_DAY); buffer[offset+3] = (byte) c.get(Calendar.HOUR_OF_DAY);
// buffer[offset+1] = (byte) c.get(Calendar.MINUTE); buffer[offset+2] = (byte) c.get(Calendar.MINUTE);
} }
private void generateWord(byte[] buffer, int i, int value) { private void generateWord(byte[] buffer, int i, int value) {
// Little endian format // Little endian format
buffer[i] = (byte) (value & 0x0ff); buffer[i] = (byte) (value & 0x0ff);
buffer[i+1] = (byte) ((value >> 8) & 0x0ff); buffer[i + 1] = (byte) ((value >> 8) & 0x0ff);
} }
private void generateName(byte[] buffer, int offset, DiskNode node) { private void generateName(byte[] buffer, int offset, DiskNode node) {
for (int i=0; i < node.getName().length(); i++) { for (int i = 0; i < node.getName().length(); i++) {
buffer[offset+i] = (byte) node.getName().charAt(i); buffer[offset + i] = (byte) node.getName().charAt(i);
} }
} }

View File

@ -100,7 +100,7 @@ public abstract class DiskNode {
} }
} }
public void refresh() { public void refresh() throws IOException {
ownerFilesystem.deallocateEntry(this); ownerFilesystem.deallocateEntry(this);
doRefresh(); doRefresh();
allocationTime = System.currentTimeMillis(); allocationTime = System.currentTimeMillis();

View File

@ -21,6 +21,7 @@ package jace.hardware.massStorage;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
/** /**
* Representation of a prodos file with a known file type and having a known * Representation of a prodos file with a known file type and having a known
@ -81,16 +82,18 @@ public class FileNode extends DiskNode {
} else if (fileSize <= SAPLING_MAX_SIZE) { } else if (fileSize <= SAPLING_MAX_SIZE) {
setType(EntryType.SAPLING); setType(EntryType.SAPLING);
return EntryType.SAPLING; return EntryType.SAPLING;
} else {
setType(EntryType.TREE);
return EntryType.TREE;
} }
setType(EntryType.TREE);
return EntryType.TREE;
} }
@Override @Override
public void setName(String name) { public void setName(String name) {
String[] parts = name.split("\\."); String[] parts = name.replaceAll("[^A-Za-z0-9]", ".").split("\\.");
FileType t = null; FileType t = FileType.UNKNOWN;
int offset = 0; int offset = 0;
String prodosName = name;
if (parts.length > 1) { if (parts.length > 1) {
String extension = parts[parts.length - 1].toUpperCase(); String extension = parts[parts.length - 1].toUpperCase();
String[] extParts = extension.split("#"); String[] extParts = extension.split("#");
@ -103,17 +106,14 @@ public class FileNode extends DiskNode {
} catch (IllegalArgumentException ex) { } catch (IllegalArgumentException ex) {
System.out.println("Not sure what extension " + extension + " is!"); System.out.println("Not sure what extension " + extension + " is!");
} }
name = ""; prodosName = "";
for (int i = 0; i < parts.length - 1; i++) { for (int i = 0; i < parts.length - 1; i++) {
name += (i > 0 ? "." + parts[i] : parts[i]); prodosName += (i > 0 ? "." + parts[i] : parts[i]);
} }
if (extParts[extParts.length - 1].equals("SYSTEM")) { if (extParts[extParts.length - 1].equals("SYSTEM")) {
name += ".SYSTEM"; prodosName += ".SYSTEM";
} }
} }
if (t == null) {
t = FileType.UNKNOWN;
}
if (offset == 0) { if (offset == 0) {
offset = t.defaultLoadAddress; offset = t.defaultLoadAddress;
} }
@ -121,12 +121,13 @@ public class FileNode extends DiskNode {
loadAddress = offset; loadAddress = offset;
// Pass usable name (stripped of file extension and other type info) as name // Pass usable name (stripped of file extension and other type info) as name
super.setName(name); super.setName(prodosName);
} }
public FileNode(ProdosVirtualDisk ownerFilesystem, File file) throws IOException { public FileNode(ProdosVirtualDisk ownerFilesystem, File file) throws IOException {
setOwnerFilesystem(ownerFilesystem); setOwnerFilesystem(ownerFilesystem);
setPhysicalFile(file); setPhysicalFile(file);
setName(file.getName());
} }
@Override @Override
@ -135,16 +136,19 @@ public class FileNode extends DiskNode {
@Override @Override
public void doAllocate() throws IOException { public void doAllocate() throws IOException {
int dataBlocks = (int) ((getPhysicalFile().length() / ProdosVirtualDisk.BLOCK_SIZE) + 1); int dataBlocks = (int) ((getPhysicalFile().length()+ProdosVirtualDisk.BLOCK_SIZE-1) / ProdosVirtualDisk.BLOCK_SIZE);
int treeBlocks; int treeBlocks =(((dataBlocks * 2) + (ProdosVirtualDisk.BLOCK_SIZE-2)) / ProdosVirtualDisk.BLOCK_SIZE);
if (dataBlocks > 1 && dataBlocks < 257) { if (treeBlocks > 1) treeBlocks++;
treeBlocks = 1; // if (dataBlocks > 1 && (dataBlocks*2) < ProdosVirtualDisk.BLOCK_SIZE) {
} else { // treeBlocks = 1;
treeBlocks = 1 + (dataBlocks / 256); // } else {
} // treeBlocks = 1 + (dataBlocks * 2 / ProdosVirtualDisk.BLOCK_SIZE);
for (int i = 1; i < dataBlocks + treeBlocks; i++) { // }
SubNode subNode = new SubNode(i, this); System.out.println("Allocating "+(dataBlocks + treeBlocks)+" blocks for file "+getName()+"; data "+dataBlocks+"; tree "+treeBlocks);
for (int i = 0; i < dataBlocks + treeBlocks; i++) {
new SubNode(i, this);
} }
setBaseBlock(additionalNodes.get(0).getBaseBlock());
} }
@Override @Override
@ -154,6 +158,9 @@ public class FileNode extends DiskNode {
@Override @Override
public void readBlock(int block, byte[] buffer) throws IOException { public void readBlock(int block, byte[] buffer) throws IOException {
// System.out.println("Read block "+block+" of file "+getName()); // System.out.println("Read block "+block+" of file "+getName());
int dataBlocks = (int) ((getPhysicalFile().length()+ProdosVirtualDisk.BLOCK_SIZE-1) / ProdosVirtualDisk.BLOCK_SIZE);
int treeBlocks =(((dataBlocks * 2) + (ProdosVirtualDisk.BLOCK_SIZE-2)) / ProdosVirtualDisk.BLOCK_SIZE);
if (treeBlocks > 1) treeBlocks++;
switch (this.getType()) { switch (this.getType()) {
case SEEDLING: case SEEDLING:
readFile(buffer, 0); readFile(buffer, 0);
@ -163,20 +170,20 @@ public class FileNode extends DiskNode {
readFile(buffer, (block - 1)); readFile(buffer, (block - 1));
} else { } else {
// Generate seedling index block // Generate seedling index block
generateIndex(buffer, 0, 256); generateIndex(buffer, 0, dataBlocks);
} }
break; break;
case TREE: case TREE:
int dataBlocks = (int) ((getPhysicalFile().length() / ProdosVirtualDisk.BLOCK_SIZE) + 1);
int treeBlocks = (dataBlocks / 256);
if (block == 0) { if (block == 0) {
generateIndex(buffer, 0, treeBlocks); System.out.println("Reading index for "+getName());
} else if (block < treeBlocks) { generateIndex(buffer, 1, treeBlocks);
int start = treeBlocks + (block - 1 * 256); } else if (block <= treeBlocks) {
int end = Math.min(start + 256, treeBlocks); System.out.println("Reading tree block "+block+" for "+getName());
generateIndex(buffer, treeBlocks, end); int start = treeBlocks + ((block - 1) * 256);
int end = treeBlocks + dataBlocks;
generateIndex(buffer, start, end);
} else { } else {
readFile(buffer, (block - treeBlocks)); readFile(buffer, (block - treeBlocks - 1));
} }
break; break;
} }
@ -190,10 +197,22 @@ public class FileNode extends DiskNode {
} }
private void generateIndex(byte[] buffer, int indexStart, int indexLimit) { private void generateIndex(byte[] buffer, int indexStart, int indexLimit) {
int pos = 0; System.out.println("Index block contents:");
for (int i = indexStart; pos < 256 && i < indexLimit && i < additionalNodes.size(); i++, pos++) { Arrays.fill(buffer, (byte) 0);
buffer[pos] = (byte) (additionalNodes.get(i).baseBlock & 0x0ff); for (int i = indexStart, count=0; count < 256 && i < indexLimit && i < additionalNodes.size(); i++, count++) {
buffer[pos + 256] = (byte) ((additionalNodes.get(i).baseBlock >> 8) & 0x0ff); int base = additionalNodes.get(i).baseBlock;
System.out.print(Integer.toHexString(base)+":");
buffer[count] = (byte) (base & 0x0ff);
buffer[count + 256] = (byte) (base >> 8);
} }
System.out.println();
for (int i=0; i < 256; i++) {
System.out.printf("%02X ",buffer[i]&0x0ff);
}
System.out.println();
for (int i=256; i < 512; i++) {
System.out.printf("%02X ",buffer[i]&0x0ff);
}
System.out.println();
} }
} }

View File

@ -38,7 +38,7 @@ import java.util.logging.Logger;
* is a folder and not a disk image. FreespaceBitmap and the various Node * is a folder and not a disk image. FreespaceBitmap and the various Node
* classes are used to represent the filesystem structure. * classes are used to represent the filesystem structure.
* *
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com * @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/ */
public class ProdosVirtualDisk implements IDisk { public class ProdosVirtualDisk implements IDisk {
@ -61,7 +61,7 @@ public class ProdosVirtualDisk implements IDisk {
DiskNode node = physicalMap.get(block); DiskNode node = physicalMap.get(block);
Arrays.fill(ioBuffer, (byte) (block & 0x0ff)); Arrays.fill(ioBuffer, (byte) (block & 0x0ff));
if (node == null) { if (node == null) {
System.out.println("Reading unknown block?!"); System.out.println("Reading unknown block " + Integer.toHexString(block));
for (int i = 0; i < BLOCK_SIZE; i++) { for (int i = 0; i < BLOCK_SIZE; i++) {
memory.write(bufferAddress + i, (byte) 0, false, false); memory.write(bufferAddress + i, (byte) 0, false, false);
} }
@ -132,11 +132,15 @@ public class ProdosVirtualDisk implements IDisk {
} }
// Mark space occupied by node // Mark space occupied by node
public void allocateEntry(DiskNode node) { public void allocateEntry(DiskNode node) throws IOException {
physicalMap.put(node.baseBlock, node); physicalMap.put(node.baseBlock, node);
node.additionalNodes.stream().forEach((sub) -> {
physicalMap.put(sub.getBaseBlock(), sub); for (DiskNode subnode : node.additionalNodes) {
}); int blockNum = getNextFreeBlock();
System.out.println("Allocating block " + Integer.toHexString(blockNum) + " for " + subnode.getName());
subnode.setBaseBlock(blockNum);
physicalMap.put(blockNum, subnode);
}
} }
// Mark space occupied by nodes as free (remove allocation mapping) // Mark space occupied by nodes as free (remove allocation mapping)
@ -145,11 +149,11 @@ public class ProdosVirtualDisk implements IDisk {
if (physicalMap.get(node.baseBlock) != null && physicalMap.get(node.baseBlock).equals(node)) { if (physicalMap.get(node.baseBlock) != null && physicalMap.get(node.baseBlock).equals(node)) {
physicalMap.remove(node.baseBlock); physicalMap.remove(node.baseBlock);
} }
node.additionalNodes.stream().filter((sub) -> node.additionalNodes.stream().filter((sub)
(physicalMap.get(sub.getBaseBlock()) != null && physicalMap.get(sub.baseBlock).equals(sub))). -> (physicalMap.get(sub.getBaseBlock()) != null && physicalMap.get(sub.baseBlock).equals(sub))).
forEach((sub) -> { forEach((sub) -> {
physicalMap.remove(sub.getBaseBlock()); physicalMap.remove(sub.getBaseBlock());
}); });
} }
// Is the specified block in use? // Is the specified block in use?
@ -200,7 +204,6 @@ public class ProdosVirtualDisk implements IDisk {
freespaceBitmap = new FreespaceBitmap(this, FREESPACE_BITMAP_START); freespaceBitmap = new FreespaceBitmap(this, FREESPACE_BITMAP_START);
allocateEntry(freespaceBitmap); allocateEntry(freespaceBitmap);
} }
@Override @Override

View File

@ -31,7 +31,6 @@ import java.io.IOException;
public class SubNode extends DiskNode { public class SubNode extends DiskNode {
int sequenceNumber; int sequenceNumber;
private int seq;
public SubNode(int seq, DiskNode parent) throws IOException { public SubNode(int seq, DiskNode parent) throws IOException {
init(seq, parent); init(seq, parent);
@ -49,6 +48,11 @@ public class SubNode extends DiskNode {
parent.additionalNodes.add(this); parent.additionalNodes.add(this);
} }
@Override
public String getName() {
return parent.getName() + "; block "+sequenceNumber;
}
@Override @Override
public void doDeallocate() { public void doDeallocate() {
} }

View File

@ -49,7 +49,7 @@ public enum DiskType {
} }
static public DiskType determineType(File file) { static public DiskType determineType(File file) {
if (!file.exists()) return null; if (file == null || !file.exists()) return null;
if (file.isDirectory()) return VIRTUAL; if (file.isDirectory()) return VIRTUAL;
if (file.getName().toLowerCase().endsWith("hdv")) { if (file.getName().toLowerCase().endsWith("hdv")) {
return LARGE; return LARGE;