// license:BSD-3-Clause
// copyright-holders:Aaron Giles
/***************************************************************************
audit.c
ROM set auditing functions.
***************************************************************************/
#include "emu.h"
#include "emuopts.h"
#include "audit.h"
#include "chd.h"
#include "drivenum.h"
#include "sound/samples.h"
#include "softlist.h"
//**************************************************************************
// CORE FUNCTIONS
//**************************************************************************
//-------------------------------------------------
// media_auditor - constructor
//-------------------------------------------------
media_auditor::media_auditor(const driver_enumerator &enumerator)
: m_enumerator(enumerator),
m_validation(AUDIT_VALIDATE_FULL),
m_searchpath(nullptr)
{
}
//-------------------------------------------------
// audit_media - audit the media described by the
// currently-enumerated driver
//-------------------------------------------------
media_auditor::summary media_auditor::audit_media(const char *validation)
{
// start fresh
m_record_list.reset();
// store validation for later
m_validation = validation;
// temporary hack until romload is update: get the driver path and support it for
// all searches
const char *driverpath = m_enumerator.config().root_device().searchpath();
int found = 0;
int required = 0;
int shared_found = 0;
int shared_required = 0;
// iterate over devices and regions
for (device_t &device : device_iterator(m_enumerator.config().root_device()))
{
// determine the search path for this source and iterate through the regions
m_searchpath = device.searchpath();
// now iterate over regions and ROMs within
for (const rom_entry *region = rom_first_region(device); region != nullptr; region = rom_next_region(region))
{
// temporary hack: add the driver path & region name
std::string combinedpath = std::string(device.searchpath()).append(";").append(driverpath);
if (device.shortname())
combinedpath.append(";").append(device.shortname());
m_searchpath = combinedpath.c_str();
for (const rom_entry *rom = rom_first_file(region); rom; rom = rom_next_file(rom))
{
const char *name = ROM_GETNAME(rom);
hash_collection hashes(ROM_GETHASHDATA(rom));
device_t *shared_device = find_shared_device(device, name, hashes, ROM_GETLENGTH(rom));
// count the number of files with hashes
if (!hashes.flag(hash_collection::FLAG_NO_DUMP) && !ROM_ISOPTIONAL(rom))
{
required++;
if (shared_device != nullptr)
shared_required++;
}
// audit a file
audit_record *record = nullptr;
if (ROMREGION_ISROMDATA(region))
record = audit_one_rom(rom);
// audit a disk
else if (ROMREGION_ISDISKDATA(region))
record = audit_one_disk(rom);
if (record != nullptr)
{
// count the number of files that are found.
if (record->status() == audit_record::STATUS_GOOD || (record->status() == audit_record::STATUS_FOUND_INVALID && find_shared_device(device, name, record->actual_hashes(), record->actual_length()) == nullptr))
{
found++;
if (shared_device != nullptr)
shared_found++;
}
record->set_shared_device(shared_device);
}
}
}
}
// if we only find files that are in the parent & either the set has no unique files or the parent is not found, then assume we don't have the set at all
if (found == shared_found && required > 0 && (required != shared_required || shared_found == 0))
{
m_record_list.reset();
return NOTFOUND;
}
// return a summary
return summarize(m_enumerator.driver().name);
}
//-------------------------------------------------
// audit_device - audit the device
//-------------------------------------------------
media_auditor::summary media_auditor::audit_device(device_t &device, const char *validation)
{
// start fresh
m_record_list.reset();
// store validation for later
m_validation = validation;
m_searchpath = device.shortname();
int found = 0;
int required = 0;
// now iterate over regions and ROMs within
for (const rom_entry *region = rom_first_region(device); region != nullptr; region = rom_next_region(region))
{
for (const rom_entry *rom = rom_first_file(region); rom; rom = rom_next_file(rom))
{
hash_collection hashes(ROM_GETHASHDATA(rom));
// count the number of files with hashes
if (!hashes.flag(hash_collection::FLAG_NO_DUMP) && !ROM_ISOPTIONAL(rom))
{
required++;
}
// audit a file
audit_record *record = nullptr;
if (ROMREGION_ISROMDATA(region))
record = audit_one_rom(rom);
// audit a disk
else if (ROMREGION_ISDISKDATA(region))
record = audit_one_disk(rom);
// count the number of files that are found.
if (record != nullptr && (record->status() == audit_record::STATUS_GOOD || record->status() == audit_record::STATUS_FOUND_INVALID))
{
found++;
}
}
}
if (found == 0 && required > 0)
{
m_record_list.reset();
return NOTFOUND;
}
// return a summary
return summarize(device.shortname());
}
//-------------------------------------------------
// audit_software
//-------------------------------------------------
media_auditor::summary media_auditor::audit_software(const char *list_name, software_info *swinfo, const char *validation)
{
// start fresh
m_record_list.reset();
// store validation for later
m_validation = validation;
std::string combinedpath(swinfo->shortname());
combinedpath.append(";");
combinedpath.append(list_name);
combinedpath.append(PATH_SEPARATOR);
combinedpath.append(swinfo->shortname());
std::string locationtag(list_name);
locationtag.append("%");
locationtag.append(swinfo->shortname());
locationtag.append("%");
if (swinfo->parentname() != nullptr)
{
locationtag.append(swinfo->parentname());
combinedpath.append(";").append(swinfo->parentname()).append(";").append(list_name).append(PATH_SEPARATOR).append(swinfo->parentname());
}
m_searchpath = combinedpath.c_str();
int found = 0;
int required = 0;
// now iterate over software parts
for (software_part &part : swinfo->parts())
{
// now iterate over regions
for ( const rom_entry *region = part.romdata(); region; region = rom_next_region( region ) )
{
// now iterate over rom definitions
for (const rom_entry *rom = rom_first_file(region); rom; rom = rom_next_file(rom))
{
hash_collection hashes(ROM_GETHASHDATA(rom));
// count the number of files with hashes
if (!hashes.flag(hash_collection::FLAG_NO_DUMP) && !ROM_ISOPTIONAL(rom))
{
required++;
}
// audit a file
audit_record *record = nullptr;
if (ROMREGION_ISROMDATA(region))
{
record = audit_one_rom(rom);
}
// audit a disk
else if (ROMREGION_ISDISKDATA(region))
{
record = audit_one_disk(rom, locationtag.c_str());
}
// count the number of files that are found.
if (record != nullptr && (record->status() == audit_record::STATUS_GOOD || record->status() == audit_record::STATUS_FOUND_INVALID))
{
found++;
}
}
}
}
if (found == 0 && required > 0)
{
m_record_list.reset();
return NOTFOUND;
}
// return a summary
return summarize(list_name);
}
//-------------------------------------------------
// audit_samples - validate the samples for the
// currently-enumerated driver
//-------------------------------------------------
media_auditor::summary media_auditor::audit_samples()
{
// start fresh
m_record_list.reset();
int required = 0;
int found = 0;
// iterate over sample entries
for (samples_device &device : samples_device_iterator(m_enumerator.config().root_device()))
{
// by default we just search using the driver name
std::string searchpath(m_enumerator.driver().name);
// add the alternate path if present
samples_iterator iter(device);
if (iter.altbasename() != nullptr)
searchpath.append(";").append(iter.altbasename());
// iterate over samples in this entry
for (const char *samplename = iter.first(); samplename != nullptr; samplename = iter.next())
{
required++;
// create a new record
audit_record &record = m_record_list.append(*global_alloc(audit_record(samplename, audit_record::MEDIA_SAMPLE)));
// look for the files
emu_file file(m_enumerator.options().sample_path(), OPEN_FLAG_READ | OPEN_FLAG_NO_PRELOAD);
path_iterator path(searchpath.c_str());
std::string curpath;
while (path.next(curpath, samplename))
{
// attempt to access the file (.flac) or (.wav)
osd_file::error filerr = file.open(curpath.c_str(), ".flac");
if (filerr != osd_file::error::NONE)
filerr = file.open(curpath.c_str(), ".wav");
if (filerr == osd_file::error::NONE)
{
record.set_status(audit_record::STATUS_GOOD, audit_record::SUBSTATUS_GOOD);
found++;
}
else
record.set_status(audit_record::STATUS_NOT_FOUND, audit_record::SUBSTATUS_NOT_FOUND);
}
}
}
if (found == 0 && required > 0)
{
m_record_list.reset();
return NOTFOUND;
}
// return a summary
return summarize(m_enumerator.driver().name);
}
//-------------------------------------------------
// summary - generate a summary, with an optional
// string format
//-------------------------------------------------
media_auditor::summary media_auditor::summarize(const char *name, std::string *output)
{
if (m_record_list.count() == 0)
{
return NONE_NEEDED;
}
// loop over records
summary overall_status = CORRECT;
for (audit_record &record : m_record_list)
{
summary best_new_status = INCORRECT;
// skip anything that's fine
if (record.substatus() == audit_record::SUBSTATUS_GOOD)
continue;
// output the game name, file name, and length (if applicable)
if (output != nullptr)
{
output->append(string_format("%-12s: %s", name, record.name()));
if (record.expected_length() > 0)
output->append(string_format(" (%d bytes)", record.expected_length()));
output->append(" - ");
}
// use the substatus for finer details
switch (record.substatus())
{
case audit_record::SUBSTATUS_GOOD_NEEDS_REDUMP:
if (output != nullptr) output->append("NEEDS REDUMP\n");
best_new_status = BEST_AVAILABLE;
break;
case audit_record::SUBSTATUS_FOUND_NODUMP:
if (output != nullptr) output->append("NO GOOD DUMP KNOWN\n");
best_new_status = BEST_AVAILABLE;
break;
case audit_record::SUBSTATUS_FOUND_BAD_CHECKSUM:
if (output != nullptr)
{
output->append("INCORRECT CHECKSUM:\n");
output->append(string_format("EXPECTED: %s\n", record.expected_hashes().macro_string().c_str()));
output->append(string_format(" FOUND: %s\n", record.actual_hashes().macro_string().c_str()));
}
break;
case audit_record::SUBSTATUS_FOUND_WRONG_LENGTH:
if (output != nullptr) output->append(string_format("INCORRECT LENGTH: %d bytes\n", record.actual_length()));
break;
case audit_record::SUBSTATUS_NOT_FOUND:
if (output != nullptr)
{
device_t *shared_device = record.shared_device();
if (shared_device == nullptr)
output->append("NOT FOUND\n");
else
output->append(string_format("NOT FOUND (%s)\n", shared_device->shortname()));
}
break;
case audit_record::SUBSTATUS_NOT_FOUND_NODUMP:
if (output != nullptr) output->append("NOT FOUND - NO GOOD DUMP KNOWN\n");
best_new_status = BEST_AVAILABLE;
break;
case audit_record::SUBSTATUS_NOT_FOUND_OPTIONAL:
if (output != nullptr) output->append("NOT FOUND BUT OPTIONAL\n");
best_new_status = BEST_AVAILABLE;
break;
default:
assert(false);
}
// downgrade the overall status if necessary
overall_status = MAX(overall_status, best_new_status);
}
return overall_status;
}
//-------------------------------------------------
// audit_one_rom - validate a single ROM entry
//-------------------------------------------------
audit_record *media_auditor::audit_one_rom(const rom_entry *rom)
{
// allocate and append a new record
audit_record &record = m_record_list.append(*global_alloc(audit_record(*rom, audit_record::MEDIA_ROM)));
// see if we have a CRC and extract it if so
UINT32 crc = 0;
bool has_crc = record.expected_hashes().crc(crc);
// find the file and checksum it, getting the file length along the way
emu_file file(m_enumerator.options().media_path(), OPEN_FLAG_READ | OPEN_FLAG_NO_PRELOAD);
file.set_restrict_to_mediapath(true);
path_iterator path(m_searchpath);
std::string curpath;
while (path.next(curpath, record.name()))
{
// open the file if we can
osd_file::error filerr;
if (has_crc)
filerr = file.open(curpath.c_str(), crc);
else
filerr = file.open(curpath.c_str());
// if it worked, get the actual length and hashes, then stop
if (filerr == osd_file::error::NONE)
{
record.set_actual(file.hashes(m_validation), file.size());
break;
}
}
// compute the final status
compute_status(record, rom, record.actual_length() != 0);
return &record;
}
//-------------------------------------------------
// audit_one_disk - validate a single disk entry
//-------------------------------------------------
audit_record *media_auditor::audit_one_disk(const rom_entry *rom, const char *locationtag)
{
// allocate and append a new record
audit_record &record = m_record_list.append(*global_alloc(audit_record(*rom, audit_record::MEDIA_DISK)));
// open the disk
chd_file source;
chd_error err = chd_error(open_disk_image(m_enumerator.options(), &m_enumerator.driver(), rom, source, locationtag));
// if we succeeded, get the hashes
if (err == CHDERR_NONE)
{
hash_collection hashes;
// if there's a SHA1 hash, add them to the output hash
if (source.sha1() != sha1_t::null)
hashes.add_sha1(source.sha1());
// update the actual values
record.set_actual(hashes);
}
// compute the final status
compute_status(record, rom, err == CHDERR_NONE);
return &record;
}
//-------------------------------------------------
// compute_status - compute a detailed status
// based on the information we have
//-------------------------------------------------
void media_auditor::compute_status(audit_record &record, const rom_entry *rom, bool found)
{
// if not found, provide more details
if (!found)
{
// no good dump
if (record.expected_hashes().flag(hash_collection::FLAG_NO_DUMP))
record.set_status(audit_record::STATUS_NOT_FOUND, audit_record::SUBSTATUS_NOT_FOUND_NODUMP);
// optional ROM
else if (ROM_ISOPTIONAL(rom))
record.set_status(audit_record::STATUS_NOT_FOUND, audit_record::SUBSTATUS_NOT_FOUND_OPTIONAL);
// just plain old not found
else
record.set_status(audit_record::STATUS_NOT_FOUND, audit_record::SUBSTATUS_NOT_FOUND);
}
// if found, provide more details
else
{
// length mismatch
if (record.expected_length() != record.actual_length())
record.set_status(audit_record::STATUS_FOUND_INVALID, audit_record::SUBSTATUS_FOUND_WRONG_LENGTH);
// found but needs a dump
else if (record.expected_hashes().flag(hash_collection::FLAG_NO_DUMP))
record.set_status(audit_record::STATUS_GOOD, audit_record::SUBSTATUS_FOUND_NODUMP);
// incorrect hash
else if (record.expected_hashes() != record.actual_hashes())
record.set_status(audit_record::STATUS_FOUND_INVALID, audit_record::SUBSTATUS_FOUND_BAD_CHECKSUM);
// correct hash but needs a redump
else if (record.expected_hashes().flag(hash_collection::FLAG_BAD_DUMP))
record.set_status(audit_record::STATUS_GOOD, audit_record::SUBSTATUS_GOOD_NEEDS_REDUMP);
// just plain old good
else
record.set_status(audit_record::STATUS_GOOD, audit_record::SUBSTATUS_GOOD);
}
}
//-------------------------------------------------
// find_shared_device - return the source that
// shares a media entry with the same hashes
//-------------------------------------------------
device_t *media_auditor::find_shared_device(device_t &device, const char *name, const hash_collection &romhashes, UINT64 romlength)
{
bool dumped = !romhashes.flag(hash_collection::FLAG_NO_DUMP);
// special case for non-root devices
device_t *highest_device = nullptr;
if (device.owner() != nullptr)
{
for (const rom_entry *region = rom_first_region(device); region != nullptr; region = rom_next_region(region))
for (const rom_entry *rom = rom_first_file(region); rom != nullptr; rom = rom_next_file(rom))
if (ROM_GETLENGTH(rom) == romlength)
{
hash_collection hashes(ROM_GETHASHDATA(rom));
if ((dumped && hashes == romhashes) || (!dumped && ROM_GETNAME(rom) == name))
highest_device = &device;
}
}
else
{
// iterate up the parent chain
for (int drvindex = m_enumerator.find(m_enumerator.driver().parent); drvindex != -1; drvindex = m_enumerator.find(m_enumerator.driver(drvindex).parent))
{
for (device_t &scandevice : device_iterator(m_enumerator.config(drvindex).root_device()))
for (const rom_entry *region = rom_first_region(scandevice); region; region = rom_next_region(region))
for (const rom_entry *rom = rom_first_file(region); rom; rom = rom_next_file(rom))
if (ROM_GETLENGTH(rom) == romlength)
{
hash_collection hashes(ROM_GETHASHDATA(rom));
if ((dumped && hashes == romhashes) || (!dumped && ROM_GETNAME(rom) == name))
highest_device = &scandevice;
}
}
}
return highest_device;
}
//-------------------------------------------------
// audit_record - constructor
//-------------------------------------------------
audit_record::audit_record(const rom_entry &media, media_type type)
: m_next(nullptr),
m_type(type),
m_status(STATUS_ERROR),
m_substatus(SUBSTATUS_ERROR),
m_name(ROM_GETNAME(&media)),
m_explength(rom_file_size(&media)),
m_length(0),
m_shared_device(nullptr)
{
m_exphashes.from_internal_string(ROM_GETHASHDATA(&media));
}
audit_record::audit_record(const char *name, media_type type)
: m_next(nullptr),
m_type(type),
m_status(STATUS_ERROR),
m_substatus(SUBSTATUS_ERROR),
m_name(name),
m_explength(0),
m_length(0),
m_shared_device(nullptr)
{
}