type)) { $e->type = $this->type; } if (isset($this->name_offset)) { $e->name_offset = $this->name_offset; } if (isset($this->name)) { $e->name = $this->name; } if (isset($this->offset)) { $e->offset = $this->offset; } if (isset($this->parent_offset)) { $e->parent_offset = $this->parent_offset; } if (isset($this->misc)) { $e->misc = $this->misc; } if (isset($this->parent_entry)) { $e->parent_entry = $this->parent_entry; } if (isset($this->fst_offset)) { $e->fst_offset = $this->fst_offset; } if (isset($this->fst_local_sequence)) { $e->fst_local_sequence = $this->fst_local_sequence; } if (isset($this->dir_contents)) { $e->dir_contents = $this->dir_contents; } if (isset($this->payload_len)) { $e->payload_len = $this->payload_len; } return $e; } function unbuild_r ($files_dir, $fp_iso, $overwrite, $verbose) { $fn = $files_dir.DIRECTORY_SEPARATOR.$this->name; switch ($this->type) { case MKISO_FILETYPE_FILE: // extract the file ... if (file_exists($fn)) { if ($overwrite) { if (@is_dir($fn)) { print "ERROR: File exists and is actually a directory, cannot overwrite:\n". " ".$fn."\n"; die(); } else if (@is_link($fn)) { print "ERROR: File exists and is actually a symlink, cannot overwrite:\n". " ".$fn."\n"; die(); } // ok, overwrite } else { print "ERROR: File already exists, will not overwrite (use +ow to override this):\n". " ".$fn."\n"; die(); } } if (FALSE === ($fp_data = mkiso_mkdir_and_fopen ($fn, "wb"))) { print "ERROR: Could not open output file for writing:\n". " ".$fn."\n"; die(); } $bytes_rem = $this->misc; safe_fseek($fp_iso, $this->offset); while ($bytes_rem > 0) { $wanted = $bytes_rem; if ($wanted > MKISO_FINAL_PAD_BLOCKSIZE) { $wanted = MKISO_FINAL_PAD_BLOCKSIZE; } $buf = safe_fread($fp_iso, $wanted); if (FALSE === @fwrite($fp_data, $buf)) { print "ERROR: Could not write output file. Disc may be full:\n". " ".$fn."\n"; die(); } $bytes_rem -= $wanted; } @fclose($f_data); if ($verbose) { print "Wrote ".$fn."\n"; } break; default: // entry is a directory, call ourselves recursively if ($this->type === MKISO_FILETYPE_ROOT) { $fn = $files_dir.$this->name; // hack: root element already has dirsep, override fn } foreach ($this->dir_contents as $k=>$v) { $v->unbuild_r($fn, $fp_iso, $overwrite, $verbose); } break; } } function mkstring() { $ret = ""; switch ($this->type) { case MKISO_FILETYPE_FILE: $type="F"; break; case MKISO_FILETYPE_DIR: $type="D"; break; case MKISO_FILETYPE_ROOT: $type="R"; break; default: print "BUG: MKISO_File_Entry->mkstring(): this->type has illegal value\n"; die(); } $name=""; if (isset($this->name)) { $name = "(".$this->name.") "; } else { $name = "name @ ".sprintf("0x%0x", $this->name_offset)." "; } $parent_name=""; if (isset($this->parent_entry) && isset($this->parent_entry->name)) { $parent_name = "parent [".($this->parent_entry->mkpath())."] "; } else if (isset($this->parent_entry)) { $parent_name = "parent ".sprintf("0x%0x", $this->name_offset)." "; } $fst_off=""; if (isset($this->fst_off)) { $fst_off = "fst_off=".sprintf("0x%0x", $this->fst_off).") "; } $ret .= $type." ". $fst_off. //." ". $name. // if loaded $parent_name. "off ".sprintf("0x%0x", $this->offset). " misc ".sprintf("0x%0x", $this->misc). "\n"; return $ret; } // recursive: function mkstring_r() { $ret = ""; $ret .= $this->mkstring(); if (isset($this->dir_contents)) { foreach ($this->dir_contents as $k=>$v) { $ret .= $v->mkstring_r(); } } return $ret; } function num_children () { // unused? if (!isset($this->dir_contents)) return 0; return count($this->dir_contents); } // recursive: function num_children_r (&$n) { if (!isset($this->dir_contents)) return; foreach ($this->dir_contents as $k=>$entry) { if ($entry->type === MKISO_FILETYPE_DIR || $entry->type === MKISO_FILETYPE_ROOT) { $entry->num_children_r($n); } $n++; } } // recursive: function mkflatlists (&$flat_list_by_offset, //&$flat_list_fst_sequence_by_path, &$flat_list_by_new_fst_sequence, &$flat_list_by_path, &$smallest_offset, &$largest_offset) { //print $this->fst_sequence." mkflatlists: ".$this->mkpath().", fst_seq=".""."\n"; // flat_list_by_new_fst_sequence // because of the work done in scan_subdirectory(), these // already come out in the correct order so we just add them // blindly to the list $flat_list_by_new_fst_sequence[] = $this; // flat_list_by_path: // PHP is weird sometimes, hence the need for mkclone() $flat_list_by_path[strtolower($this->mkpath())] = $this->mkclone(); // flat_list_by_offset if (!$this->offset || isset($flat_list_by_offset[$this->offset]) || $this->type !== MKISO_FILETYPE_FILE) { // use the next available numerical offset key in flat_list, like this: $flat_list_by_offset[] = $this; } else { if ($this->offset < $smallest_offset) { $smallest_offset = $this->offset; } if ($this->offset > $largest_offset) { $largest_offset = $this->offset; } $k = $this->offset; $flat_list_by_offset[$k] = $this; // update the value in the array } $this->offset = 0; // wipe the offset because we need to recompute them all // and recurse: if (isset($this->dir_contents)) { foreach ($this->dir_contents as $seq => $entry) { $entry->mkflatlists($flat_list_by_offset, //$flat_list_fst_sequence_by_path, $flat_list_by_new_fst_sequence, $flat_list_by_path, $smallest_offset, $largest_offset); } } } function mkpath() { $lol = $this->parent_entry; $my_path=$this->name; // build path local to this node if ($this->type === MKISO_FILETYPE_ROOT) { return DIRECTORY_SEPARATOR; } while (isset($lol)) { // for the root case, name is "", therefore the root node prepends "/" $my_path = $lol->name . DIRECTORY_SEPARATOR . $my_path; // lol indeed $lol = $lol->parent_entry; } return $my_path; } } // ----- // START // ----- print "\n".MKISO_BANNER_1."\n"; print MKISO_BANNER_2."\n\n"; $fname_blob_boot = NULL; $fname_blob_bi2 = NULL; $fname_blob_appldr = NULL; $fname_blob_fst = NULL; $fname_iso = NULL; $fname_files_dir = NULL; $fname_blob_dol = NULL; $game_name = NULL; $game_code = NULL; $verbose = FALSE; $ignore_new_files = FALSE; $overwrite = FALSE; $build = FALSE; $align_list = NULL; $align_mode = NULL; $align_limit = NULL; $pack = FALSE; if (FALSE === parse_cli ($argv, $fname_blob_boot, $fname_blob_bi2, $fname_blob_appldr, $fname_blob_fst, $fname_blob_dol, $game_name, $game_code, $ignore_new_files, $fname_iso, $fname_files_dir, $verbose, $overwrite, $build, $align_mode, $align_limit, $align_list, $pack)) { // pack not used //print mkiso_usage()."\n"; die(); } if ($build) { print "Build mode.\n"; mkiso_build($fname_blob_boot, $fname_blob_bi2, $fname_blob_appldr, $fname_blob_fst, $fname_blob_dol, $fname_iso, $fname_files_dir, $game_name, $game_code, $ignore_new_files, $verbose, $overwrite, $align_mode, $align_limit, $align_list, $pack); } else { print "Unbuild mode.\n"; mkiso_unbuild($fname_blob_boot, $fname_blob_bi2, $fname_blob_appldr, $fname_blob_fst, $fname_blob_dol, $fname_iso, $fname_files_dir, $verbose, $overwrite); } die(); // ***** // FIN // ***** function unbuild_check_blob ($blob_fname, $overwrite, $blob_name) { if (@file_exists($blob_fname)) { if (@is_dir($blob_fname)) { if ($overwrite) { print "ERROR: ".$blob_name." already exists and is a directory, cannot overwrite:\n". " ".$blob_fname."\n"; } else { print "ERROR: ".$blob_name." already exists and is a directory:\n". " ".$blob_fname."\n"; } } else if (@is_link($blob_fname)) { if ($overwrite) { print "ERROR: ".$blob_name." already exists and is a symlink, cannot overwrite:\n". " ".$blob_fname."\n"; } else { print "ERROR: ".$blob_name." already exists and is a symlink:\n". " ".$blob_fname."\n"; } } else { if ($overwrite) { return TRUE; } else { print "ERROR: ".$blob_name." file already exists. Use +ow to force overwrite:\n". " ".$blob_fname."\n"; } } return FALSE; } return TRUE; } function mkiso_unbuild($fname_blob_boot, $fname_blob_bi2, $fname_blob_appldr, $fname_blob_fst, $fname_blob_dol, $fname_iso, $fname_files_dir, $verbose, $overwrite) { if (FALSE === unbuild_check_blob($fname_blob_boot, $overwrite, "Boot blob")) { die(); } if (FALSE === unbuild_check_blob($fname_blob_bi2, $overwrite, "BI2 blob")) { die(); } if (FALSE === unbuild_check_blob($fname_blob_appldr, $overwrite, "Apploader blob")) { die(); } if (FALSE === unbuild_check_blob($fname_blob_fst, $overwrite, "FST blob")) { die(); } if (FALSE === unbuild_check_blob($fname_blob_dol, $overwrite, "DOL blob")) { die(); } if (!@file_exists($fname_files_dir)) { // create files directory if (FALSE === mkiso_mkdir($fname_files_dir)) { print "ERROR: Could not create output files directory:\n". " ".$fname_files_dir."\n"; die(); } } else { // files directory already exists, but is it a directory? if (!@is_dir($fname_files_dir)) { print "ERROR: Output files directory already exists but is not a directory:\n". " ".$fname_files_dir."\n"; die(); } } // loading the whole ISO at once would be a bit antisocial so ... if (FALSE === ($f_iso = @fopen ($fname_iso, "rb"))) { print "ERROR: Could not open source ISO file:\n". " ".$fname_iso."\n"; die(); } // extract the blobs ... print "Reading boot blob ...\n"; $blob_boot = safe_fread($f_iso, 0x440); print "Reading BI2 blob ...\n"; $blob_bi2 = safe_fread($f_iso, 0x2000); print "Reading apploader header ...\n"; $appldr_header = safe_fread($f_iso, 0x20); $appldr_main_len = -1; $appldr_trailer_len = -1; print "Validating apploader header ...\n"; if (FALSE === validate_blob_appldr ($appldr_header, 1, // header only $appldr_main_len, $appldr_trailer_len)) { die(); } print "Reading apploader ...\n"; $blob_appldr = $appldr_header . safe_fread($f_iso, $appldr_main_len + $appldr_trailer_len); print "Writing BI2 blob: ".$fname_blob_bi2."\n"; if (FALSE === (mkiso_file_put_contents ($fname_blob_bi2, $blob_bi2))) { print "ERROR: Could not write BI2 blob.\n"; die(); } print "Writing apploader blob: ".$fname_blob_appldr."\n"; if (FALSE === (mkiso_file_put_contents ($fname_blob_appldr, $blob_appldr))) { print "ERROR: Could not write apploader blob.\n"; die(); } $offset_subtract = 0; //0x440 + 0x2000 + strlen($blob_appldr); $fst = NULL; $dol_offset = -1; $fst_offset = -1; $fst_size = -1; print "Validating boot blob: ".$fname_blob_boot."\n"; if (FALSE === parse_boot_blob ($blob_boot, $offset_subtract, $dol_offset, $fst_offset, $fst_size)) { die(); } print "Writing boot blob: ".$fname_blob_boot."\n"; if (FALSE === (mkiso_file_put_contents ($fname_blob_boot, $blob_boot))) { print "ERROR: Could not write boot blob.\n"; die(); } // work out how big DOL is safe_fseek($f_iso, $dol_offset); $dol_header_blob = safe_fread($f_iso, 0x100); $dol_size = determine_dol_size($dol_header_blob); // copy DOL $blob_dol = $dol_header_blob.safe_fread($f_iso, $dol_size - 0x100); print "Writing DOL blob: ".$fname_blob_dol."\n"; if (FALSE === (mkiso_file_put_contents ($fname_blob_dol, $blob_dol))) { print "ERROR: Could not write DOL blob.\n"; die(); } safe_fseek($f_iso, $fst_offset); $fst = safe_fread($f_iso, $fst_size); //print grenola_hexdump($fst, 0); $fst_root_entry = NULL; $dummy = -1; if (FALSE === parse_fst_new ($fst, $blob_boot, $fst_root_entry, // PBR $dummy, // PBR $verbose)) { die(); } print "Writing FST blob: ".$fname_blob_fst."\n"; if (FALSE === (mkiso_file_put_contents ($fname_blob_fst, $fst))) { print "ERROR: Could not write FST blob.\n"; die(); } // extract all the files: print "Please wait, extracting to ".$fname_files_dir." ...\n"; $fst_root_entry->unbuild_r ($fname_files_dir, $f_iso, $overwrite, $verbose); print "Done.\n"; } function determine_dol_size($dol_header_blob) { if (strlen($dol_header_blob) !== 0x100) { print "ERROR: determine_dol_size: dol_header_blob has bad length (".strlen($dol_header_blob).")\n"; die(); } // find which section in the DOL is the last one // (we just number the sections 0 to 17) $i=0x0; $final_section_number = 0; $final_section_pos = 0; for ($j=0;$j<17;$j++) { $pos = stream_32bit_unsigned_bigendian($dol_header_blob, $i); // advances $i if ($pos > $final_section_pos) { $final_section_pos = $pos; $final_section_number = $j; } } //print "DOL final section is section ".$final_section_number." at ".sprintf("0x%x", $final_section_pos)."\n"; // now find the size of this section $i = 0x90 + $final_section_number * 4; $size = stream_32bit_unsigned_bigendian($dol_header_blob, $i); $size += $final_section_pos; if ($size > MKISO_DOL_SIZE_MAX || $size < MKISO_DOL_SIZE_MIN) { print "ERROR: DOL reported size is insane (".sprintf("0x%x", $size)."\n"; die(); } if ($size > MKISO_DOL_SIZE_WARN) { print "WARNING: DOL reported size is insane (". sprintf("0x%x", $size). ", continuing anyway\n"; } print "DOL size: ".sprintf("0x%x (%s)", $size, number_format($size))."\n"; return $size; } function safe_fseek($fp, $offset) { if (FALSE === fseek($fp, $offset, SEEK_SET)) { print "ERROR: safe_fseek() could not seek to offset ".sprintf("0x%x", $offset)."\n"; die(); } } function safe_fread ($fp, $len) { if ($len <= 0) { print "BUG: safe_fread() called with bad length (".number_format($len).")\n"; die(); } if (FALSE === ($buf = fread($fp, $len))) { print "ERROR: safe_fread() failed.\n"; die(); } if (strlen($buf) !== $len) { print "ERROR: safe_fread() was truncated.\n"; die(); } return $buf; } function mkiso_mkdir($path) { if (!isset($path) || $path==="") { print "BUG: mkiso_mkdir(): path is NULL or zero-length.\n"; die(); } if (@is_dir($path)) { return TRUE; // "base case", aka a hack } if (FALSE === @mkdir($path, 0755, true)) { print "ERROR: Could not create directory ".$path."\n"; return FALSE; } return TRUE; } function mkiso_file_put_contents($fname, $data) { // need to create directories if the path contains them // FIXME: move common code to function $boom = explode(DIRECTORY_SEPARATOR, $fname); if (FALSE !== array_pop($boom)) { $directory = implode(DIRECTORY_SEPARATOR, $boom); } if ($directory !== "" && !@is_dir($directory) && (FALSE === mkiso_mkdir($directory))) { return FALSE; } return file_put_contents($fname, $data); } function mkiso_mkdir_and_fopen($fname, $mode) { // need to create directories if the path contains them // FIXME: move common code to function $boom = explode(DIRECTORY_SEPARATOR, $fname); if (FALSE !== array_pop($boom)) { $directory = implode(DIRECTORY_SEPARATOR, $boom); } if ($directory !== "" && !@is_dir($directory) && (FALSE === mkiso_mkdir($directory))) { return FALSE; } return fopen($fname, $mode); } function mkiso_get_extension($path) { $boom = explode(".", $path); if (count($boom) < 2) { return ""; } else { return strtolower($boom[count($boom)-1]); } } function mkiso_build($fname_blob_boot, $fname_blob_bi2, $fname_blob_appldr, $fname_blob_fst, $fname_blob_dol, $fname_iso, $fname_files_dir, $game_name, $game_code, $ignore_new_files, $verbose, $overwrite, $align_mode, $align_limit, $align_list, $pack) { if (!isset($align_mode)) { $align_mode = MKISO_ALIGN_MODE_EXT; // use extension mode by default } if (@is_dir($fname_blob_boot)) { print "ERROR: Supplied boot.bin is actually a directory.\n"; die(); } if (@is_dir($fname_blob_bi2)) { print "ERROR: Supplied bi2.bin is actually a directory.\n"; die(); } if (@is_dir($fname_blob_appldr)) { print "ERROR: Supplied appldr.bin is actually a directory.\n"; die(); } if (@is_dir($fname_blob_fst)) { print "ERROR: Supplied fst.bin is actually a directory.\n"; die(); } if (@is_dir($fname_blob_dol)) { print "ERROR: Supplied main.dol is actually a directory.\n"; die(); } if (@is_dir($fname_iso)) { print "ERROR: Supplied destination ISO already exists, and is actually a directory.\n"; die(); } if (@file_exists($fname_iso) && !$overwrite) { print "ERROR: Supplied destination ISO already exists. Use +ow to force overwrite.\n"; die(); } if (!(@file_exists($fname_files_dir))) { if (FALSE === mkiso_mkdir($fname_files_dir)) { print "ERROR: Files directory does not exist, could not be created: ".$fname_files_dir.".\n"; die(); } } /* if (!(@file_exists($fname_sys_dir))) { if (FALSE === mkiso_mkdir($fname_sys_dir)) { print "ERROR: Sys directory does not exist, could not be created: ".$fname_sys_dir.".\n"; die(); } } */ if (!(@is_dir($fname_files_dir))) { print "ERROR: Files directory is not a directory: ".$fname_files_dir.".\n"; die(); } if (FALSE === ($blob_boot = @file_get_contents($fname_blob_boot))) { print "ERROR: Could not load boot blob ".$fname_blob_boot."\n"; die(); } if (FALSE === validate_blob_boot ($blob_boot)) { die(); } if (FALSE === ($blob_bi2 = @file_get_contents($fname_blob_bi2))) { print "ERROR: Could not load BI2 blob ".$fname_blob_bi2."\n"; die(); } if (FALSE === validate_blob_bi2 ($blob_bi2)) { die(); } if (FALSE === ($blob_appldr = @file_get_contents($fname_blob_appldr))) { print "ERROR: Could not load apploader blob ".$fname_blob_appldr."\n"; die(); } $appldr_len = 0; $appldr_trailer_len = 0; // FIXME: get this function to split appldr and trailer and return them in different variables? if (FALSE === validate_blob_appldr ($blob_appldr, 0, // not just the header $appldr_len, $appldr_trailer_len)) { die(); } if (FALSE === ($blob_fst = @file_get_contents($fname_blob_fst))) { print "ERROR: Could not load FST blob ".$fname_blob_fst."\n"; die(); } // we don't need a validate_blob_fst() because we have parse_fst() if (FALSE === ($blob_dol = @file_get_contents($fname_blob_dol))) { print "ERROR: Could not load DOL blob ".$fname_blob_dol."\n"; die(); } if (FALSE === validate_blob_dol ($blob_dol)) { die(); } print "Loaded all blobs.\n"; if (isset($game_name) && (!ctype_print($game_name) || strlen($game_name)>MKISO_GAMENAME_LEN)) { print "ERROR: Illegal game name: ".$game_name."\n"; die(); } if (isset($game_code) && (strlen($game_code)!==2 || !ctype_alnum($game_code))) { print "ERROR: Illegal game code ( ".grenola_hexdump($game_code, 1)."), should be two alphanumerical characters.\n"; die(); } // parse the FST to get the order //$offset_subtract = 0x440 + 0x2000 + strlen($blob_appldr); $fst = NULL; $old_fst_offset = -1; // FIXME: this function is obsolete // make calls to parse_boot_blob() and parse_fst_new() instead if (FALSE === parse_fst_old ($blob_fst, $blob_boot, 0, // offset_subtract $fst, $old_fst_offset, $verbose)) { // fst PBR die(); } // in order to parse the on-disc files, we will need a list of paths. // we get this by calling mkflatlists() on $fst $flatlist_by_offset = array(); $dummy2 = array(); $smallest_offset = 0x7fffffff; $largest_offset = 0; $flatlist_by_path = array(); $sequence_correction=0; // used internally by mkflatlists() $fst->mkflatlists ($flatlist_by_offset, $dummy2, $flatlist_by_path, $smallest_offset, $largest_offset); // PBR print "Smallest offset from FST is ".sprintf("0x%x", $smallest_offset). " (absolute = ".sprintf("0x%x", $smallest_offset+$old_fst_offset).").\n"; print "Largest offset from FST is ".sprintf("0x%x", $largest_offset). " (absolute = ".sprintf("0x%x", $largest_offset+$old_fst_offset).").\n"; $align_by_extension = NULL; if ($align_mode === MKISO_ALIGN_MODE_EXT) { $align_by_extension = array(); foreach ($flatlist_by_offset as $off => $entry) { //print $entry->mkstring(); if ($entry->type !== MKISO_FILETYPE_FILE) { continue; } $ext = mkiso_get_extension($entry->mkpath()); if (!isset($align_by_extension[$ext])) { $align_by_extension[$ext] = 32; } // use the offset with the least number of zeroes // in the low bits for ($i=0;$i<32;$i++) { if ($off&1) { break; } $off>>=1; } if ($i < $align_by_extension[$ext]) { $align_by_extension[$ext] = $i; } } // convert from bit number to actual value foreach ($align_by_extension as $ext => $bitnum) { $mask = 1; for ($i=0;$i<$bitnum;$i++) { $mask <<= 1; $mask &= 0x7ffffffe; } $align_by_extension[$ext] = $mask; // overwrite } print "Alignments from file extensions:\n"; foreach ($align_by_extension as $ext => $value) { print " ".sprintf("0x%x", $value); if ($ext !== "") { print " *.".$ext."\n"; } else { print " \n"; } } } //print_r($align_by_extension); die(); print "Checking path against FST: ".($fname_files_dir)."\n"; // parse the source directory, check files $files_on_disc = new MKISO_File_Entry; // root directory $files_on_disc->type = MKISO_FILETYPE_ROOT; $files_on_disc->name = ""; $files_on_disc->fst_sequence = 0; scan_subdirectory ($flatlist_by_path, $fname_files_dir, $fname_files_dir, 0, $files_on_disc, $verbose, $ignore_new_files); $flatlist_by_offset = array(); //$flatlist_fst_sequence_by_path = array(); $flatlist_by_new_fst_sequence = array(); $dummy2 = 0x7fffffff; $dummy3 = 0; $dummy1=array(); $files_on_disc->mkflatlists ($flatlist_by_offset, $flatlist_by_new_fst_sequence, $dummy1, $dummy2, // don't care about the offsets $dummy3); // PBR ksort($flatlist_by_offset, SORT_NUMERIC); // we have the flatlists, now we can actually build the ISO // build the new FST first, then patch new DOL pointer/offset/size/max size into old boot.bin // 0 | boot.bin (patched) // 0x440 | bi2.bin (stock) // 0x2440 | appldr.bin (stock) // <------- (some games put the DOL here?) -------> // 0x2440 + len(appldr) | fst.bin + name table (rebuilt) // 0x2440 + len(appldr) + (0xc * len(flatlist)) + len(name table) |
// 0x2440 + len(appldr) + (0xc * len(flatlist)) + len(name table) + len(dol) | $start_of_fst_offset = 0x440 + 0x2000 + strlen($blob_appldr); print "New FST starts at ".sprintf("0x%x.\n", $start_of_fst_offset); // we can work out the start of the data by summing all of the name // lengths in flatlist $nametable_len = 0; foreach ($flatlist_by_new_fst_sequence as $sequence => $entry) { $nametable_len += strlen($entry->name) + 1; // +1 for null terminator } $fst_predicted_len = (count($flatlist_by_new_fst_sequence) * 0x0c); //$fst_predicted_len += calculate_padding_len(MKISO_BLOCK_ALIGNMENT, $fst_predicted_len, 1); print "New nametable starts at FST + ".sprintf("0x%0x", $fst_predicted_len).".\n"; $fst_predicted_len += $nametable_len; // + $nametable_pad_len; $fst_predicted_len += calculate_padding_len(0x20, $fst_predicted_len, 1); //if ( print "New FST total predicted length is ".sprintf("0x%x", $fst_predicted_len).".\n"; $start_of_data_offset = $start_of_fst_offset + $fst_predicted_len; //(0xc * count($flatlist)) + $nametable_len + $nametable_pad_len; $start_of_data_pad_len = calculate_padding_len(MKISO_BLOCK_ALIGNMENT, $start_of_data_offset, 1); print "New data offset start predicted to be ".sprintf("0x%x", $start_of_data_offset). ", pad to at least ".sprintf("0x%x", $start_of_data_pad_len+$start_of_data_offset)."\n"; // OK, try putting the DOL after the FST $next_file_offset = $start_of_data_pad_len + $start_of_data_offset; $next_file_offset += calculate_padding_len (MKISO_BLOCK_ALIGNMENT, $next_file_offset, 1); $dol_offset = $next_file_offset; $next_file_offset += strlen($blob_dol); $next_file_offset += calculate_padding_len (MKISO_BLOCK_ALIGNMENT, $next_file_offset, 1); print "Placing new DOL at ".sprintf("0x%x", $dol_offset).".\n"; // next thing to do is to build an intermediate table of offsets, now // we know where the data lies //$new_file_offsets = array(); $new_file_offsets = build_new_offsets_table($flatlist_by_offset, $flatlist_by_path, $fst, $next_file_offset, $verbose, $align_mode, $align_limit, $align_list, $pack, $align_by_extension); $flatlist_by_fst_sequence_clone = array(); $blob_fst_new = ""; $cur_fst_offset = 0; $found_root=0; $new_fst = ""; $new_name_table = ""; // for looking up the offsets of previously-encountered parents as we go: $flatlist_by_path_with_new_fst_offset = array(); foreach ($flatlist_by_new_fst_sequence as $whatever => $entry) { $name_offset = strlen($new_name_table); // FIXME ? is offset correct $new_name_table .= $entry->name . chr(0); $path = strtolower($entry->mkpath()); switch ($entry->type) { case MKISO_FILETYPE_ROOT: if ($found_root) { print "BUG: two root entries?!\n"; die(); } $found_root = 1; $flags = 1; // field B is parent_offset -- N/A for root $field_b = 0; $field_c = 1 + ($cur_fst_offset / 0x0c); $entry->num_children_r($field_c, 0); // PBR, 1 = don't count directories //$field_c *= 0x0c; //print "num_children=".$field_c."\n"; break; case MKISO_FILETYPE_DIR: if (!$found_root) { print "BUG: first entry is not root entry: ".$path."\n"; die(); } $flags = 1; //print "parent_entry=".$entry->parent_entry->mkpath()."\n"; if (!isset($entry->parent_entry)) { print "BUG: directory node has no parent.\n"; die(); } $p = strtolower($entry->parent_entry->mkpath()); if (!isset($flatlist_by_path_with_new_fst_offset[$p])) { print "ERROR: trying to get offset of parent we have not encountered (".$p.").\n"; die(); } $better_parent = $flatlist_by_path_with_new_fst_offset[$p]; if (!isset($better_parent->fst_offset)) { print "ERROR: trying to get offset of parent we have not encountered (".$p.").\n"; die(); } // field B is parent_offset $field_b=0; //print "parent is ".$better_parent->mkpath()."\n"; $field_b = $better_parent->fst_offset; //$last_dir_offset = $cur_fst_offset; $field_c = 1 + ($cur_fst_offset / 0x0c); $entry->num_children_r($field_c, 0); // PBR, 1 = don't count directories //$field_c *= 0x0c; //print "field_c=".$field_c."\n"; //if (!$field_c) { print ("!field_c\n"); die(); } break; case MKISO_FILETYPE_FILE: if (!$found_root) { print "BUG: first entry is not root entry: ".$path."\n"; die(); } $flags = 0; // field B is file_offset //$field_b = $next_file_offset; // + calculate_padding_len(MKISO_BLOCK_ALIGNMENT, $last_file_offset, 1); if (!isset($new_file_offsets[$path])) { print "ERROR: path \"".$path."\" not found in new_file_offsets\n"; die(); } $field_b = $new_file_offsets[$path]; //$field_c = strlen($entry->file_contents); // FIXME: should this be padded or not? $field_c = $entry->payload_len; break; } $field_a = (($flags<<24)&0xff000000) | ($name_offset&0xffffff); $blob_fst_new .= to_bigendian32($field_a) . to_bigendian32($field_b) . to_bigendian32($field_c); $entry->fst_offset = $cur_fst_offset; //$next_file_offset += strlen($entry->file_contents); //$next_file_offset += $entry->payload_len; //$next_file_offset += calculate_padding_len(MKISO_BLOCK_ALIGNMENT, $next_file_offset, 1); if ($verbose) { print sprintf("fst[%08x] = %08x %08x %08x %s\n", $entry->fst_offset, (($flags<<24)&0xff000000) | ($name_offset&0xffffff), $field_b, $field_c, //$entry->fst_local_sequence, $path); } // store the entry in a new flatlist_by_path, so we can look // up earlier-encountered parents with known new FST offsets $flatlist_by_path_with_new_fst_offset[$path] = $entry; $cur_fst_offset += 0x0c; } if (!$found_root) { print "BUG: no root entry\n"; die(); } $blob_fst_new .= $new_name_table; $pad = calculate_padding_len(0x20, strlen($blob_fst_new), 1); $blob_fst_new .= str_repeat(chr(0), $pad); if (strlen($blob_fst_new) !== $fst_predicted_len) { print "BUG: fst len (".sprintf("0x%08x", strlen($blob_fst_new)). ") != fst predicted len (".sprintf("0x%08x", $fst_predicted_len). ").\n"; die(); } print "New FST and name table built, length ".sprintf("0x%0x", strlen($blob_fst_new)).".\n"; // OK, patch boot.bin print "patching boot.bin:\n"; if (isset($game_name)) { print "GN: ".$game_name."\n"; $game_name .= str_repeat(chr(0x00), MKISO_GAMENAME_LEN - strlen($game_name)); if (strlen($game_name) !== MKISO_GAMENAME_LEN) { print "BUG: strlen(game_name)=".sprintf("0x%x", strlen($game_name)).", should be 0x3e0.\n"; die(); } for ($j=0;$j"); } safe_fwrite($f_iso, substr($blob_boot, 0, 0x440)); // boot.bin if ($verbose) { printf("ISO: [%08x] prepad %08x len %08x %s\n", 0x440, 0, 0x2000, ""); } safe_fwrite($f_iso, substr($blob_bi2, 0, 0x2000)); // bi2.bin if ($verbose) { printf("ISO: [%08x] prepad %08x len %08x %s\n", 0x440 + 0x2000, 0, strlen($blob_appldr), ""); } safe_fwrite($f_iso, substr($blob_appldr, 0, strlen($blob_appldr))); // apploader if ($verbose) { printf("ISO: [%08x] prepad %08x len %08x %s\n", 0x440 + 0x2000 + strlen($blob_appldr), 0, strlen($blob_fst_new), ""); } safe_fwrite($f_iso, $blob_fst_new); // FST, includes the name table $cur_off = 0x2440 + strlen($blob_fst_new) + strlen($blob_appldr); // next thing is the DOL(OLOL) $pad = $dol_offset - $cur_off; if ($pad < 0) { print "BUG: pre-DOL padding is negative in length.\n"; die(); } safe_fwrite($f_iso, str_repeat(chr(0), $pad)); $cur_off = $dol_offset; if ($verbose) { printf("ISO: [%08x] prepad %08x len %08x %s\n", $cur_off, $pad, strlen($blob_dol), ""); } safe_fwrite($f_iso, $blob_dol); $cur_off += strlen($blob_dol); // the rest comes from the offset table foreach ($flatlist_by_offset as $whatever => $entry) { if ($entry->type !== MKISO_FILETYPE_FILE) { continue; } $next_offset = $new_file_offsets[strtolower($entry->mkpath())]; $pad = $next_offset - $cur_off; if ($pad<0) { print "ERROR: writing ISO: offset precedes current position.\n"; die(); } // pad to next offset safe_fwrite($f_iso, str_repeat(chr(0), $pad)); //safe_fwrite($f_iso, $entry->file_contents); $p = $fname_files_dir.DIRECTORY_SEPARATOR.$entry->mkpath(); if (FALSE === ($payload = file_get_contents ($p))) { print "ERROR: Source file went missing: ".$p."\n"; die(); } if (strlen($payload) !== $entry->payload_len) { print "ERROR: Length of source file has changed: ".$p."\n"; die(); } safe_fwrite($f_iso, $payload); if ($verbose) { printf("ISO: [%08x] prepad %08x len %08x %s\n", $next_offset, $pad, $entry->payload_len, $entry->mkpath()); } $cur_off += $pad + $entry->payload_len; if ($cur_off > MKISO_CAPACITY) { print "ERROR: Image size exceeded GameCube disc capacity.\n"; print " Maybe try reducing MKISO_MAX_ALIGNMENT ?\n"; die(); } } // pad to end of file if ($cur_off < MKISO_CAPACITY) { $total_padding = MKISO_CAPACITY - $cur_off; print "Padding to capacity (".number_format($total_padding)." bytes).\n"; $current_padding=0; while ($current_padding < $total_padding) { $want = $total_padding - $current_padding; if ($want > MKISO_FINAL_PAD_BLOCKSIZE) { $want = MKISO_FINAL_PAD_BLOCKSIZE; } safe_fwrite($f_iso, str_repeat(chr(0), $want)); $current_padding += $want; } } fclose($f_iso); print "Wrote \"".$fname_iso."\".\n"; print "Done.\n"; } class AlignThing { var $path; var $align; var $glob_fragments; function mkstring() { $s = ""; $s .= $this->path." ".sprintf("0x%x", $this->align); $t=""; foreach($this->glob_fragments as $k=>$v) { $t.=$v."|"; } if (strlen($t)) { $s.="[".substr($t,0,-1)."]"; } return $s; } } function mkiso_glob_match ($things, $file) { // slow :( foreach ($things as $whatever => $thing) { $last_win_offset=-1; // all the non-zero-length fragments must appear in the filename, in order $win = 1; $numfrags = count($thing->glob_fragments); for ($i=0;$i<$numfrags;$i++) { // foreach ($thing->glob_fragments as $whatever => $fragment) { $fragment = $thing->glob_fragments[$i]; if ($fragment === "") { // wildcard piece // skip continue; } if ((FALSE === ($win_offset = strpos($file, $fragment))) // fragment not found || (!$i && $win_offset) // or this is the first fragment and it wasn't found at [0] || (($i===$numfrags-1) && ($win_offset !== strlen($file) - strlen($fragment)))) { // or this is the last fragment and it wasn't found at the end $win = 0; break; } if ($win_offset <= $last_win_offset) { $win = 0; break; } } if ($win) { return $thing; } } return FALSE; } function build_new_offsets_table($flatlist_by_offset, $flatlist_by_path, $fst, $next_file_offset, $verbose, $align_mode, $align_limit, $align_list, $pack, $align_by_extension) { $num_alignment_violations = 0; $new_file_offsets = array(); // setup if (isset($align_list)) { $glob_align_list = array(); // need to do some work on the align list and get any // glob patterns out into a separate list $align_list_clone = array(); foreach ($align_list as $path => $align_value) { $align_list_clone[$path] = $align_value; //print "path: ".$path."\n"; } $things = array(); foreach ($align_list_clone as $path => $align_value) { //print "path: ".$path."\n"; $boom = explode("*", $path); //print_r($boom);print"\n"; if (count($boom)<2) { // it's not a glob match, so make sure it starts with a slash if ($path[0] !== DIRECTORY_SEPARATOR) { unset($align_list[$path]); $align_list[DIRECTORY_SEPARATOR.$path] = $align_value; } } else { // it's a glob match, move to glob list unset($align_list[$path]); $at = new AlignThing; $at->path = $path; $at->align = $align_value; $last_v="|"; foreach ($boom as $k=>$v) { if ($v==="" && $last_v==="") { $last_v = $v; continue; } $last_v = $v; $at->glob_fragments[] = $v; } $things[] = $at; } } } //if ($verbose) { if (isset($align_list) || isset($things)) { print "build_new_offsets_table():\n"; print " Align list:\n"; foreach ($align_list as $k=>$v) { print " ".$k." 0x".sprintf("%x", $v)."\n"; } print " Glob list:\n"; foreach ($things as $k=>$v) { print " ".$v->mkstring()."\n"; } } //} foreach ($flatlist_by_offset as $sequence => $entry) { if (isset($align)) { unset ($align); } $path = strtolower($entry->mkpath()); $forced_align = NULL; if (isset($new_file_offsets[$path])) { print "BUG: duplicate paths found in flatlist_by_offset\n"; die(); } if ($entry->type !== MKISO_FILETYPE_FILE) { continue; } // OK, if we have an align_list then we need to check // to see if this entry matches anything in the align_list // do the glob matches first // beware: we only use the alignment value from the first // glob match, subsequent matches are ignored if (isset($things) && count($things)) { if (FALSE !== ($thing = mkiso_glob_match($things, $path))) { if ($verbose) { print "GLOB MATCH: ".$path." matches ".$thing->mkstring()."\n"; } $forced_align = $thing->align; } } if (isset($align_list) && isset($align_list[$path])) { if ($verbose) { print "ALIGN LIST MATCH: ".$path." :: ".$align_list[$path]."\n"; } $forced_align = $align_list[$path]; // the align list overrides any globmatchery } if ($align_mode === MKISO_ALIGN_MODE_SRC) { // align from source file //print $path."\n"; if (isset($fst) && isset($flatlist_by_path[$path])) { $old_entry = $flatlist_by_path[$path]; $old_offset = $old_entry->offset; //print "old_offset=".sprintf("%x", $old_offset)." for path ".$path."\n"; $align = 1; while (!($old_offset & 1)) { $old_offset >>= 1; $align <<= 1; if ($align > MKISO_MAX_ALIGNMENT) { if ($verbose) { print "WARNING: MAX_ALIGNMENT exceeded (".$path. "), using ".sprintf("0x%x\n", MKISO_MAX_ALIGNMENT); } $num_alignment_violations++; $align = MKISO_MAX_ALIGNMENT; break; } } } else { $align = MKISO_BLOCK_ALIGNMENT; // just use default alignment } } else if ($align_mode === MKISO_ALIGN_MODE_EXT) { // align by extension group $ext = mkiso_get_extension($path); if (isset($align_by_extension[$ext])) { $align = $align_by_extension[$ext]; } } if (!isset($align)) { if ($verbose) { print "WARNING: Using default ".sprintf("0x%x", MKISO_BLOCK_ALIGNMENT). " alignment for file:\n"; print $path."\n"; } //die(); $align = MKISO_BLOCK_ALIGNMENT; } // this is a hack but it was needed to make // extension-align work by default with some games (hi, Burnout) // so the alignment is always >= 0x20 no matter what we // detected if ($align < MKISO_ALIGN_MIN) { $align = MKISO_ALIGN_MIN; } // however, finer alignments may still be forced by explicit order if ($forced_align) { print "Forcing ".sprintf("0x%x", $forced_align)." alignment for ".$path."\n"; $align = $forced_align; } // work out prepadding //$next_file_offset += calculate_padding_len ($align, $next_file_offset, 1); $overspill = $next_file_offset & ($align-1); if ($overspill) { $next_file_offset += ($align - $overspill); } $new_file_offsets[$path] = $next_file_offset; //$next_file_offset += strlen($entry->file_contents); $next_file_offset += $entry->payload_len; //$next_file_offset += calculate_padding_len ($align, $next_file_offset, 1); //$next_file_offset += } if ($num_alignment_violations) { print "WARNING: new offset table: ".number_format($num_alignment_violations). " alignment violations.\n"; } return $new_file_offsets; } function safe_fwrite ($fptr, $buf) { if (FALSE===fwrite($fptr, $buf)) { print "ERROR: safe_fwrite() failed -- are you out of disc space?\n"; die(); } } function to_bigendian32 ($i) { return chr(($i>>24)&0xff). chr(($i>>16)&0xff). chr(($i>>8)&0xff). chr($i&0xff); } function calculate_padding_len ($modulus, $block_len, $round_down) { //if ($block_len === $modulus) { // return 0; // if it looks like a hack and quacks like a hack //} //$ret = -1; if ($round_down || ($block_len & ($modulus-1))) { $ret = $modulus - ($block_len & ($modulus-1)); } //print "calculate_padding_len(0x".sprintf("%x", $modulus). // ", 0x".sprintf("%x", $block_len).", ".$round_down. //")=".$ret."\n"; return $ret; } function parse_boot_blob ($blob_boot, $offset_subtract, &$dol_offset, &$fst_offset, &$fst_size) { print "----- begin boot blob -----\n"; $off = 0x0420; $dol_offset = stream_32bit_unsigned_bigendian($blob_boot, $off); if (($dol_offset < 0) || ($dol_offset > MKISO_DOL_OFF_MAX)) { print "ERROR: boot blob: bad DOL offset (".sprintf("0x%x", $dol_offset). ", max ".sprintf("0x%x", MKISO_DOL_OFF_MAX). ", blob_fst_len ".sprintf("0x%x", $blob_fst_len).")\n"; return FALSE; } print "DOL offset: ".sprintf("0x%x", $dol_offset)."\n"; $off = 0x0424; $fst_offset = stream_32bit_unsigned_bigendian($blob_boot, $off); $fst_offset -= $offset_subtract; if (($fst_offset < 0)) { print "ERROR: boot blob: bad fst offset (".sprintf("0x%x", $fst_offset). ", max ".sprintf("0x%x", MKISO_FST_OFF_MAX). ", blob_fst_len ".sprintf("0x%x", $blob_fst_len).")\n"; return FALSE; } print "FST offset: ".sprintf("0x%x", $fst_offset)."\n"; $off = 0x0428; // this fst_size is the whole thing, string table and everything $fst_size = stream_32bit_unsigned_bigendian($blob_boot, $off); if ( ($fst_size <= 0) || ($fst_size > MKISO_FST_SIZE_MAX)) { print "ERROR: boot blob: bad fst size (".number_format($fst_size).")\n"; return FALSE; } print "FST size from boot blob (includes name table): ".sprintf("0x%0x", $fst_size).".\n"; print "----- end boot blob -----\n"; return TRUE; } function parse_fst_old ($blob_fst, $blob_boot, $offset_subtract, &$fst_root, &$fst_off, $verbose) { $dummy1 = -1; $dummy2 = -1; $dummy3 = -1; // this just checks blob_boot is sane. this has nothing // to do with the FST, but it was part of the function of // the old parse_fst() so it's in this compatibility wrapper // really the code that calls this should just make // two separate calls instead //print "FIXME: remove parse_fst_old()\n"; if (FALSE === parse_boot_blob ($blob_boot, $offset_subtract, $dummy1, $dummy2, $dummy3)) { return FALSE; } if (FALSE === parse_fst_new ($blob_fst, $blob_boot, $fst_root, // PBR $fst_off, // PBR $verbose)) { return FALSE; } } function parse_fst_new ($blob_fst, $blob_boot, &$fst_root, &$fst_off, $verbose) { print "----- begin FST -----\n"; $blob_fst_len = strlen($blob_fst); print "FST blob size: ".sprintf("0x%x", $blob_fst_len)."\n"; $root=1; $nametable_start = -1; $prev_offset=0; $directory_stack = array(); // number of files remaining in each parent directory $sequence_stack = array(); // ordering of files in each parent directory $file_dup_check_list = array(); $local_sequence=0; $fst_root = new MKISO_File_Entry; $fst_root->type = MKISO_FILETYPE_ROOT; $fst_root->name = ""; $fst_root->fst_local_sequence = 0; $parent_entry = NULL; for ($i=0;$i<$blob_fst_len;) { // $i incremented by stream_() calls if ($i) { $entry = new MKISO_File_Entry; // to be added to the list on the current parent_entry } else { $entry = $fst_root; } $a = stream_32bit_unsigned_bigendian($blob_fst, $i); $b = stream_32bit_unsigned_bigendian($blob_fst, $i); $c = stream_32bit_unsigned_bigendian($blob_fst, $i); $flags = (($a>>24)&0xff); if (!$flags) { //print " FILE "; $entry->type = MKISO_FILETYPE_FILE; } else if ($flags===1) { if ($root) { //print " ROOT "; $entry->type = MKISO_FILETYPE_ROOT; } else { //print " DIRECTORY "; $entry->type = MKISO_FILETYPE_DIR; } } else { print "ERROR: Fell off end of FST while parsing it.\n"; die(); //$i -= 0x0c; // rewind //break; } if (!$i && ($entry->type !== MKISO_FILETYPE_ROOT)) { print "ERROR: parse_fst: first entry was not root entry\n"; die(); } $a &= 0xffffff; if ($a < 0 || $a > ($blob_fst_len - $i)) { // win print "\nERROR: parse_fst: name offset > blob_fst_len - i (or < 0)\n"; // FIXME: print more return FALSE; } if ($entry->type !== MKISO_FILETYPE_ROOT) { $entry->name_offset = $a; } if ($b > MKISO_CAPACITY) { print "ERROR: Image size would exceed GameCube disc capacity.\n"; print " Maybe try tighter file alignment?\n"; die(); } if ($entry->type === MKISO_FILETYPE_FILE) { //if ($b < $prev_offset) { // print "WARNING: non-monotonic offset (".sprintf("0x%x",$b).")\n"; //} $entry->offset = $b; $prev_offset = $b; } else if ($entry->type === MKISO_FILETYPE_DIR) { // need to set up fake offset, use previous offset plus one $prev_offset++; $entry->offset = $prev_offset; // fake $entry->parent_offset = $b; } if ($c > MKISO_CAPACITY) { print "\nERROR: parse_fst: misc field exceeds capacity\n"; return FALSE; } $entry->misc = $c; if ($entry->type === MKISO_FILETYPE_ROOT) { // for root, misc is number of entries // so we can calculate the start of the string table at this point $nametable_start = $entry->misc * 0x0c; print "Name table starts at FST + ".sprintf("0x%0x", $nametable_start).".\n"; } if ($entry->type !== MKISO_FILETYPE_ROOT) { $entry->name = ""; $pos = $nametable_start + $entry->name_offset; for ($j=0;$j= $blob_fst_len) { print "ERROR: truncated name table for entry ".count($fst)."\n"; return FALSE; } if (!ord($blob_fst[$pos])) { break; } // that terminator is out there $entry->name .= $blob_fst[$pos]; $pos++; } } $entry->parent_entry = $parent_entry; $parent_entry->dir_contents[] = $entry; //$entry->fst_sequence = $i / 0x0c; //$parent_entry->fst_local_sequence[$entry->mkpath()] = $local_sequence; $entry->fst_local_sequence = $local_sequence; // have we seen an entry with this path before? if (isset($file_dup_check_list[$entry->mkpath()])) { print "ERROR: FST contains the same file multiple times: ".$entry->mkpath()."\n"; die(); } $stacksize = count($directory_stack); if ($entry->type === MKISO_FILETYPE_DIR || $entry->type === MKISO_FILETYPE_ROOT) { // descend into subdirectory // push entry counter to top of stack $num_subentries = $entry->misc - ($i / 0x0c); array_push ($directory_stack, $num_subentries+1); // FIXME: check value is sane $parent_entry = $entry; array_push ($sequence_stack, $local_sequence); $local_sequence=0; if ($verbose) { print "+ descending, ".$num_subentries." subentries\n"; } } // since "number of entries in subdirectory" field actually // includes sub-sub-directories and etc., we need to // go through the entire stack and subtract one from every // element in the whole stack to update the quotas for each // superdirectory. // clone the directory stack, because otherwise we end up // removing elements as we're iterating, which caused a horrible bug $dirstack_clone = array(); foreach ($directory_stack as $seq => $num_remaining) { $dirstack_clone[$seq] = $num_remaining; } // iterate using the clone, remove items from master list foreach ($dirstack_clone as $seq => $num_remaining) { $num_remaining--; if ($num_remaining < 0) { // fatal print "ERROR: parse_fst: negative value on dirstack, entry is:\n ".$entry->mkstring(); die(); } if ($num_remaining === 0) { // directory is finished // pop this directory off the stack unset($directory_stack[$seq]); if (!isset($parent_entry->parent_entry)) { $parent_entry = NULL; } else { $parent_entry = $parent_entry->parent_entry; } $local_sequence = array_pop($sequence_stack); if ($verbose) { print "- ascending, stack size now ".count($directory_stack)."\n"; } if (!count($directory_stack)) { // we're done break; } } else { // store the decremented value for each item on the stack $directory_stack[$seq] = $num_remaining; } } if (!count($directory_stack)) { // popped root element, this means we've finished break; } // reindex stack: $directory_stack = array_values($directory_stack); if ($verbose) { $stacksize = count($directory_stack); print "FST(".$stacksize; if ($stacksize) { print " ".$directory_stack[$stacksize-1].") "; } print $entry->mkstring(); } $root=0; $local_sequence++; $file_dup_check_list[$entry->mkpath()] = $entry; // note entry's path so we can check for dups as we go } //print "End of whole FST: ".sprintf("0x%0x", $pos)."\n"; print "Parsed FST with ".number_format($i / 0x0c)." entries.\n"; print "----- end FST -----\n"; return TRUE; } function scan_subdirectory ($fst_by_path, $rootpath, $path, $depth, &$parent_dir, $verbose, $ignore_new_files) { if (!is_dir($path) || (FALSE === ($dir = @opendir ($path)))) { print "WARNING: Could not descend into directory at ".$path."\n"; return FALSE; } if ($depth > MKISO_MAX_DEPTH) { print "ERROR: scan_subdirectory: maximum depth exceeded\n"; return FALSE; } $temporary_new_files_list = array(); $max_sequence = 0; while (FALSE !== ($filename = readdir($dir))) { if ($filename === "." || $filename === "..") { continue; } $file = new MKISO_File_Entry; $file->name = $filename; // FIXME: check length, illegal characters etc. $filepath = $path.DIRECTORY_SEPARATOR.$filename; $fst_entry = FALSE; $hacked_path = substr($filepath, 1+strlen($rootpath)); // ***** FIXME ***** //if (FALSE !== ($fst_entry = ($fst_root->get_entry_by_path (DIRECTORY_SEPARATOR.$hacked_path)))) { //if (FALSE !== ($fst_entry = $fst_by_path [DIRECTORY_SEPARATOR.$hacked_path])) { if (!isset ($fst_by_path [strtolower(DIRECTORY_SEPARATOR.$hacked_path)])) { // file was not present in old FST if ($ignore_new_files) { if ($verbose) { print "* SKIP ".$hacked_path."\n"; } continue; } else { $fst_text = "NEW "; $fst_entry = NULL; } } else { $fst_entry = $fst_by_path [strtolower(DIRECTORY_SEPARATOR.$hacked_path)]; $fst_text = "FST "; } if (@is_dir($filepath)) { $file->type = MKISO_FILETYPE_DIR; scan_subdirectory ($fst_by_path, $rootpath, $filepath, $depth+1, $file, $verbose, $ignore_new_files); // recurse } else { $file->type = MKISO_FILETYPE_FILE; if (FALSE === ($file->payload_len = @filesize ($filepath))) { print "WARNING: Could not find file ".$filepath."\n"; return FALSE; } // if file has FST entry, check length matches if (isset($fst_entry)) { if ($file->payload_len !== $fst_entry->misc) { print "File size has changed: ".$filepath.".\n"; } } } if (isset($fst_entry)) { // get some of the fields from the FST tree, if they exist $file->offset = $fst_entry->offset; $file->fst_local_sequence = $fst_entry->fst_local_sequence; } if (isset($fst_entry->fst_local_sequence)) { if ($fst_entry->fst_local_sequence > $max_sequence) { $max_sequence = $fst_entry->fst_local_sequence; } $file->fst_local_sequence = $fst_entry->fst_local_sequence; // add to parent_entry node if (!isset($parent_dir->dir_contents)) { $parent_dir->dir_contents = array(); } $file->parent_entry = $parent_dir; $parent_dir->dir_contents[$file->fst_local_sequence] = $file; } else { // new file: temporarily add to new files list, // to be committed when the directory closes and we // can work out the new sequence numbers $temporary_new_files_list[] = $file; } if ($verbose) { $b=""; for ($i=0;$i<$depth;$i++) { $b.=" "; } $tc = ($file->type === MKISO_FILETYPE_FILE) ? "+" : DIRECTORY_SEPARATOR; //print $b.$tc." ".$fst_text.($hacked_path)." (".number_format(($ram/1000000.0), 1)." MB RAM)\n"; print $b.$tc." ".$fst_text.($hacked_path)."\n"; } } // commit new files in temporary list to main list on entry object // now we can calculate sequence numbers for them if (!$ignore_new_files) { foreach ($temporary_new_files_list as $whatever => $file) { // add to parent_entry node if (!isset($parent_dir->dir_contents)) { $parent_dir->dir_contents = array(); } $max_sequence++; $file->fst_local_sequence = $max_sequence; $file->parent_entry = $parent_dir; $parent_dir->dir_contents[$file->fst_local_sequence] = $file; } } // put directory in new FST sequence order ksort($parent_dir->dir_contents, SORT_NUMERIC); } function validate_blob_boot ($blob_boot) { // FIXME: do more than just check the length if (($x = strlen ($blob_boot)) !== 0x440) { print "ERROR: Boot blob has incorrect length (". sprintf("0x%x, should be 0x440", $x).").\n"; return FALSE; } return TRUE; } function validate_blob_bi2 ($blob_bi2) { // FIXME: do more than just check the length if (($x = strlen ($blob_bi2)) !== 0x2000) { print "ERROR: BI2 blob has incorrect length (". sprintf("0x%x, should be 0x2000", $x).").\n"; return FALSE; } return TRUE; } function validate_blob_appldr ($blob_appldr, $header_only, &$len, &$trailer_len) { // apploader blob is like // - 0x20 bytes of header // - N bytes of apploader // - M bytes of trailer // (no alignment padding) if (!$header_only) { if (($x = strlen ($blob_appldr)) < MKISO_APPLDR_MINLEN) { // shrug print "ERROR: apploader blob is too small (". sprintf("0x%x bytes, should be >= 0x%x", $x, MKISO_APPLDR_MINLEN).").\n"; return FALSE; } } else { if (($x = strlen ($blob_appldr)) !== 0x20) { // header only print "ERROR: apploader header is wrong size (". sprintf("0x%x bytes, should be >= 0x%x", $x, 0x20).").\n"; return FALSE; } } print "Apploader:\n"; $i = 0x14; $len1 = stream_32bit_unsigned_bigendian($blob_appldr, $i); $len2 = stream_32bit_unsigned_bigendian($blob_appldr, $i); if (!$header_only) { $len3 = strlen($blob_appldr); print " total size: ".sprintf("0x%x", $len3)."\n"; } print " version: ".substr($blob_appldr, 0, 10)."\n"; print " main size: ".sprintf("0x%x", $len1)."\n"; print " trailer size: ".sprintf("0x%x", $len2)."\n"; $len4 = $len1 + $len2; //print "l1=".$len1." l2=".$len2." l3=".$len3." l4=".$len4."\n"; if (!$header_only && ($len3 !== ($len4 + 32))) { print "ERROR: len(appldr) + len(appldr_trailer) + 32 != strlen(blob_appldr)\n"; return FALSE; } $len = $len1; $trailer_len = $len2; return TRUE; } function validate_blob_dol ($blob_dol) { if (($x = strlen ($blob_dol)) < 0x100) { // standard DOL header size print "ERROR: DOL blob is too small (". sprintf("0x%x bytes, must be at least 0x100", $x).").\n"; return FALSE; } $size = determine_dol_size(substr($blob_dol, 0, 0x100)); if ($size !== strlen($blob_dol)) { print "ERROR: DOL blob size and DOL self-reported size do not match, corrupt DOL.\n"; return FALSE; } return TRUE; } function parse_cli ($argv, &$blob_boot, &$blob_bi2, &$blob_appldr, &$blob_fst, &$blob_dol, &$gamename, &$gamecode, &$ignore_new_files, &$iso, &$files_dir, &$verbose, &$overwrite, &$build, &$align_mode, &$align_limit, &$align_list, &$pack) { $blob_boot = NULL; $blob_bi2 = NULL; $blob_appldr = NULL; $blob_fst = NULL; $blob_dol = NULL; $iso = NULL; $files_dir = NULL; $gamename = NULL; $gamecode = NULL; $verbose = FALSE; $ignore_new_files = FALSE; $overwrite = FALSE; $build = FALSE; $align_mode = NULL; $align_limit = NULL; $align_list = NULL; $pack = FALSE; // locals: $my_root_dir = NULL; $my_files_dir = NULL; $my_sys_dir = NULL; $my_blob_boot = NULL; $my_blob_bi2 = NULL; $my_blob_appldr = NULL; $my_blob_fst = NULL; $my_blob_dol = NULL; $tmp_sys_dir = NULL; $num_aligns = 0; // check $align_limit_s = NULL; //$align_val_s = 0; $mode = 0; // state machine if (!isset($argv[1]) || ($argv[1] !== "build" && $argv[1] !== "unbuild")) { print "ERROR: You must specify either build or unbuild mode.\n\n"; print mkiso_usage()."\n"; return FALSE; } if ($argv[1] === "build") { $build = TRUE; } $multiarg_phase = 0; $stored_align_pattern = NULL; for ($i=2; $i 1) { print "ERROR: You cannot choose more than one alignment mode.\n"; return FALSE; } if (isset($align_limit_s)) { $i = validate_command_line_integer($align_limit_s); $fail = 0; if (FALSE === $i) { $fail = 1; } else { // check it's a power of two and below the limit if ($i>MKISO_ALIGN_LIMIT_MAX || !is_power_of_two($i)) { $fail=1; } } if ($fail) { print "Specified alignment limit is invalid (\"".$align_limit_s."\").\n"; print "It must be a power-of-two integer below 32K.\n"; print "(You may prefix the value with 0x to indicate a hexadecimal value).\n"; return FALSE; } } if (isset($align_list)) { foreach ($align_list as $pattern => $align_s) { //print "FIXME: validate pattern? ".$pattern."\n"; $i = validate_command_line_integer($align_s); if ((FALSE === $i) || !is_power_of_two($i)) { print "Specified alignment (".$align_s.") is invalid for file pattern:\n"; print " ".$pattern."\n"; print "It must be a power-of-two integer below 32K.\n"; print "You may prefix this value with 0x to indicate a hexadecimal value.\n"; return FALSE; } $align_list[$pattern] = $i; } } if (isset($my_root_dir)) { $files_dir = $my_root_dir.DIRECTORY_SEPARATOR."files"; $tmp_sys_dir = $my_root_dir.DIRECTORY_SEPARATOR."sys"; } if (isset($my_files_dir)) { $files_dir = $my_files_dir; } if (isset($my_sys_dir)) { $tmp_sys_dir = $my_sys_dir; } if (isset($tmp_sys_dir)) { $blob_boot = $tmp_sys_dir.DIRECTORY_SEPARATOR."boot.bin"; $blob_bi2 = $tmp_sys_dir.DIRECTORY_SEPARATOR."bi2.bin"; $blob_appldr = $tmp_sys_dir.DIRECTORY_SEPARATOR."apploader.img"; $blob_fst = $tmp_sys_dir.DIRECTORY_SEPARATOR."fst.bin"; // also Game.toc ? $blob_dol = $tmp_sys_dir.DIRECTORY_SEPARATOR."main.dol"; } if (isset($my_blob_boot)) { $blob_boot = $my_blob_boot; } if (isset($my_blob_bi2)) { $blob_bi2 = $my_blob_bi2; } if (isset($my_blob_appldr)) { $blob_appldr = $my_blob_appldr; } if (isset($my_blob_fst)) { $blob_fst = $my_blob_fst; } if (isset($my_blob_dol)) { $blob_dol = $my_blob_dol; } // now check we have the things we need if (!isset($files_dir)) { print "ERROR: You need to provide the path to a files directory, either implicitly\n". " with +pd, or explicitly with +fd.\n"; return FALSE; } if (!isset($blob_boot)) { print "ERROR: You need to provide the path to a boot blob file (boot.bin),\n". " either explicitly with +bb, or implicitly using +pd or +sd.\n"; return FALSE; } if (!isset($blob_bi2)) { print "ERROR: You need to provide the path to a BI2 blob file (bi2.bin),\n". " either explicitly with +2b, or implicitly using +pd or +sd.\n"; return FALSE; } if (!isset($blob_appldr)) { print "ERROR: You need to provide the path to an apploader blob file\n". " (apploader.img), either explicitly with +ab, or implicitly\n". " using +pd or +sd.\n"; return FALSE; } if (!isset($blob_fst)) { print "ERROR: You need to provide the path to an FST blob file (fst.bin),\n". " either explicitly with +fb, or implicitly using +pd or +sd.\n"; return FALSE; } if (!isset($blob_dol)) { print "ERROR: You need to provide the path to a DOL blob file (main.dol),\n". " either explicitly with +db, or implicitly using +pd or +sd.\n"; return FALSE; } if (!isset($iso)) { print "ERROR: You need to provide an ISO filename.\n"; return FALSE; } return TRUE; } function is_power_of_two($x) { $num_ones=0; $mask = 0; $php_int_size = PHP_INT_SIZE*8; for ($i=0;$i<$php_int_size-1;$i++) { $mask <<= 1; $mask |= 1; //printf ("mask=%x\n", $mask); } for ($i=0;$i<$php_int_size;$i++) { if ($x&1) { $num_ones++; } $x>>=1; $x&=$mask; //printf ("x=%x\n", $x); } return ($num_ones === 1); } function validate_command_line_integer($i) { if (ctype_digit($i)) { $p = 0; if (FALSE === sscanf($i, "%d", $p)) { //print "[1]\n"; return FALSE; } return $p; } $s = substr($i, 0, 2); if ($s !== "0x" && $s !== "0X") { //print "[2]\n"; return FALSE; } $s = substr($i, 2); if (!ctype_xdigit($s)) { //print "[3]\n"; return FALSE; } $p = 0; if (FALSE === sscanf($s, "%x", $p)) { //print "[4]\n"; return FALSE; } return $p; } function mkiso_usage() { $ds = DIRECTORY_SEPARATOR; $exd = "game_root"; // example dir $usage = "Usage: php -f mkiso.php (build | unbuild) [options ...]\n\n". "Options for both building and unbuilding:\n". "-----------------------------------------\n". " specify a project directory containing \"files\" and \"sys\":\n". " (+pd | +project-dir) ".$exd."\n\n". " or specify \"files\" and \"sys\" independently:\n". " (+fd | +files-dir) ".$exd.$ds."files\n". " (+sd | +sys-dir) ".$exd.$ds."sys\n\n". " or specify just \"files\" using +fd, and then the blobs independently:\n". " (+bb | +boot-blob) ".$exd.$ds."sys".$ds."boot.bin\n". " (+2b | +bi2-blob) ".$exd.$ds."sys".$ds."bi2.bin\n". " (+ab | +appldr-blob) ".$exd.$ds."sys".$ds."apploader.img\n". " (+fb | +fst-blob) ".$exd.$ds."sys".$ds."fst.bin\n". " (+db | +dol-blob) ".$exd.$ds."sys".$ds."main.dol\n\n". " the ISO file to be built or unbuilt:\n". " (+if | +iso-file) myisofile.iso\n\n". " files are not overwritten by default, but use this to override:\n". " (+ow | +overwrite)\n\n". " produce more output:\n". " (+vb | +verbose)\n\n". "Options for building only:\n". "--------------------------\n". " override game name and game code in generated image:\n". " (+gn | +game-name) 'XYXY'\n". " (+gc | +game-code) 'XY'\n\n". " file alignment options:\n". " (+ae | +align-ext) - align by extension group (default)\n". " (+as | +align-src) - align as source file, with limit\n". " (+af | +align-file) - override align for matching files\n\n". // " pack mode -- adjust file ordering to make best use of available space:\n". // " (+pk | +pack)\n\n". " ignore files that are not in the source FST:\n". " (+in | +ignore-new)\n"; print $usage; } function stream_32bit_unsigned_bigendian($buf, &$off) { $len = strlen($buf); if ($off + 4 >= $len) { print "ERROR: stream_32bit_unsigned_bigendian(): truncated\n"; die(); } $i = parse_32bit_unsigned_bigendian(substr($buf, $off, 4)); $off+=4; return $i; } function parse_32bit_unsigned_bigendian($v) { if (($len=strlen($v))!==4) { print "ERROR: parse_32bit_unsigned_bigendian(): bad length (".$len.", should be 4)\n"; die(); } $vi = array(); $vi[0] = ord($v[0]); $vi[1] = ord($v[1]); $vi[2] = ord($v[2]); $vi[3] = ord($v[3]); return (($vi[0]<<24) | ($vi[1]<<16) | ($vi[2]<<8) | ($vi[3])); } function grenola_hexdump($mem, $minimal_mode) { $buf=""; if (!isset($mem) || $mem==="") { return; } $l=strlen($mem); if (defined("HEXDUMP_MAX") && $l>HEXDUMP_MAX) $l=HEXDUMP_MAX; $sbuf=""; for ($i=0;$i<$l;$i++) { if (!$minimal_mode && !($i%16)) { $buf.=sprintf ("%04x ", $i); } $buf.=sprintf ("%02x ", $z=ord($mem[$i])); if (!$minimal_mode) { if (!ctype_print($w=$mem[$i])||$z>127||$w==="\r"||$w==="\n") { $sbuf.="."; } else { $sbuf.=$w; } } if ($i%16 == 15) { if (!$minimal_mode) { $buf.=" "; } $buf.= $sbuf; if (!$minimal_mode) { $buf.="\n"; } $sbuf=""; } } if ($i%16<15) { if (!$minimal_mode) { for ($i=(16-$i%16);$i;$i--) { $buf.= " "; } $buf.= " "; } $buf.=$sbuf; if (!$minimal_mode) { $buf.="\n"; } } return $buf; //if ($i%16 != 15) { print "\n"; } } ?>