/**
* This file is part of muCommander, http://www.mucommander.com
* Copyright (C) 2002-2016 Maxence Bernard
*
* muCommander is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* muCommander 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mucommander.commons.file.archive.iso;
import com.mucommander.commons.file.AbstractFile;
import com.mucommander.commons.io.BufferPool;
import com.mucommander.commons.io.RandomAccessInputStream;
import com.mucommander.commons.io.StreamUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Calendar;
import java.util.Vector;
/**
* Parses entries contained in an ISO/NRG file.
* <p>
* <pre>
* Reference: http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-119.pdf
* <p/>
* Todo:
* * test with more images
* * rewrite/sanitize InputStream for cooked
* * add RockRidge, UDF and others extensions
* * add DiscJuggler & other weirdo file formats
* </pre>
* </p>
*
* @author Xavier Martin
*/
class IsoParser {
private static final Logger LOGGER = LoggerFactory.getLogger(IsoParser.class);
public static Vector<IsoArchiveEntry> getEntries(byte[] buffer, RandomAccessInputStream rais, int sectSize, long sector_offset, long shiftOffset) throws Exception {
Vector<IsoArchiveEntry> entries = new Vector<IsoArchiveEntry>();
Calendar calendar = Calendar.getInstance();
int start = 16;
isoPvd pvd = null;
todo todo_idr = null;
int level = 0;
for (int i = 1; i < 17; i++) { // fuzzy search, can have type=0 (bootable el torito), type=2 (svd)
pvd = new isoPvd(buffer, rais, start + i, sectSize, shiftOffset);
if (pvd.type[0] == 2 && pvd.id[0] == 'C' && pvd.id[1] == 'D' && pvd.id[2] == '0' && pvd.id[3] == '0' && pvd.id[4] == '1') {
// gotta read docs a little more about those UCS-2 Escape Sequences
switch (pvd.unused3[2]) {
case 0x40:
level = 1;
break;
case 0x43:
level = 2;
break;
case 0x45:
level = 3;
}
break;
}
}
if (level == 0) // if no SVD with Joliet, fallback to plain-old ISO9660
pvd = new isoPvd(buffer, rais, start, sectSize, shiftOffset);
isoDr idr = new isoDr(pvd.root_directory_record, 0);
todo_idr = parse_dir(todo_idr, "", isonum_733(idr.extent), isonum_733(idr.size), rais, buffer, entries, sectSize, level, shiftOffset, sector_offset, calendar);
while (todo_idr != null) {
todo_idr = parse_dir(todo_idr, todo_idr.name, todo_idr.extent, todo_idr.length, rais, buffer, entries, sectSize, level, shiftOffset, sector_offset, calendar);
todo_idr = todo_idr.next;
}
return entries;
}
/**
* Parses the given ISO file and returns the list of entries it contains. The specified stream will *not* be closed
* by this method.
*
* @param file the ISO file to parse
* @param rais random access stream to read the ISO file. It will *not* be closed by this method.
* @return the list of entries contained by the ISO file
* @throws IOException if an I/O error occurs
*/
static Vector<IsoArchiveEntry> getEntries(AbstractFile file, RandomAccessInputStream rais) throws IOException {
byte[] buffer = BufferPool.getByteArray(IsoUtil.MODE1_2048);
try {
if ("nrg".equals(file.getExtension())) {
return NrgParser.getEntries(buffer, file, rais);
}
int sectSize = IsoUtil.guessSectorSize(file);
// sector shift : 0 most of the time
long sector_offset = 0;
// bytes : depend if there's earlier track we discard
long shiftOffset = 0;
return getEntries(buffer, rais, sectSize, sector_offset, shiftOffset);
/*
if ("cdi".equals(file.getExtension())) {
// WIP
// http://cvs.berlios.de/cgi-bin/viewcvs.cgi/libdiscmage/libdiscmage/src/filter/cdi.c?revision=1.3&view=markup
int len = rais.available();
long offset = -1;
rais.seek(len - 7);
rais.read(buffer, 0, 8);
long version = IsoUtil.toDwordBE(buffer, 0);
offset = IsoUtil.toDwordBE(buffer, 4);
FileLogger.finest("cdi root " + Long.toHexString(offset) + " version " + Long.toHexString(version));
}
*/
}
catch (Exception e) {
LOGGER.info("Exception caught while parsing iso, throwing IOException", e);
throw new IOException();
}
finally {
// Release the buffer
BufferPool.releaseByteArray(buffer);
}
}
private static void newString(byte b[], int len, int level, StringBuffer name) throws Exception {
name.append((level == 0) ? new String(b, 0, len) : new String(b, 0, len, "UnicodeBigUnmarked"));
}
public static todo parse_dir(todo todo_idr, String rootname, int extent, int len, RandomAccessInputStream rais, byte[] buffer, Vector<IsoArchiveEntry> entries, int sectSize, int level, long shiftOffset, long sector_offset, Calendar calendar) throws Exception {
todo td;
int i;
isoDr idr;
while (len > 0) {
rais.seek(IsoUtil.offsetInSector(extent - sector_offset, sectSize, false) + shiftOffset);
StreamUtils.readFully(rais, buffer);
len -= buffer.length;
extent++;
i = 0;
while (true) {
idr = new isoDr(buffer, i);
if (idr.length[0] == 0) break;
stat fstat_buf = new stat();
StringBuffer name_buf = new StringBuffer();
fstat_buf.st_size = isonum_733(idr.size);
if ((idr.flags[0] & 2) > 0)
fstat_buf.st_mode |= S_IFDIR;
else
fstat_buf.st_mode |= S_IFREG;
if (idr.name_len[0] == 1 && idr.name[0] == 0)
name_buf.append(".");
else if (idr.name_len[0] == 1 && idr.name[0] == 1)
name_buf.append("..");
else {
newString(idr.name, idr.name_len[0] & 0xff, level, name_buf);
//if (level == 0) { // strip ;VERSION
int p = name_buf.lastIndexOf(";");
if (p != -1)
name_buf.setLength(p);
p = name_buf.lastIndexOf("."); // strip empty extension
if (p != -1) {
int s = name_buf.length() - 1;
if (p == s)
name_buf.setLength(s);
}
//}
}
if ((idr.flags[0] & 2) != 0
&& (idr.name_len[0] != 1
|| (idr.name[0] != 0 && idr.name[0] != 1))) {
td = todo_idr;
if (td != null) {
while (td.next != null) td = td.next;
td.next = new todo();
td = td.next;
} else {
todo_idr = td = new todo();
}
td.next = null;
td.extent = isonum_733(idr.extent);
td.length = isonum_733(idr.size);
td.name = rootname + name_buf + "/";
}
boolean dir = false;
String n = name_buf.toString();
if (!(".".equals(n) || "..".equals(n))) {
StringBuffer name = new StringBuffer(rootname);
name.append(n);
if (S_ISDIR(fstat_buf.st_mode)) {
dir = true;
if (!n.endsWith("/")) {
name.append('/');
}
}
calendar.set((idr.date[0] & 0xff) + 1900, idr.date[1] - 1, idr.date[2], idr.date[3], idr.date[4], idr.date[5]);
// date_buf[6]
// offset from Greenwich Mean Time, in 15-minute intervals, as a twos complement signed number,
// positive for time zones east of Greenwich, and negative for time zones
calendar.setTimeZone(new java.util.SimpleTimeZone(15 * 60 * 1000 * idr.date[6], ""));
entries.add(
new IsoArchiveEntry(
name.toString(),
dir,
calendar.getTimeInMillis(),
fstat_buf.st_size,
isonum_733(idr.extent) - sector_offset,
sectSize,
shiftOffset,
false)
);
}
i += (buffer[i] & 0xff);
if (i > IsoUtil.MODE1_2048 - idr.s_length) break;
}
}
return todo_idr;
}
// ======================================
private static int ISODCL(int start, int end) {
return (end - start + 1);
}
private static int isonum_731(byte p[]) {
return IsoUtil.toDwordBE(p, 0);
}
public static int isonum_733(byte p[]) {
return (isonum_731(p));
}
private static boolean S_ISDIR(int m) {
return ((m & S_IFDIR) == S_IFDIR);
}
// ======================================
private static int S_IFREG = 0100000;
private static int S_IFDIR = 0040000;
private static class stat {
int st_size;
int st_mode;
}
public static class todo {
public todo next;
public String name;
public int extent;
public int length;
}
public static class isoDr {
public byte[] length = new byte[ISODCL(1, 1)];
public byte[] ext_attr_length = new byte[ISODCL(2, 2)];
public byte[] extent = new byte[ISODCL(3, 10)];
public byte[] size = new byte[ISODCL(11, 18)];
public byte[] date = new byte[ISODCL(19, 25)];
public byte[] flags = new byte[ISODCL(26, 26)];
public byte[] file_unit_size = new byte[ISODCL(27, 27)];
public byte[] interleave = new byte[ISODCL(28, 28)];
public byte[] volume_sequence_number = new byte[ISODCL(29, 32)];
public byte[] name_len = new byte[ISODCL(33, 33)];
public byte[] name = new byte[/*38*/128]; // quickly bumped to 128 for Joliet : doesn't lead to a crash yet :)
public int s_length = 34;
public byte dataDr[][] = {length, ext_attr_length, extent, size,
date, flags, file_unit_size, interleave,
volume_sequence_number, name_len, name};
public isoDr(byte src[], int pos) {
for (int i = 0, max = dataDr.length; i < max; i++) {
int l = dataDr[i].length;
if ((src.length - pos) < dataDr[i].length && i == 10)
l = src.length - pos;
System.arraycopy(src, pos, dataDr[i], 0, l);
pos += dataDr[i].length;
}
}
}
public static class isoPvd {
public byte[] type = new byte[ISODCL(1, 1)];
public byte[] id = new byte[ISODCL(2, 6)];
public byte[] version = new byte[ISODCL(7, 7)];
public byte[] unused1 = new byte[ISODCL(8, 8)];
public byte[] system_id = new byte[ISODCL(9, 40)];
public byte[] volume_id = new byte[ISODCL(41, 72)];
public byte[] unused2 = new byte[ISODCL(73, 80)];
public byte[] volume_space_size = new byte[ISODCL(81, 88)];
public byte[] unused3 = new byte[ISODCL(89, 120)];
public byte[] volume_set_size = new byte[ISODCL(121, 124)];
public byte[] volume_sequence_number = new byte[ISODCL(125, 128)];
public byte[] logical_block_size = new byte[ISODCL(129, 132)];
public byte[] path_table_size = new byte[ISODCL(133, 140)];
public byte[] type_l_path_table = new byte[ISODCL(141, 144)];
public byte[] opt_type_l_path_table = new byte[ISODCL(145, 148)];
public byte[] type_m_path_table = new byte[ISODCL(149, 152)];
public byte[] opt_type_m_path_table = new byte[ISODCL(153, 156)];
public byte[] root_directory_record = new byte[ISODCL(157, 190)];
public byte[] volume_set_id = new byte[ISODCL(191, 318)];
public byte[] publisher_id = new byte[ISODCL(319, 446)];
public byte[] preparer_id = new byte[ISODCL(447, 574)];
public byte[] application_id = new byte[ISODCL(575, 702)];
public byte[] copyright_file_id = new byte[ISODCL(703, 739)];
public byte[] abstract_file_id = new byte[ISODCL(740, 776)];
public byte[] bibliographic_file_id = new byte[ISODCL(777, 813)];
public byte[] creation_date = new byte[ISODCL(814, 830)];
public byte[] modification_date = new byte[ISODCL(831, 847)];
public byte[] expiration_date = new byte[ISODCL(848, 864)];
public byte[] effective_date = new byte[ISODCL(865, 881)];
public byte[] file_structure_version = new byte[ISODCL(882, 882)];
public byte[] unused4 = new byte[ISODCL(883, 883)];
public byte[] application_data = new byte[ISODCL(884, 1395)];
public byte[] unused5 = new byte[ISODCL(1396, IsoUtil.MODE1_2048)];
byte dataPvr[][] = {type, id, version, unused1,
system_id, volume_id, unused2, volume_space_size,
unused3, volume_set_size, volume_sequence_number, logical_block_size,
path_table_size, type_l_path_table, opt_type_l_path_table, type_m_path_table,
opt_type_m_path_table, root_directory_record, volume_set_id, publisher_id,
preparer_id, application_id, copyright_file_id, abstract_file_id,
bibliographic_file_id, creation_date, modification_date, expiration_date,
effective_date, file_structure_version, unused4, application_data,
unused5};
public isoPvd(byte[] pvd, RandomAccessInputStream rais, int start, int sectSize, long shiftOffset) throws IOException {
rais.seek(IsoUtil.offsetInSector(start, sectSize, false) + shiftOffset);
if (rais.read(pvd) == -1)
throw new IOException("unable to read PVD");
load(pvd);
}
void load(byte src[]) {
for (int i = 0, pos = 0, max = dataPvr.length; i < max; i++) {
System.arraycopy(src, pos, dataPvr[i], 0, dataPvr[i].length);
pos += dataPvr[i].length;
}
}
}
}