Implemented ISO9660 parsing to access the game's serial number

The serial number is based on the "BOOT" executable name in
SYSTEM.CNF.

We now base region detection on the serial number instead of the
license string.
This commit is contained in:
Lionel Flandrin 2016-06-23 01:07:46 +02:00
parent d112e027d7
commit d4d9ff91fd
4 changed files with 518 additions and 66 deletions

@ -1 +1 @@
Subproject commit 88a2a5fdf4c7dc8e7524518acb274458a03bb3f8
Subproject commit 3140ff5a081139200a39c5b1a57e29522bfb2a75

View file

@ -1,9 +1,14 @@
use std::fmt;
use cdimage::{Image, CdError};
use cdimage::msf::Msf;
use cdimage::bcd::Bcd;
use cdimage::sector::Sector;
use rustc_serialize::{Decodable, Encodable, Decoder, Encoder};
use super::iso9660;
/// PlayStation disc.
///
/// XXX: add support for CD-DA? Not really useful but shouldn't
@ -11,103 +16,67 @@ use rustc_serialize::{Decodable, Encodable, Decoder, Encoder};
pub struct Disc {
/// Image file
image: Box<Image>,
/// Disc region
region: Region,
/// Disc serial number
serial: SerialNumber,
}
impl Disc {
/// Reify a disc using `image` as a backend.
pub fn new(image: Box<Image>) -> Result<Disc, CdError> {
pub fn new(mut image: Box<Image>) -> Result<Disc, String> {
let serial =
match extract_serial_number(&mut *image) {
Some(s) => s,
None => {
return Err("Couldn't find disc serial number".into());
}
};
let disc = Disc {
image: image,
// Use a dummy id for now.
region: Region::Japan,
serial: serial,
};
disc.extract_region()
Ok(disc)
}
pub fn region(&self) -> Region {
self.region
// For now I prefer to panic to catch potential issues with
// the serial number handling code, alternatively we could
// fallback on `extract_system_region`
match self.serial.region() {
Some(r) => r,
None => panic!("Can't establish the region of {}", self.serial),
}
}
pub fn serial_number(&self) -> SerialNumber {
self.serial
}
pub fn image(&mut self) -> &mut Image {
&mut*self.image
}
/// Attempt to discover the region of the disc. This way we know
/// which string to return in the CD-ROM drive's "get id" command
/// and we can also decide which BIOS and output video standard to
/// use based on the game disc.
fn extract_region(mut self) -> Result<Disc, CdError> {
// In order to identify the type of disc we're going to use
// sector 00:02:04 which should contain the "Licensed by..."
// string.
let msf = Msf::from_bcd(0x00, 0x02, 0x04).unwrap();
let mut sector = Sector::empty();
try!(self.image.read_sector(&mut sector, msf));
// On the discs I've tried we always have an ASCII license
// string in the first 76 data bytes. We'll see if it holds
// true for all the discs out there...
let license_blob = &try!(sector.mode2_xa_payload())[0..76];
// There are spaces everywhere in the license string
// (including in the middle of some words), let's clean it up
// and convert to a canonical string
let license: String = license_blob.iter()
.filter_map(|&b| {
match b {
b'A'...b'z' => Some(b as char),
_ => None,
}
})
.collect();
self.region =
match license.as_ref() {
"LicensedbySonyComputerEntertainmentInc"
=> Region::Japan,
"LicensedbySonyComputerEntertainmentAmerica"
=> Region::NorthAmerica,
"LicensedbySonyComputerEntertainmentofAmerica"
=> Region::NorthAmerica,
"LicensedbySonyComputerEntertainmentEurope"
=> Region::Europe,
_ => {
warn!("Couldn't identify disc region string: {}", license);
return Err(CdError::BadFormat);
}
};
Ok(self)
}
}
impl Encodable for Disc {
fn encode<S: Encoder>(&self, s: &mut S) -> Result<(), S::Error> {
// XXX We could maybe store something to make sure we're
// loading the right disc. A checksum might be a bit overkill,
// maybe just the game's serial number or something?
s.emit_nil()
// Only encode the serial number
self.serial.encode(s)
}
}
impl Decodable for Disc {
fn decode<D: Decoder>(d: &mut D) -> Result<Disc, D::Error> {
try!(d.read_nil());
let serial = try!(SerialNumber::decode(d));
// Placeholder disc image
Ok(Disc {
image: Box::new(MissingImage),
region: Region::Japan,
serial: serial,
})
}
}
/// Dummy Image implemementation used when deserializing a Disc. Since
/// we don't want to store the entire disc in the image it will be
/// missing after a load, it's up to the frontend to make sure to
@ -122,9 +91,13 @@ impl Image for MissingImage {
fn read_sector(&mut self, _: &mut Sector, _: Msf) -> Result<(), CdError> {
panic!("Missing CD image!");
}
fn track_msf(&self, _: Bcd, _: Msf) -> Result<Msf, CdError> {
panic!("Missing CD image!");
}
}
/// Disc region coding
/// Disc region
#[derive(Clone, Copy, Debug, PartialEq, Eq, RustcDecodable, RustcEncodable)]
pub enum Region {
/// Japan (NTSC): SCEI
@ -134,3 +107,198 @@ pub enum Region {
/// Europe (PAL): SCEE
Europe,
}
/// Disc serial number
#[derive(Copy, Clone, PartialEq, Eq, RustcDecodable, RustcEncodable)]
pub struct SerialNumber([u8; 10]);
impl SerialNumber {
/// Create a dummy serial number: UNKN-00000. Used when no serial
/// number can be found.
pub fn dummy() -> SerialNumber {
SerialNumber(*b"UNKN-00000")
}
/// Extract a serial number from a standard PlayStation binary
/// name of the form "aaaa_ddd.dd"
fn from_bin_name(bin: &[u8]) -> Option<SerialNumber> {
if bin.len() != 11 {
return None;
}
if bin[4] != b'_' {
// This will fail for the few "lightspan educational"
// discs since they have a serial number looking like
// "LSP-123456". Those games are fairly obscure and
// some of them seem to have weird and nonstandards
// SYSTEM.CNF anyway.
return None;
}
let mut serial = [0u8; 10];
let to_upper = |b| {
if b >= b'a' && b <= b'z' {
b - b'a' + b'A'
} else {
b
}
};
serial[0] = to_upper(bin[0]);
serial[1] = to_upper(bin[1]);
serial[2] = to_upper(bin[2]);
serial[3] = to_upper(bin[3]);
serial[4] = b'-';
serial[5] = bin[5];
serial[6] = bin[6];
serial[7] = bin[7];
serial[8] = bin[9];
serial[9] = bin[10];
Some(SerialNumber(serial))
}
pub fn region(&self) -> Option<Region> {
match &self.0[0..4] {
b"SCPS" | b"SLPS" | b"SLPM" | b"PAPX" => Some(Region::Japan),
b"SCUS" | b"SLUS" | b"LSP-" => Some(Region::NorthAmerica),
b"SCES" | b"SCED" | b"SLES" | b"SLED" => Some(Region::Europe),
_ => None,
}
}
}
impl fmt::Display for SerialNumber {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", String::from_utf8_lossy(&self.0))
}
}
/// Attempt to discover the region of the disc using the license
/// string stored in the system area of the official PlayStation
/// ISO filesystem.
pub fn extract_system_region(image: &mut Image) -> Result<Region, CdError> {
// In order to identify the type of disc we're going to use
// sector 00:00:04 from Track01 which should contain the
// "Licensed by..." string.
let msf = try!(image.track_msf(Bcd::one(),
Msf::from_bcd(0, 0, 4).unwrap()));
let mut sector = Sector::empty();
try!(image.read_sector(&mut sector, msf));
// On the discs I've tried we always have an ASCII license
// string in the first 76 data bytes. We'll see if it holds
// true for all the discs out there...
let license_blob = &try!(sector.mode2_xa_payload())[0..76];
// There are spaces everywhere in the license string
// (including in the middle of some words), let's clean it up
// and convert to a canonical string
let license: String = license_blob.iter()
.filter_map(|&b| {
match b {
b'A'...b'z' => Some(b as char),
_ => None,
}
})
.collect();
let region =
match license.as_ref() {
"LicensedbySonyComputerEntertainmentInc"
=> Region::Japan,
"LicensedbySonyComputerEntertainmentAmerica"
=> Region::NorthAmerica,
"LicensedbySonyComputerEntertainmentofAmerica"
=> Region::NorthAmerica,
"LicensedbySonyComputerEntertainmentEurope"
=> Region::Europe,
_ => {
warn!("Couldn't identify disc region string: {}", license);
return Err(CdError::BadFormat);
}
};
Ok(region)
}
/// Attempt to extract the serial number of the disc. All officially
/// licensed PlayStation game should have a serial number.
fn extract_serial_number(image: &mut Image) -> Option<SerialNumber> {
let system_cnf =
match read_system_cnf(image) {
Ok(c) => c,
Err(e) => {
warn!("Couldn't read SYSTEM.CNF: {:?}", e);
return None;
}
};
// Now we need to parse the SYSTEM.CNF file to get the content of
// the "BOOT" line
let mut boot_path = None;
for line in system_cnf.split(|&b| b == b'\n') {
let words: Vec<_> = line
.split(|&b| b == b' ' || b == b'\t' || b == b'=')
.filter(|w| !w.is_empty())
.collect();
if words.len() == 2 {
if words[0] == b"BOOT" {
boot_path = Some(words[1]);
break;
}
}
}
let boot_path =
match boot_path {
Some(b) => b,
None => {
warn!("Couldn't find BOOT line in SYSTEM.CNF");
return None;
}
};
// boot_path should look like "cdrom:\FOO\BAR\...\aaaa_ddd.dd;1"
let path: Vec<_> = boot_path
.split(|&b| b == b':' || b == b';' || b == b'\\')
.collect();
if path.len() < 2 {
warn!("Unexpected boot path: {}", String::from_utf8_lossy(boot_path));
return None;
}
let bin_name = path[path.len() - 2];
let serial = SerialNumber::from_bin_name(&bin_name);
if serial.is_none() {
warn!("Unexpected bin name: {}", String::from_utf8_lossy(bin_name));
}
serial
}
fn read_system_cnf(image: &mut Image) -> Result<Vec<u8>, iso9660::Error> {
let dir = try!(iso9660::open_image(image));
let system_cnf = try!(dir.entry_by_name(b"SYSTEM.CNF;1"));
// SYSTEM.CNF should be a small text file, 1MB should bb way more
// than necessary
let len = system_cnf.extent_len();
if len > 1024 * 1024 {
let desc = format!("SYSTEM.CNF is too big: {}B", len);
return Err(iso9660::Error::BadFormat(desc));
}
system_cnf.read_file(image)
}

283
src/cdrom/iso9660.rs Normal file
View file

@ -0,0 +1,283 @@
use cdimage::{Image, CdError};
use cdimage::sector::Sector;
use cdimage::msf::Msf;
use cdimage::bcd::Bcd;
/// Structure representing an ISO9660 directory
pub struct Directory {
/// Contents of the directory
entries: Vec<Entry>,
}
impl Directory {
pub fn new(image: &mut Image, entry: &Entry) -> Result<Directory, Error> {
if !entry.is_dir() {
return Err(Error::NotADirectory);
}
let mut dir =
Directory {
entries: Vec::new(),
};
// Directory entries cannot span multiple sectors so it's safe
// to handle them one by one
let mut extent_len = entry.extent_len() as usize;
let extent_location = entry.extent_location();
let track_msf =
match Msf::from_sector_index(extent_location) {
Some(m) => m,
None => return Err(Error::BadExtent(extent_location)),
};
let mut msf = try!(image.track_msf(Bcd::one(), track_msf));
let mut sector = Sector::empty();
while extent_len > 0 {
try!(image.read_sector(&mut sector, msf));
let data = try!(sector.mode2_xa_payload());
let len =
if extent_len > 2048 {
2048
} else {
extent_len
};
try!(dir.parse_entries(&data[0..len]));
extent_len -= len;
msf = msf.next().unwrap();
}
Ok(dir)
}
fn parse_entries(&mut self, mut raw: &[u8]) -> Result<(), Error> {
while raw.len() > 0 {
let dir_len = raw[0] as usize;
if dir_len == 0 {
// It seems we've reached the last directory. Or at
// least I think so? I'm not entirely sure how
// directories which span several sectors are handled,
// if the padding is not part of any entry then we
// should skip ahead to the next sector. Needs more
// testing.
break;
}
if dir_len < 34 {
let desc = format!("Directory entry too short ({})", dir_len);
return Err(Error::BadFormat(desc));
}
let name_len = raw[32] as usize;
let name_end = 33 + name_len;
if name_end > dir_len {
return Err(Error::BadFormat("Entry name too long".into()));
}
self.entries.push(Entry::new(&raw[0..dir_len]));
raw = &raw[dir_len..];
}
Ok(())
}
/// Attempt to "cd" to a subdirectory, returning a new `Directory`
/// instance
pub fn cd(&self,
image: &mut Image,
name: &[u8]) -> Result<Directory, Error> {
let entry = try!(self.entry_by_name(name));
Directory::new(image, entry)
}
pub fn entry_by_name(&self, name: &[u8]) -> Result<&Entry, Error> {
match
self.entries.iter().find(|e| e.name() == name) {
Some(e) => Ok(e),
None => Err(Error::EntryNotFound),
}
}
/// Retreive a list of all the entries in this directory
pub fn ls(&self) -> &[Entry] {
&self.entries
}
}
/// A single directory entry
pub struct Entry(Vec<u8>);
impl Entry {
fn new(entry: &[u8]) -> Entry {
Entry(entry.into())
}
pub fn name(&self) -> &[u8] {
let name_len = self.0[32] as usize;
let name_end = 33 + name_len;
// No need to validate the len, it should've been done on
// entry creation
&self.0[33..name_end]
}
pub fn is_dir(&self) -> bool {
let flags = self.0[25];
(flags & 0x2) != 0
}
pub fn extent_location(&self) -> u32 {
read_u32(&self.0[2..10])
}
pub fn extent_len(&self) -> u32 {
read_u32(&self.0[10..18])
}
pub fn read_file(&self, image: &mut Image) -> Result<Vec<u8>, Error> {
if self.is_dir() {
return Err(Error::NotAFile);
}
let mut extent_len = self.extent_len() as usize;
let extent_location = self.extent_location();
let mut contents = Vec::with_capacity(extent_len);
let track_msf =
match Msf::from_sector_index(extent_location) {
Some(m) => m,
None => return Err(Error::BadExtent(extent_location)),
};
let mut msf = try!(image.track_msf(Bcd::one(), track_msf));
let mut sector = Sector::empty();
while extent_len > 0 {
try!(image.read_sector(&mut sector, msf));
let data = try!(sector.mode2_xa_payload());
let len =
if extent_len > 2048 {
2048
} else {
extent_len
};
for &b in &data[0..len] {
contents.push(b);
}
extent_len -= len;
msf = msf.next().unwrap();
}
Ok(contents)
}
}
#[derive(Debug)]
pub enum Error {
/// Cdimage access error
CdError(CdError),
/// Couldn't find the ISO9660 magic "CD0001"
BadMagic,
/// Couldn't find the Primary Volume Descriptor
MissingPrimaryVolumeDescriptor,
/// Unexpected Volume Descriptor version
BadVolumDescriptorVersion,
/// Encountered an invalid extent location
BadExtent(u32),
/// Miscellaneous ISO9660 format error containing a description of
/// the problem
BadFormat(String),
/// The requested entry couldn't be found
EntryNotFound,
/// We expected a directory and got a file
NotADirectory,
/// We expected a file and got a directory
NotAFile,
}
impl From<CdError> for Error {
fn from(e: CdError) -> Error {
Error::CdError(e)
}
}
pub fn open_image(image: &mut Image) -> Result<Directory, Error> {
// The first 16 sectors are the "system area" which is ignored by
// the ISO filesystem. The Volume Descriptor Set should start at
// 00:00:16 in track 01
let mut msf = try!(image.track_msf(Bcd::one(),
Msf::from_bcd(0, 0, 0x16).unwrap()));
let mut sector = Sector::empty();
// Look for the primary volume descriptor
loop {
try!(image.read_sector(&mut sector, msf));
let volume_descriptor = try!(sector.mode2_xa_payload());
// Check volume descriptor "standard identifier"
if &volume_descriptor[1..6] != b"CD001" {
return Err(Error::BadMagic);
}
// Byte 0 contains the "volume descriptor type".
match volume_descriptor[0] {
// Primary Volume Descriptor
0x01 => break,
// Volume Descriptor Set Terminator
0xff => return Err(Error::MissingPrimaryVolumeDescriptor),
// Unhandled volume descriptor type, ignore
_ => (),
}
// Not the primary volume descriptor, move on to the next
// sector
msf = msf.next().unwrap();
}
let volume_descriptor = try!(sector.mode2_xa_payload());
// Volume Descriptor Version
if volume_descriptor[6] != 0x01 {
return Err(Error::BadVolumDescriptorVersion);
}
// We can now open the root directory descriptor
let root_dir_descriptor = &volume_descriptor[156..190];
let root_dir = Entry::new(root_dir_descriptor);
Directory::new(image, &root_dir)
}
/// Read a 32bit number stored in "both byte order" format
fn read_u32(v: &[u8]) -> u32 {
// Only use the little endian representation. Should we bother
// validating that the BE version is coherent?
v[0] as u32 |
((v[1] as u32) << 8) |
((v[2] as u32) << 16) |
((v[3] as u32) << 24)
}

View file

@ -27,6 +27,7 @@ use self::disc::{Disc, Region};
use self::simple_rand::SimpleRand;
pub mod disc;
pub mod iso9660;
mod simple_rand;