/***************************************************************************
 *   Copyright (C) 2007 by Arep                                            *
 *   Support is provided through the forums at                             *
 *   http://www.console-tribe.com                                          *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program 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 General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
 ***************************************************************************/

#include "misc.h"
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <multihash.h>
#include "constants.h"
#include "disc.h"
#include "dumper.h"
#include "xbox_ref_bridge.h"
#include "xbox_portable.h"
#include "xbox_ref/xbox_ref_log.h"

#ifndef WIN32
#include <unistd.h>
#include <sys/types.h>
#include <sys/time.h>
#endif

struct dumper_s {
	disc *dsk;
	char *outfile_raw;
	u_int32_t start_sector_raw;
	FILE *fp_raw;
	char *outfile_iso;
	u_int32_t start_sector_iso;
	FILE *fp_iso;
	char *outfile_xiso;
	u_int32_t start_sector_xiso;
	FILE *fp_xiso;
	u_int32_t start_sector;
	bool hashing;
	bool flushing;

	multihash hash_raw;
	multihash hash_iso;
	multihash hash_xiso;
	xbox_ref_dump_result xbox_ref_result;
	
	progress_func progress;
	void *progress_data;
	dumper_cancel_func cancel;
	void *cancel_data;
	bool cancelled;
};

#define DUMPER_READ_BLOCK_RETRIES 3

static bool dumper_cancel_requested (dumper *dmp) {
	if (!dmp || !dmp -> cancel)
		return false;
	if (!dmp -> cancel (dmp -> cancel_data))
		return false;
	dmp -> cancelled = true;
	return true;
}

static int dumper_xbox_ref_cancel_requested (void *cancel_data) {
	return dumper_cancel_requested ((dumper *) cancel_data) ? 1 : 0;
}

static double xbox_portable_now_seconds (void) {
	struct timeval now;
	gettimeofday (&now, NULL);
	return (double) now.tv_sec + ((double) now.tv_usec / 1000000.0);
}

static u_int64_t xbox_portable_file_size (const char *path) {
	FILE *file;
	my_off_t position;

	if (!path || path[0] == '\0')
		return 0;
	file = fopen (path, "rb");
	if (!file)
		return 0;
	if (my_fseek (file, 0, SEEK_END) != 0) {
		fclose (file);
		return 0;
	}
	position = my_ftell (file);
	fclose (file);
	if (position < 0)
		return 0;
	return (u_int64_t) position;
}

static void xbox_portable_copy_string (char *destination, size_t destination_size, const char *source) {
	if (!destination || destination_size == 0)
		return;
	if (!source)
		source = "";
	snprintf (destination, destination_size, "%s", source);
}

static void xbox_portable_result_begin (dumper *dmp, const char *path, char mode) {
	if (!dmp)
		return;
	xbox_ref_dump_result_init (&(dmp -> xbox_ref_result));
	dmp -> xbox_ref_result.attempted = 1;
	dmp -> xbox_ref_result.mode = mode;
	xbox_portable_copy_string (dmp -> xbox_ref_result.output_path,
			sizeof (dmp -> xbox_ref_result.output_path), path);
}

static void xbox_portable_sanitize_filename_component (char *value) {
	size_t read_index;
	size_t write_index;
	bool previous_space;

	if (!value)
		return;

	write_index = 0;
	previous_space = true;
	for (read_index = 0; value[read_index] != '\0'; ++read_index) {
		unsigned char c = (unsigned char) value[read_index];
		bool forbidden = c < 32 || c == '"' || c == '*' || c == '/' ||
			c == ':' || c == '<' || c == '>' || c == '?' || c == '\\' || c == '|';

		if (forbidden)
			continue;
		if (c == ' ') {
			if (previous_space)
				continue;
			value[write_index++] = ' ';
			previous_space = true;
		} else {
			value[write_index++] = (char) c;
			previous_space = false;
		}
	}

	while (write_index > 0 && (value[write_index - 1] == ' ' ||
			value[write_index - 1] == '.'))
		--write_index;
	value[write_index] = '\0';
}

static bool xbox_portable_resolve_output_path (dumper *dmp, bool xiso_mode) {
	xbox_portable_metadata metadata;
	char title[XBOX_PORTABLE_TITLE_MAX];
	char path[XBOX_REF_RESULT_PATH_MAX];
	char **target;
	const char *extension;

	if (!dmp)
		return false;

	target = xiso_mode ? &(dmp -> outfile_xiso) : &(dmp -> outfile_iso);
	if (!*target || (*target)[0] != '\0')
		return true;

	xbox_ref_log_fprintf (stderr,
		"[XBOX-PORTABLE] Resolving the default output filename from the unlocked XBE/DMI view.\n");
	disc_xbox_wait_ready (dmp -> dsk, 30000);
	if (disc_xbox_unlock (dmp -> dsk) < 0)
		xbox_ref_log_fprintf (stderr,
			"[XBOX-PORTABLE][WARN] Filename preflight unlock returned failure; attempting metadata reads anyway.\n");
	disc_xbox_refresh_volume (dmp -> dsk);
	disc_xbox_wait_ready (dmp -> dsk, 30000);

	xbox_portable_metadata_init (&metadata);
	if (!xbox_portable_read_metadata_from_disc (dmp -> dsk, &metadata)) {
		error ("Could not derive an Xbox output filename because XBE/DMI metadata was unavailable; provide an explicit output path");
		return false;
	}

	xbox_portable_copy_string (title, sizeof (title),
		metadata.title[0] ? metadata.title : "Xbox DVD");
	xbox_portable_sanitize_filename_component (title);
	if (!title[0])
		xbox_portable_copy_string (title, sizeof (title), "Xbox DVD");
	extension = xiso_mode ? ".xiso" : ".iso";

	if (metadata.media_id[0])
		snprintf (path, sizeof (path), "%s[%s]%s", title, metadata.media_id, extension);
	else
		snprintf (path, sizeof (path), "%s%s", title, extension);

	my_free (*target);
	my_strdup (*target, path);
	xbox_ref_log_retarget (*target);
	xbox_ref_log_fprintf (stderr,
		"[XBOX-PORTABLE] Derived output filename: %s\n", *target);
	return true;
}

static void xbox_portable_result_capture_metadata (dumper *dmp) {
	xbox_portable_metadata metadata;

	if (!dmp || (dmp -> xbox_ref_result.title[0] &&
			dmp -> xbox_ref_result.have_game_region))
		return;

	xbox_portable_metadata_init (&metadata);
	if (!xbox_portable_read_metadata_from_disc (dmp -> dsk, &metadata)) {
		xbox_ref_log_fprintf (stderr,
			"[XBOX-PORTABLE][WARN] XBE title/region metadata was not available from the unlocked view.\n");
		return;
	}

	xbox_portable_copy_string (dmp -> xbox_ref_result.title,
			sizeof (dmp -> xbox_ref_result.title), metadata.title);
	xbox_portable_copy_string (dmp -> xbox_ref_result.media_id,
			sizeof (dmp -> xbox_ref_result.media_id), metadata.media_id);
	xbox_portable_copy_string (dmp -> xbox_ref_result.region,
			sizeof (dmp -> xbox_ref_result.region), metadata.region);
	dmp -> xbox_ref_result.have_game_region = metadata.have_game_region;
	dmp -> xbox_ref_result.game_region = metadata.game_region;

	xbox_ref_log_fprintf (stderr,
		"[XBOX-PORTABLE] XBE metadata: title='%s', media_id='%s', region='%s', GameRegion=0x%08x.\n",
		dmp -> xbox_ref_result.title[0] ? dmp -> xbox_ref_result.title : "(unknown)",
		dmp -> xbox_ref_result.media_id[0] ? dmp -> xbox_ref_result.media_id : "(unknown)",
		dmp -> xbox_ref_result.region[0] ? dmp -> xbox_ref_result.region : "(unknown)",
		dmp -> xbox_ref_result.game_region);
}

static void xbox_portable_result_finish (dumper *dmp,
		bool success,
		u_int32_t expected_output_sectors,
		double started_seconds,
		bool xiso_mode) {
	xbox_ref_dump_result *result;
	const char *crc32;
	const char *md5;
	const char *sha1;
	const char *sha256;

	if (!dmp || !(dmp -> xbox_ref_result).attempted)
		return;

	result = &(dmp -> xbox_ref_result);
	result -> dump_success = success ? 1 : 0;
	result -> cancelled = dmp -> cancelled ? 1 : 0;
	result -> output_sectors = expected_output_sectors;
	result -> output_size = xbox_portable_file_size (result -> output_path);
	result -> completed_sectors = (u_int32_t) (result -> output_size / SECTOR_SIZE);
	result -> elapsed_seconds = xbox_portable_now_seconds () - started_seconds;

	if (!dmp -> hashing || result -> output_size == 0)
		return;

	if (xiso_mode) {
		crc32 = dumper_get_xiso_crc32 (dmp);
		md5 = dumper_get_xiso_md5 (dmp);
		sha1 = dumper_get_xiso_sha1 (dmp);
		sha256 = dumper_get_xiso_sha2 (dmp);
	} else {
		crc32 = dumper_get_iso_crc32 (dmp);
		md5 = dumper_get_iso_md5 (dmp);
		sha1 = dumper_get_iso_sha1 (dmp);
		sha256 = dumper_get_iso_sha2 (dmp);
	}

	if (!crc32 || !md5 || !sha1 || !sha256 ||
		!crc32[0] || !md5[0] || !sha1[0] || !sha256[0])
		return;

	xbox_portable_copy_string (result -> crc32, sizeof (result -> crc32), crc32);
	xbox_portable_copy_string (result -> md5, sizeof (result -> md5), md5);
	xbox_portable_copy_string (result -> sha1, sizeof (result -> sha1), sha1);
	xbox_portable_copy_string (result -> sha256, sizeof (result -> sha256), sha256);
	result -> hashes_complete = 1;
}

static bool dumper_read_sector_with_retry (dumper *dmp, u_int32_t sector_no, u_int8_t **isobuf, u_int8_t **rawbuf) {
	int attempt;
	bool need_iso, need_raw;

	if (!dmp || !dmp -> dsk)
		return false;

	need_iso = dmp -> fp_iso != NULL;
	need_raw = dmp -> fp_raw != NULL;

	for (attempt = 0; attempt < DUMPER_READ_BLOCK_RETRIES; attempt++) {
		if (isobuf) *isobuf = NULL;
		if (rawbuf) *rawbuf = NULL;

		disc_read_sector (dmp -> dsk, sector_no, isobuf, rawbuf);
		if ((!need_iso || (isobuf && *isobuf)) && (!need_raw || (rawbuf && *rawbuf))) {
			if (attempt > 0)
				warning ("Recovered sectors %u..%u after dump-level retry %d", sector_no, sector_no + SECTORS_PER_BLOCK - 1, attempt);
			return true;
		}

		if (attempt + 1 < DUMPER_READ_BLOCK_RETRIES)
			warning ("Dump-level read retry %d/%d for sectors %u..%u", attempt + 1, DUMPER_READ_BLOCK_RETRIES - 1, sector_no, sector_no + SECTORS_PER_BLOCK - 1);
	}

	return false;
}

#define XBOX_XISO_LEADIN_SECTORS 32
#define XBOX_XISO_STANDARD_GAME_LBA 32
#define XBOX_XISO_START_LBA_MAGIC 306112
#define XBOX_XISO_DUAL_LAYER_END_LBA 1913920

/* DiscImageCreator-compatible Original Xbox/XGD1 2048-byte-sector layout
 * constants. DIC uses XBOX_SIZE=3820880, XBOX_LAYER_BREAK=1913776,
 * DVD start PSN 0x30000, and Xbox start PSN 0x60600. The virtual redump
 * layout below follows those constants: video L0, middle/padding, game
 * partition, middle/padding, and video L1 tail.
 */
#define XBOX_XGD1_DIC_DVD_START_PSN 0x30000
#define XBOX_XGD1_DIC_XBOX_START_PSN 0x60600
#define XBOX_XGD1_DIC_XBOX_SIZE 3820880
#define XBOX_XGD1_DIC_LAYER_BREAK 1913776
#define XBOX_XGD1_UNLOCKED_GAME_VIEW_SECTORS 3431264
#define XBOX_XGD1_FULL_REDUMP_SECTORS XBOX_XGD1_DIC_XBOX_SIZE
#define XBOX_XGD1_GAME_OUTPUT_START_LBA (XBOX_XGD1_DIC_XBOX_START_PSN - XBOX_XGD1_DIC_DVD_START_PSN)
#define XBOX_XGD1_VIDEO_L0_SECTORS 6832
#define XBOX_XGD1_VIDEO_L1_SECTORS 160
#define XBOX_XGD1_VIDEO_L1_OUTPUT_START_LBA (XBOX_XGD1_FULL_REDUMP_SECTORS - XBOX_XGD1_VIDEO_L1_SECTORS)
#define XBOX_XGD1_GAME_SOURCE_SECTORS (XBOX_XGD1_UNLOCKED_GAME_VIEW_SECTORS - XBOX_XISO_LEADIN_SECTORS)
#define XBOX_XGD1_LAYER_THRESHOLD 2000000
#define XBOX_METADATA_PATH_MAX 4096

static u_int32_t read_le32 (const u_int8_t *p) {
	return ((u_int32_t) p[0]) | ((u_int32_t) p[1] << 8) | ((u_int32_t) p[2] << 16) | ((u_int32_t) p[3] << 24);
}

static bool xbox_probe_xdfs_volume (disc *d, u_int32_t lba, u_int32_t *root_lba, u_int32_t *volume_size_raw) {
	u_int8_t sector[SECTOR_SIZE];

	memset (sector, 0, sizeof (sector));
	if (disc_xbox_read_10 (d, lba, 1, sector, sizeof (sector)) < 0)
		return false;

	if (memcmp (sector, "MICROSOFT", 9) != 0)
		return false;

	if (root_lba)
		*root_lba = read_le32 (sector + 0x14);
	if (volume_size_raw)
		*volume_size_raw = read_le32 (sector + 0x1C);

	return true;
}

static bool xbox_redump_layout_constants_match_dic (void) {
	return XBOX_XGD1_FULL_REDUMP_SECTORS == XBOX_XGD1_DIC_XBOX_SIZE &&
	       XBOX_XGD1_DIC_LAYER_BREAK == 1913776 &&
	       XBOX_XGD1_GAME_OUTPUT_START_LBA == 0x30600 &&
	       XBOX_XGD1_VIDEO_L1_OUTPUT_START_LBA == 3820720 &&
	       XBOX_XGD1_VIDEO_L0_SECTORS +
	       (XBOX_XGD1_GAME_OUTPUT_START_LBA - XBOX_XGD1_VIDEO_L0_SECTORS) +
	       XBOX_XGD1_UNLOCKED_GAME_VIEW_SECTORS +
	       (XBOX_XGD1_VIDEO_L1_OUTPUT_START_LBA -
	        (XBOX_XGD1_GAME_OUTPUT_START_LBA + XBOX_XGD1_UNLOCKED_GAME_VIEW_SECTORS)) +
	       XBOX_XGD1_VIDEO_L1_SECTORS == XBOX_XGD1_FULL_REDUMP_SECTORS;
}


typedef struct {
	bool has_pfi;
	bool has_dmi;
	bool pfi_written;
	bool dmi_written;
	u_int8_t pfi[2048];
	u_int8_t dmi[2048];
	u_int32_t locked_visible_sectors;
	u_int32_t unlocked_visible_sectors;
	u_int32_t output_sectors;
	u_int32_t game_source_lba;
	u_int32_t game_source_sectors;
	u_int32_t game_leadin_read_sectors;
	u_int32_t game_leadin_zero_sectors;
	char pfi_path[XBOX_METADATA_PATH_MAX];
	char dmi_path[XBOX_METADATA_PATH_MAX];
	char json_path[XBOX_METADATA_PATH_MAX];
} xbox_redump_metadata;

static void xbox_make_related_path (const char *filename, const char *suffix, char *out, size_t out_size) {
	const char *slash1, *slash2, *slash, *dot;
	size_t suffix_len, base_len;

	if (!out || out_size == 0)
		return;
	out[0] = '\0';
	if (!filename || !suffix)
		return;

	suffix_len = strlen (suffix);
	if (out_size <= suffix_len + 1)
		return;

	slash1 = strrchr (filename, '/');
	slash2 = strrchr (filename, '\\');
	if (slash1 && slash2)
		slash = slash1 > slash2 ? slash1 : slash2;
	else
		slash = slash1 ? slash1 : slash2;

	dot = strrchr (filename, '.');
	if (!dot || (slash && dot < slash))
		dot = filename + strlen (filename);

	base_len = (size_t) (dot - filename);
	if (base_len + suffix_len + 1 > out_size)
		base_len = out_size - suffix_len - 1;
	memcpy (out, filename, base_len);
	out[base_len] = '\0';
	strncat (out, suffix, out_size - strlen (out) - 1);
}

static const char *xbox_path_leaf (const char *path) {
	const char *slash1, *slash2;
	if (!path)
		return "";
	slash1 = strrchr (path, '/');
	slash2 = strrchr (path, '\\');
	if (slash1 && slash2)
		return (slash1 > slash2 ? slash1 : slash2) + 1;
	if (slash1)
		return slash1 + 1;
	if (slash2)
		return slash2 + 1;
	return path;
}

static void xbox_json_string (FILE *f, const char *s) {
	fputc ('"', f);
	if (s) {
		while (*s) {
			unsigned char c = (unsigned char) *s++;
			if (c == '"' || c == '\\') {
				fputc ('\\', f);
				fputc (c, f);
			} else if (c == '\n') {
				fputs ("\\n", f);
			} else if (c == '\r') {
				fputs ("\\r", f);
			} else if (c == '\t') {
				fputs ("\\t", f);
			} else if (c < 0x20) {
				fprintf (f, "\\u%04x", c);
			} else {
				fputc (c, f);
			}
		}
	}
	fputc ('"', f);
}

static bool xbox_write_binary_file (const char *path, const u_int8_t *data, size_t len) {
	FILE *f;
	if (!path || !data || len == 0)
		return false;
	f = fopen (path, "wb");
	if (!f)
		return false;
	if (fwrite (data, 1, len, f) != len) {
		fclose (f);
		return false;
	}
	return fclose (f) == 0;
}

static void xbox_capture_pfi_dmi (disc *d, xbox_redump_metadata *meta) {
	int rc;
	if (!d || !meta)
		return;
	if (!meta -> has_pfi) {
		rc = disc_xbox_read_dvd_structure (d, 0x00, 0x00, meta -> pfi, sizeof (meta -> pfi));
		meta -> has_pfi = rc >= 0;
		xbox_ref_log_fprintf (
			stderr,
			"[XBOX-DVD-STRUCTURE] format=0x00 name=PFI cdb_format_byte=7 result=%s\n",
			meta -> has_pfi ? "PASS" : "FAIL");
	}
	if (!meta -> has_dmi) {
		rc = disc_xbox_read_dvd_structure (d, 0x04, 0x00, meta -> dmi, sizeof (meta -> dmi));
		meta -> has_dmi = rc >= 0;
		xbox_ref_log_fprintf (
			stderr,
			"[XBOX-DVD-STRUCTURE] format=0x04 name=DMI cdb_format_byte=7 result=%s\n",
			meta -> has_dmi ? "PASS" : "FAIL");
	}
}

static void xbox_write_redump_metadata (dumper *dmp, xbox_redump_metadata *meta) {
	FILE *json;
	const char *pfi_leaf;
	const char *dmi_leaf;

	if (!dmp || !meta || !dmp -> outfile_iso)
		return;

	xbox_make_related_path (dmp -> outfile_iso, ".pfi.bin", meta -> pfi_path, sizeof (meta -> pfi_path));
	xbox_make_related_path (dmp -> outfile_iso, ".dmi.bin", meta -> dmi_path, sizeof (meta -> dmi_path));
	xbox_make_related_path (dmp -> outfile_iso, ".redump.json", meta -> json_path, sizeof (meta -> json_path));

	if (meta -> has_pfi)
		meta -> pfi_written = xbox_write_binary_file (meta -> pfi_path, meta -> pfi, sizeof (meta -> pfi));
	if (meta -> has_dmi)
		meta -> dmi_written = xbox_write_binary_file (meta -> dmi_path, meta -> dmi, sizeof (meta -> dmi));

	json = fopen (meta -> json_path, "wb");
	if (!json) {
		warning ("Could not write Xbox redump metadata JSON");
		return;
	}

	pfi_leaf = meta -> pfi_written ? xbox_path_leaf (meta -> pfi_path) : NULL;
	dmi_leaf = meta -> dmi_written ? xbox_path_leaf (meta -> dmi_path) : NULL;

	fprintf (json, "{\n");
	fprintf (json, "  \"format\": \"friidump-xbox-redump\",\n");
	fprintf (json, "  \"version\": 1,\n");
	fprintf (json, "  \"image\": {\n");
	fprintf (json, "    \"file\": "); xbox_json_string (json, xbox_path_leaf (dmp -> outfile_iso)); fprintf (json, ",\n");
	fprintf (json, "    \"sector_size\": %u,\n", SECTOR_SIZE);
	fprintf (json, "    \"sector_count\": %u,\n", meta -> output_sectors);
	fprintf (json, "    \"byte_count\": %llu,\n", (unsigned long long) meta -> output_sectors * SECTOR_SIZE);
	fprintf (json, "    \"hashes\": {\n");
	fprintf (json, "      \"crc32\": "); xbox_json_string (json, dumper_get_iso_crc32 (dmp)); fprintf (json, ",\n");
	fprintf (json, "      \"md5\": "); xbox_json_string (json, dumper_get_iso_md5 (dmp)); fprintf (json, ",\n");
	fprintf (json, "      \"sha1\": "); xbox_json_string (json, dumper_get_iso_sha1 (dmp)); fprintf (json, ",\n");
	fprintf (json, "      \"sha256\": "); xbox_json_string (json, dumper_get_iso_sha2 (dmp)); fprintf (json, "\n");
	fprintf (json, "    }\n");
	fprintf (json, "  },\n");
	fprintf (json, "  \"drive\": {\n");
	fprintf (json, "    \"model\": "); xbox_json_string (json, disc_get_drive_model_string (dmp -> dsk)); fprintf (json, ",\n");
	fprintf (json, "    \"command\": %u,\n", disc_get_command (dmp -> dsk));
	fprintf (json, "    \"method\": %u\n", disc_get_method (dmp -> dsk));
	fprintf (json, "  },\n");
	fprintf (json, "  \"layout\": {\n");
	fprintf (json, "    \"layout_type\": \"original_xbox_xgd1_redump_style_2048\",\n");
	fprintf (json, "    \"locked_visible_sector_count\": %u,\n", meta -> locked_visible_sectors);
	fprintf (json, "    \"unlocked_visible_sector_count\": %u,\n", meta -> unlocked_visible_sectors);
	fprintf (json, "    \"video_l0_start_lba\": 0,\n");
	fprintf (json, "    \"video_l0_sector_count\": %u,\n", XBOX_XGD1_VIDEO_L0_SECTORS);
	fprintf (json, "    \"pregame_padding_start_lba\": %u,\n", XBOX_XGD1_VIDEO_L0_SECTORS);
	fprintf (json, "    \"pregame_padding_sector_count\": %u,\n", XBOX_XGD1_GAME_OUTPUT_START_LBA - XBOX_XGD1_VIDEO_L0_SECTORS);
	fprintf (json, "    \"game_output_start_lba\": %u,\n", XBOX_XGD1_GAME_OUTPUT_START_LBA);
	fprintf (json, "    \"game_leadin_sector_count\": %u,\n", XBOX_XISO_LEADIN_SECTORS);
	fprintf (json, "    \"game_leadin_unlocked_source_start_lba\": %u,\n",
		(meta -> game_source_lba >= XBOX_XISO_LEADIN_SECTORS) ?
		meta -> game_source_lba - XBOX_XISO_LEADIN_SECTORS : 0);
	fprintf (json, "    \"game_leadin_source_policy\": \"drive_read10_with_per_sector_zero_fallback\",\n");
	fprintf (json, "    \"game_unlocked_source_start_lba\": %u,\n", meta -> game_source_lba);
	fprintf (json, "    \"game_unlocked_source_sector_count\": %u,\n", meta -> game_source_sectors);
	fprintf (json, "    \"postgame_padding_start_lba\": %u,\n", XBOX_XGD1_GAME_OUTPUT_START_LBA + XBOX_XGD1_UNLOCKED_GAME_VIEW_SECTORS);
	fprintf (json, "    \"postgame_padding_sector_count\": %u,\n", XBOX_XGD1_VIDEO_L1_OUTPUT_START_LBA - (XBOX_XGD1_GAME_OUTPUT_START_LBA + XBOX_XGD1_UNLOCKED_GAME_VIEW_SECTORS));
	fprintf (json, "    \"video_l1_start_lba\": %u,\n", XBOX_XGD1_VIDEO_L1_OUTPUT_START_LBA);
	fprintf (json, "    \"video_l1_sector_count\": %u,\n", XBOX_XGD1_VIDEO_L1_SECTORS);
	fprintf (json, "    \"layer_break_lba\": %u\n", XBOX_XGD1_DIC_LAYER_BREAK);
	fprintf (json, "  },\n");
	fprintf (json, "  \"discimagecreator_reference\": {\n");
	fprintf (json, "    \"xbox_size_sectors\": %u,\n", XBOX_XGD1_DIC_XBOX_SIZE);
	fprintf (json, "    \"xbox_layer_break_lba\": %u,\n", XBOX_XGD1_DIC_LAYER_BREAK);
	fprintf (json, "    \"dvd_start_psn\": %u,\n", XBOX_XGD1_DIC_DVD_START_PSN);
	fprintf (json, "    \"xbox_start_psn\": %u,\n", XBOX_XGD1_DIC_XBOX_START_PSN);
	fprintf (json, "    \"game_output_start_lba\": %u,\n", XBOX_XGD1_GAME_OUTPUT_START_LBA);
	fprintf (json, "    \"layout_constants_match\": %s\n", xbox_redump_layout_constants_match_dic () ? "true" : "false");
	fprintf (json, "  },\n");
	fprintf (json, "  \"lead_in_capture\": {\n");
	fprintf (json, "    \"game_leadin_attempted\": true,\n");
	fprintf (json, "    \"unlocked_source_start_lba\": %u,\n",
		(meta -> game_source_lba >= XBOX_XISO_LEADIN_SECTORS) ?
		meta -> game_source_lba - XBOX_XISO_LEADIN_SECTORS : 0);
	fprintf (json, "    \"sector_count\": %u,\n", XBOX_XISO_LEADIN_SECTORS);
	fprintf (json, "    \"readable_sectors\": %u,\n", meta -> game_leadin_read_sectors);
	fprintf (json, "    \"zero_filled_sectors\": %u,\n", meta -> game_leadin_zero_sectors);
	fprintf (json, "    \"physical_geometry_evidence\": \"friidump_0.5.3.13_cache_aligned_raw_id_probe\"\n");
	fprintf (json, "  },\n");
	fprintf (json, "  \"dvd_structures\": {\n");
	fprintf (json, "    \"pfi\": "); if (pfi_leaf) xbox_json_string (json, pfi_leaf); else fprintf (json, "null"); fprintf (json, ",\n");
	fprintf (json, "    \"dmi\": "); if (dmi_leaf) xbox_json_string (json, dmi_leaf); else fprintf (json, "null"); fprintf (json, "\n");
	fprintf (json, "  },\n");
	fprintf (json, "  \"notes\": [\n");
	fprintf (json, "    \"PFI and DMI are stored as separate binary READ DVD STRUCTURE captures because they are not READ(10) user-data sectors.\",\n");
	fprintf (json, "    \"The 32-sector game lead-in is captured from unlocked source LBA 0..31; only unreadable lead-in sectors use zero-fill fallback.\",\n");
	fprintf (json, "    \"Pregame and postgame padding retain zero-filled placeholder content; their physical locations are resolved but their inaccessible bytes are not.\"\n");
	fprintf (json, "  ]\n");
	fprintf (json, "}\n");
	fclose (json);

	if (meta -> pfi_written) {
		debug ("Wrote Xbox PFI: %s", meta -> pfi_path);
	} else {
		warning ("Xbox PFI was not captured or could not be written");
	}
	if (meta -> dmi_written) {
		debug ("Wrote Xbox DMI: %s", meta -> dmi_path);
	} else {
		warning ("Xbox DMI was not captured or could not be written");
	}
	debug ("Wrote Xbox redump metadata: %s", meta -> json_path);
}

static bool xbox_write_iso_sectors (dumper *dmp, const u_int8_t *buf, u_int32_t sectors, u_int32_t output_lba, u_int32_t total_sectors) {
	if (dumper_cancel_requested (dmp))
		return false;
	if (!dmp || !dmp -> fp_iso || !buf)
		return false;
	clearerr (dmp -> fp_iso);
	if (fwrite (buf, SECTOR_SIZE, sectors, dmp -> fp_iso) != sectors || ferror (dmp -> fp_iso)) {
		error ("fwrite() to Xbox redump ISO failed at output LBA %u", output_lba);
		return false;
	}
	if (dmp -> hashing)
		multihash_update (&(dmp -> hash_iso), (u_int8_t *) buf, (size_t) sectors * SECTOR_SIZE);
	if (dmp -> flushing)
		fflush (dmp -> fp_iso);
	if (dmp -> progress && ((output_lba % 320) == 0 || output_lba + sectors >= total_sectors))
		dmp -> progress (false, output_lba + sectors, total_sectors, dmp -> progress_data);
	return true;
}

static bool xbox_write_zero_iso_range (dumper *dmp, u_int32_t sectors, u_int32_t output_lba, u_int32_t total_sectors, const char *label) {
	u_int8_t zero[BLOCK_SIZE];
	u_int32_t done = 0;
	memset (zero, 0, sizeof (zero));
	debug ("Writing Xbox %s zero-fill: output LBA %u, sectors %u", label ? label : "range", output_lba, sectors);
	while (done < sectors) {
		u_int32_t todo;
		if (dumper_cancel_requested (dmp))
			return false;
		todo = sectors - done;
		if (todo > SECTORS_PER_BLOCK)
			todo = SECTORS_PER_BLOCK;
		if (!xbox_write_iso_sectors (dmp, zero, todo, output_lba + done, total_sectors))
			return false;
		done += todo;
	}
	return true;
}

static bool xbox_dump_read10_iso_range (dumper *dmp, u_int32_t source_lba, u_int32_t sectors, u_int32_t output_lba, u_int32_t total_sectors, const char *label, u_int32_t *current_sector) {
	u_int8_t buf[BLOCK_SIZE];
	u_int8_t one[SECTOR_SIZE];
	u_int32_t done = 0;
	debug ("Reading Xbox %s: source LBA %u, output LBA %u, sectors %u", label ? label : "range", source_lba, output_lba, sectors);
	while (done < sectors) {
		u_int32_t todo;
		int retry;
		bool ok = false;
		if (dumper_cancel_requested (dmp))
			return false;
		todo = sectors - done;
		if (todo > SECTORS_PER_BLOCK)
			todo = SECTORS_PER_BLOCK;
		if (current_sector)
			*current_sector = output_lba + done;
		for (retry = 0; retry < 3 && !ok; retry++) {
			memset (buf, 0, sizeof (buf));
			if (disc_xbox_read_10 (dmp -> dsk, source_lba + done, todo, buf, sizeof (buf)) >= 0)
				ok = true;
		}
		if (!ok && todo > 1) {
			/* Some Xbox-capable drives are more reliable at visible DVD-video edges
			 * and layer/layout boundaries with single-sector READ(10) commands.
			 * Fall back before declaring the whole 16-sector batch bad. */
			u_int32_t i;
			bool singles_ok = true;
			for (i = 0; i < todo; i++) {
				bool one_ok = false;
				if (dumper_cancel_requested (dmp))
					return false;
				for (retry = 0; retry < 3 && !one_ok; retry++) {
					memset (one, 0, sizeof (one));
					if (disc_xbox_read_10 (dmp -> dsk, source_lba + done + i, 1, one, sizeof (one)) >= 0)
						one_ok = true;
				}
				if (!one_ok) {
					if (current_sector)
						*current_sector = output_lba + done + i;
					xbox_ref_log_fprintf (stderr, "\n[XBOX] READ(10) failed for %s at source LBA %u / output LBA %u\n", label ? label : "range", source_lba + done + i, output_lba + done + i);
					error ("Xbox READ(10) failed for %s at source LBA %u", label ? label : "range", source_lba + done + i);
					singles_ok = false;
					break;
				}
				memcpy (buf + ((size_t) i * SECTOR_SIZE), one, SECTOR_SIZE);
			}
			ok = singles_ok;
		}
		if (!ok) {
			if (current_sector)
				*current_sector = output_lba + done;
			xbox_ref_log_fprintf (stderr, "\n[XBOX] READ(10) failed for %s at source LBA %u / output LBA %u\n", label ? label : "range", source_lba + done, output_lba + done);
			error ("Xbox READ(10) failed for %s at source LBA %u", label ? label : "range", source_lba + done);
			return false;
		}
		if (!xbox_write_iso_sectors (dmp, buf, todo, output_lba + done, total_sectors))
			return false;
		done += todo;
	}
	return true;
}

static bool xbox_read_range_to_memory (dumper *dmp, u_int32_t source_lba, u_int32_t sectors, u_int8_t *out, size_t out_size, const char *label) {
	u_int8_t one[SECTOR_SIZE];
	u_int32_t done = 0;
	if (!dmp || !dmp -> dsk || !out || out_size < (size_t) sectors * SECTOR_SIZE)
		return false;
	debug ("Capturing Xbox %s to memory: source LBA %u, sectors %u", label ? label : "range", source_lba, sectors);
	while (done < sectors) {
		u_int32_t todo;
		int retry;
		bool ok = false;
		if (dumper_cancel_requested (dmp))
			return false;
		todo = sectors - done;
		if (todo > SECTORS_PER_BLOCK)
			todo = SECTORS_PER_BLOCK;
		for (retry = 0; retry < 3 && !ok; retry++) {
			if (disc_xbox_read_10 (dmp -> dsk, source_lba + done, todo, out + ((size_t) done * SECTOR_SIZE), out_size - ((size_t) done * SECTOR_SIZE)) >= 0)
				ok = true;
		}
		if (!ok && todo > 1) {
			u_int32_t i;
			bool singles_ok = true;
			for (i = 0; i < todo; i++) {
				bool one_ok = false;
				if (dumper_cancel_requested (dmp))
					return false;
				for (retry = 0; retry < 3 && !one_ok; retry++) {
					memset (one, 0, sizeof (one));
					if (disc_xbox_read_10 (dmp -> dsk, source_lba + done + i, 1, one, sizeof (one)) >= 0)
						one_ok = true;
				}
				if (!one_ok) {
					xbox_ref_log_fprintf (stderr, "\n[XBOX] READ(10) failed while capturing %s at source LBA %u\n", label ? label : "range", source_lba + done + i);
					error ("Xbox READ(10) failed while capturing %s at source LBA %u", label ? label : "range", source_lba + done + i);
					singles_ok = false;
					break;
				}
				memcpy (out + ((size_t) (done + i) * SECTOR_SIZE), one, SECTOR_SIZE);
			}
			ok = singles_ok;
		}
		if (!ok) {
			xbox_ref_log_fprintf (stderr, "\n[XBOX] READ(10) failed while capturing %s at source LBA %u\n", label ? label : "range", source_lba + done);
			error ("Xbox READ(10) failed while capturing %s at source LBA %u", label ? label : "range", source_lba + done);
			return false;
		}
		done += todo;
	}
	return true;
}

static bool xbox_write_leadin_iso_range (dumper *dmp, u_int32_t source_lba, u_int32_t output_lba, u_int32_t total_sectors, xbox_redump_metadata *meta, const char *label, u_int32_t *current_sector) {
	u_int8_t buf[SECTOR_SIZE];
	u_int8_t zero[SECTOR_SIZE];
	u_int32_t i;
	bool warned = false;
	memset (zero, 0, sizeof (zero));
	debug ("Attempting Xbox %s lead-in from source LBA %u..%u", label ? label : "game", source_lba, source_lba + XBOX_XISO_LEADIN_SECTORS - 1);
	for (i = 0; i < XBOX_XISO_LEADIN_SECTORS; i++) {
		const u_int8_t *src = buf;
		if (dumper_cancel_requested (dmp))
			return false;
		if (current_sector)
			*current_sector = output_lba + i;
		memset (buf, 0, sizeof (buf));
		if (disc_xbox_read_10 (dmp -> dsk, source_lba + i, 1, buf, sizeof (buf)) < 0) {
			if (!warned) {
				warning ("Drive could not read one or more Xbox lead-in sectors; zero-filling only unreadable lead-in sectors");
				warned = true;
			}
			src = zero;
			if (meta) meta -> game_leadin_zero_sectors++;
		} else {
			if (meta) meta -> game_leadin_read_sectors++;
		}
		if (!xbox_write_iso_sectors (dmp, src, 1, output_lba + i, total_sectors))
			return false;
	}
	return true;
}

static bool xbox_get_xiso_range (disc *d, u_int32_t *source_lba, u_int32_t *source_sectors, u_int32_t *target_sectors) {
	u_int32_t total_sectors, root_lba, volume_size_raw, volume_sectors;

	if (!d || !source_lba || !source_sectors || !target_sectors)
		return false;

	if (disc_xbox_unlock (d) < 0) {
		error ("Could not switch Xbox drive into the game/XDVDFS view");
		return false;
	}

	total_sectors = disc_get_sectors_no (d);
	root_lba = 0;
	volume_size_raw = 0;

	/* Try the historical physical/game-partition XDVDFS location first,
	 * then the unlocked/XISO-visible LBA 0x20. GDR-8050L after the
	 * native Xbox handshake normally resolves at 0x20.
	 */
	if (xbox_probe_xdfs_volume (d, XBOX_XISO_START_LBA_MAGIC, &root_lba, &volume_size_raw)) {
		*source_lba = XBOX_XISO_START_LBA_MAGIC;
	} else if (xbox_probe_xdfs_volume (d, XBOX_XISO_STANDARD_GAME_LBA, &root_lba, &volume_size_raw)) {
		*source_lba = XBOX_XISO_STANDARD_GAME_LBA;
	} else {
		error ("No Xbox XDVDFS volume found at LBA %u or %u", XBOX_XISO_START_LBA_MAGIC, XBOX_XISO_STANDARD_GAME_LBA);
		return false;
	}

	if (total_sectors > 3300000) {
		/* Retail dual-layer XISO copies from the detected game/XDVDFS
		 * start LBA through logical LBA 1913920, then prepends 32
		 * output lead-in sectors.
		 */
		if (*source_lba >= XBOX_XISO_DUAL_LAYER_END_LBA) {
			error ("Xbox XISO source LBA %u is beyond dual-layer XISO end LBA %u", *source_lba, XBOX_XISO_DUAL_LAYER_END_LBA);
			return false;
		}
		*source_sectors = XBOX_XISO_DUAL_LAYER_END_LBA - *source_lba;
	} else {
		/* For single-layer/homebrew-style media, XDVDFS VolumeSize is a
		 * byte count, so convert it to 2048-byte sectors.
		 */
		if (volume_size_raw == 0) {
			error ("Xbox XDVDFS header reported a zero volume size");
			return false;
		}

		volume_sectors = volume_size_raw / SECTOR_SIZE;
		if (volume_sectors == 0) {
			error ("Xbox XDVDFS volume size %u is smaller than one sector", volume_size_raw);
			return false;
		}
		if (*source_lba + volume_sectors > total_sectors) {
			warning ("Xbox XISO volume size extends beyond visible capacity; clipping to remaining visible sectors");
			if (total_sectors > *source_lba)
				volume_sectors = total_sectors - *source_lba;
			else
				return false;
		}
		*source_sectors = volume_sectors;
	}

	*target_sectors = *source_sectors + XBOX_XISO_LEADIN_SECTORS;
	debug ("Xbox XISO range: source LBA %u, descriptor RootLBA %u, source sectors %u, output sectors %u", *source_lba, root_lba, *source_sectors, *target_sectors);
	return true;
}


/**
 * Tries to open the output file for writing and to find out if it contains valid data so that the dump can continue.
 * @param dvd 
 * @param outfile 
 * @param[out] fp The file pointer to write to.
 * @param[out] start_sector The sector to start reading from.
 * @return true if the dumping can start/continue, false otherwise (for instance if outfile cannot be written to).
 */
bool dumper_set_raw_output_file (dumper *dmp, char *outfile_raw, bool resume) {
	bool out;
	my_off_t filesize;
	FILE *fp;

	if (!outfile_raw) {
		/* Raw output disabled */
		out = true;
		dmp -> start_sector_raw = -1;
		my_free (dmp -> outfile_raw);
		dmp -> outfile_raw = NULL;
	} else if (dmp -> outfile_raw) {
		error ("Raw output file already defined");
		out = false;
	} else if (!(fp = fopen (outfile_raw, "rb"))) {		/** @todo Maybe we could not open file for permission problems */
		/* Raw output file does not exist, start from scratch */
		out = true;
		dmp -> start_sector_raw = 0;
		my_strdup (dmp -> outfile_raw, outfile_raw);
	} else if (resume) {
		/* Raw output file exists and resume was requested, so see how many dumped sectors it contains */
		my_fseek (fp, 0, SEEK_END);
		filesize = my_ftell (fp);
		fclose (fp);
		out = true;
		dmp -> start_sector_raw = (u_int32_t) (filesize / RAW_SECTOR_SIZE / SECTORS_PER_BLOCK) * SECTORS_PER_BLOCK;
		debug ("Raw output can restart from sector %u", dmp -> start_sector_raw);
		my_strdup (dmp -> outfile_raw, outfile_raw);
	} else {
		/* Raw output file exists but resume was not requested, error */
		fclose (fp);
		error ("Raw output file exists, but resume was not requested.");
		out = false;
		dmp -> start_sector_raw = -1;
		my_free (dmp -> outfile_raw);
		dmp -> outfile_raw = NULL;
	}

	return (out);
}


bool dumper_set_iso_output_file (dumper *dmp, char *outfile_iso, bool resume) {
	bool out;
	my_off_t filesize;
	FILE *fp;

	if (!outfile_iso) {
		/* ISO output disabled */
		out = true;
		dmp -> start_sector_iso = -1;
		my_free (dmp -> outfile_iso);
		dmp -> outfile_iso = NULL;
	} else if (outfile_iso[0] == '\0') {
		/* Xbox/GDR-8050L auto-name placeholder; the copied reference path opens the final file. */
		out = true;
		dmp -> start_sector_iso = 0;
		my_strdup (dmp -> outfile_iso, outfile_iso);
	} else if (dmp -> outfile_iso) {
		error ("ISO output file already defined");
		out = false;
	} else if (!(fp = fopen (outfile_iso, "rb"))) {		/** @todo Maybe we could not open file for permission problems */
		/* Raw output file does not exist, start from scratch */
		out = true;
		dmp -> start_sector_iso = 0;
		my_strdup (dmp -> outfile_iso, outfile_iso);
	} else if (resume) {
		/* Raw output file exists and resume was requested, so see how many dumped sectors it contains */
		my_fseek (fp, 0, SEEK_END);
		filesize = my_ftell (fp);
		fclose (fp);
		out = true;
		dmp -> start_sector_iso = (u_int32_t) (filesize / SECTOR_SIZE / SECTORS_PER_BLOCK) * SECTORS_PER_BLOCK;
		debug ("ISO output can restart from sector %u", dmp -> start_sector_iso);
		my_strdup (dmp -> outfile_iso, outfile_iso);
	} else {
		/* Raw output file exists but resume was not requested, error */
		fclose (fp);
		error ("ISO output file exists, but resume was not requested.");
		out = false;
		dmp -> start_sector_iso = -1;
		my_free (dmp -> outfile_iso);
		dmp -> outfile_iso = NULL;
	}

	return (out);
}


bool dumper_set_xiso_output_file (dumper *dmp, char *outfile_xiso, bool resume) {
	bool out;
	my_off_t filesize;
	FILE *fp;

	if (!outfile_xiso) {
		out = true;
		dmp -> start_sector_xiso = -1;
		my_free (dmp -> outfile_xiso);
		dmp -> outfile_xiso = NULL;
	} else if (outfile_xiso[0] == '\0') {
		/* Xbox/GDR-8050L auto-name placeholder; the copied reference path opens the final file. */
		out = true;
		dmp -> start_sector_xiso = 0;
		my_strdup (dmp -> outfile_xiso, outfile_xiso);
	} else if (dmp -> outfile_xiso) {
		error ("XISO output file already defined");
		out = false;
	} else if (!(fp = fopen (outfile_xiso, "rb"))) {
		out = true;
		dmp -> start_sector_xiso = 0;
		my_strdup (dmp -> outfile_xiso, outfile_xiso);
	} else if (resume) {
		my_fseek (fp, 0, SEEK_END);
		filesize = my_ftell (fp);
		fclose (fp);
		out = true;
		dmp -> start_sector_xiso = (u_int32_t) (filesize / SECTOR_SIZE / SECTORS_PER_BLOCK) * SECTORS_PER_BLOCK;
		debug ("XISO output can restart from output sector %u", dmp -> start_sector_xiso);
		my_strdup (dmp -> outfile_xiso, outfile_xiso);
	} else {
		fclose (fp);
		error ("XISO output file exists, but resume was not requested.");
		out = false;
		dmp -> start_sector_xiso = -1;
		my_free (dmp -> outfile_xiso);
		dmp -> outfile_xiso = NULL;
	}

	return (out);
}


bool dumper_prepare_xiso (dumper *dmp) {
	bool out;
	u_int8_t buf[SECTOR_SIZE];
	size_t r;
	u_int32_t i;

	if (!dmp -> outfile_xiso) {
		error ("No XISO output file defined");
		return false;
	}

	dmp -> start_sector = dmp -> start_sector_xiso;

#ifndef WIN32
	if (!xbox_portable_resolve_output_path (dmp, true))
		return false;
#endif

#ifdef WIN32
	if (dmp -> outfile_xiso[0] == '\0' && disc_is_xbox_challenge_drive (dmp -> dsk)) {
		if (dmp -> hashing)
			multihash_init (&(dmp -> hash_xiso));
		dmp -> fp_xiso = NULL;
		return true;
	}
#endif

	if (dmp -> hashing)
		multihash_init (&(dmp -> hash_xiso));

	dmp -> fp_xiso = fopen (dmp -> outfile_xiso, "a+b");
	if (!dmp -> fp_xiso)
		return false;

	if (dmp -> hashing) {
		debug ("Calculating hashes for pre-existing XISO dump data");
		if (dmp -> start_sector > 0) {
			for (i = 0; i < dmp -> start_sector && (r = fread (buf, SECTOR_SIZE, 1, dmp -> fp_xiso)) > 0; i++)
				multihash_update (&(dmp -> hash_xiso), buf, SECTOR_SIZE);
			MY_ASSERT (r > 0);
		}
	}

	if (my_fseek (dmp -> fp_xiso, dmp -> start_sector * SECTOR_SIZE, SEEK_SET) == 0 &&
	    ftruncate (fileno (dmp -> fp_xiso), (int64_t) dmp -> start_sector * SECTOR_SIZE) == 0) {
		out = true;
		debug ("Writing to file \"%s\" in Xbox XISO format (fseeked() to %lld)", dmp -> outfile_xiso, my_ftell (dmp -> fp_xiso));
	} else {
		out = false;
		fclose (dmp -> fp_xiso);
		dmp -> fp_xiso = NULL;
	}

	return (out);
}


bool dumper_prepare (dumper *dmp) {
	bool out;
	u_int8_t buf[RAW_SECTOR_SIZE];
	size_t r;
	u_int32_t i;

	/* Outputting to both files, resume must start from the file with the least sectors. Hopefully they will have the same number of sectors, anyway... */
	if (dmp -> outfile_raw && dmp -> outfile_iso && dmp -> start_sector_raw != dmp -> start_sector_iso) {
		if (dmp -> start_sector_raw < dmp -> start_sector_iso)
			dmp -> start_sector = dmp -> start_sector_raw;
		else
			dmp -> start_sector = dmp -> start_sector_iso;
	} else if (dmp -> outfile_raw) {
		dmp -> start_sector = dmp -> start_sector_raw;
	} else if (dmp -> outfile_iso) {
		dmp -> start_sector = dmp -> start_sector_iso;
	} else {
		MY_ASSERT (0);
	}

#ifndef WIN32
	if (dmp -> outfile_iso && dmp -> outfile_iso[0] == '\0' &&
			disc_is_xbox_challenge_drive (dmp -> dsk) &&
			!xbox_portable_resolve_output_path (dmp, false))
		return false;
#endif

#ifdef WIN32
	if (dmp -> outfile_iso && dmp -> outfile_iso[0] == '\0' && disc_is_xbox_challenge_drive (dmp -> dsk)) {
		if (dmp -> hashing) {
			multihash_init (&(dmp -> hash_raw));
			multihash_init (&(dmp -> hash_iso));
		}
		dmp -> fp_raw = NULL;
		dmp -> fp_iso = NULL;
		return true;
	}
#endif

	/* Prepare hashes */
	if (dmp -> hashing) {
		multihash_init (&(dmp -> hash_raw));
		multihash_init (&(dmp -> hash_iso));
	}

	/* Setup raw output file */
	if (dmp -> outfile_raw) {
		dmp -> fp_raw = fopen (dmp -> outfile_raw, "a+b");

		if (dmp -> hashing) {
			debug ("Calculating hashes for pre-existing raw dump data");
			if (dmp -> start_sector > 0) {
				for (i = 0; i < dmp -> start_sector && (r = fread (buf, RAW_SECTOR_SIZE, 1, dmp -> fp_raw)) > 0; i++)
					multihash_update (&(dmp -> hash_raw), buf, RAW_SECTOR_SIZE);
				MY_ASSERT (r > 0);
			}
		}

		/* Now call fseek as file will only be written, from now on */
		if (my_fseek (dmp -> fp_raw, dmp -> start_sector * RAW_SECTOR_SIZE, SEEK_SET) == 0 &&
		    ftruncate (fileno (dmp -> fp_raw), (int64_t) dmp -> start_sector * RAW_SECTOR_SIZE) == 0) {
			out = true;
			debug ("Writing to file \"%s\" in raw format (fseeked() to %lld)", dmp -> outfile_raw, my_ftell (dmp -> fp_raw));
		} else {
			out = false;
			fclose (dmp -> fp_raw);
			dmp -> fp_raw = NULL;
		}
	} else {
		dmp -> fp_raw = NULL;
	}

	/* Setup ISO output file */
	if (dmp -> outfile_iso) {
		dmp -> fp_iso = fopen (dmp -> outfile_iso, "a+b");

		if (dmp -> hashing) {
			debug ("Calculating hashes for pre-existing ISO dump data");
			if (dmp -> start_sector > 0) {
				for (i = 0; i < dmp -> start_sector && (r = fread (buf, SECTOR_SIZE, 1, dmp -> fp_iso)) > 0; i++)
					multihash_update (&(dmp -> hash_iso), buf, SECTOR_SIZE);
				MY_ASSERT (r > 0);
			}
		}

		if (my_fseek (dmp -> fp_iso, dmp -> start_sector * SECTOR_SIZE, SEEK_SET) == 0 &&
		    ftruncate (fileno (dmp -> fp_iso), (int64_t) dmp -> start_sector * SECTOR_SIZE) == 0) {
			out = true;
			debug ("Writing to file \"%s\" in ISO format (fseeked() to %lld)", dmp -> outfile_iso, my_ftell (dmp -> fp_iso));
		} else {
			out = false;
			fclose (dmp -> fp_iso);
			dmp -> fp_iso = NULL;
		}
	} else {
		dmp -> fp_iso = NULL;
	}

	return (out);
}



static int dumper_dump_xbox_redump_iso (dumper *dmp, u_int32_t *current_sector) {
	xbox_redump_metadata meta;
	u_int8_t *video_l1 = NULL;
	u_int32_t locked_sectors = 0, unlocked_sectors = 0, sector_size = 0;
	u_int32_t game_source_lba = 0xFFFFFFFFU;
	u_int32_t root_lba = 0, volume_size_raw = 0;
	u_int32_t pregame_padding = XBOX_XGD1_GAME_OUTPUT_START_LBA - XBOX_XGD1_VIDEO_L0_SECTORS;
	u_int32_t postgame_padding = XBOX_XGD1_VIDEO_L1_OUTPUT_START_LBA - (XBOX_XGD1_GAME_OUTPUT_START_LBA + XBOX_XGD1_UNLOCKED_GAME_VIEW_SECTORS);
	bool out = false;
	bool locked_view_is_xdfs;
	disc_type type_id;
	char *type = NULL;
	double portable_start = 0.0;
	int volume_lock_result;

	if (!dmp)
		return false;

	disc_get_type (dmp -> dsk, &type_id, &type);
	if (type_id != DISC_TYPE_XBOX)
		return false;

	if (dmp -> fp_raw) {
		error ("Xbox redump-style ISO output cannot be combined with -r/raw output in one run");
		return false;
	}

	if (dmp -> start_sector != 0) {
		error ("Resume is not supported for Xbox redump-style ISO reconstruction because the drive view changes mid-dump");
		return false;
	}

#ifdef WIN32
	if (disc_is_xbox_challenge_drive (dmp -> dsk)) {
		int ref_status;
		if (dmp -> fp_iso) {
			fclose (dmp -> fp_iso);
			dmp -> fp_iso = NULL;
		}
		if (current_sector)
			*current_sector = 0;
		xbox_ref_log_fprintf (stderr, "[XBOX] Dispatching GDR-8050L -i directly to copied original dumper code using FriiDump's existing drive HANDLE.\n");
		ref_status = xbox_ref_gdr8050l_dump_with_handle (disc_get_native_handle (dmp -> dsk), disc_get_device (dmp -> dsk), dmp -> outfile_iso, '1', dumper_xbox_ref_cancel_requested, dmp, &(dmp -> xbox_ref_result));
		if (current_sector)
			*current_sector = dmp -> xbox_ref_result.completed_sectors;
		return ref_status == 0;
	}
#endif

	if (!dmp -> fp_iso)
		return false;

	memset (&meta, 0, sizeof (meta));
	portable_start = xbox_portable_now_seconds ();
	xbox_portable_result_begin (dmp, dmp -> outfile_iso, '1');

	/* Original GDR-8050L dumper order for redump-style output:
	 *   1. unlock/authenticate into the game view;
	 *   2. read XDVDFS/game metadata while that view is available;
	 *   3. perform a real media transition to return to the visible DVD-video view;
	 *   4. dump video/front/tail, unlock again, then dump game data.
	 *
	 * The video partition is not encrypted/locked; "lock" here only means the
	 * drive-visible DVD-video state before the Xbox game-view unlock. */
	if (disc_is_xbox_challenge_drive (dmp -> dsk)) {
		xbox_ref_log_fprintf (stderr, "[XBOX] GDR-8050L Linux flow mirrors the proven Windows state-aware sequence.\n");
		xbox_ref_log_fprintf (stderr, "[XBOX] Windows sequence 1/4: establish and verify the game view.\n");
		if (disc_xbox_prepare_game_view (dmp -> dsk, &unlocked_sectors, &sector_size) < 0) {
			xbox_ref_log_fprintf (stderr, "[XBOX][FATAL] Windows-sequence game-view preparation failed.\n");
			goto cleanup;
		}

		xbox_ref_log_fprintf (stderr, "[XBOX] Windows sequence 2/4: acquire the volume lock and probe metadata.\n");
		volume_lock_result = disc_xbox_lock_volume (dmp -> dsk);
		if (volume_lock_result < 0)
			xbox_ref_log_fprintf (stderr, "[XBOX][WARN] Exclusive volume lock failed; continuing like reference dumper warning path.\n");
		else if (volume_lock_result > 0)
			xbox_ref_log_fprintf (stderr, "[XBOX][INFO] Linux exclusive optical-device lock is unavailable; continuing without one.\n");
		disc_xbox_read_capacity_10 (dmp -> dsk, &unlocked_sectors, &sector_size);
		meta.unlocked_visible_sectors = unlocked_sectors;
		xbox_capture_pfi_dmi (dmp -> dsk, &meta);
		if (xbox_probe_xdfs_volume (dmp -> dsk, XBOX_XISO_STANDARD_GAME_LBA, &root_lba, &volume_size_raw)) {
			meta.game_source_lba = XBOX_XISO_STANDARD_GAME_LBA;
			meta.game_source_sectors = XBOX_XGD1_GAME_SOURCE_SECTORS;
		} else if (xbox_probe_xdfs_volume (dmp -> dsk, XBOX_XISO_START_LBA_MAGIC, &root_lba, &volume_size_raw)) {
			meta.game_source_lba = XBOX_XISO_START_LBA_MAGIC;
			meta.game_source_sectors = XBOX_XGD1_GAME_SOURCE_SECTORS;
		}
		xbox_portable_result_capture_metadata (dmp);

		xbox_ref_log_fprintf (stderr, "[XBOX] Windows sequence 3/4: media cycle back to visible DVD-video view before sector 0 capture.\n");
		if (disc_xbox_media_cycle (dmp -> dsk) < 0) {
			xbox_ref_log_fprintf (stderr, "[XBOX][FATAL] Stage 5 failed: could not restore visible DVD-video view.\n");
			goto cleanup;
		}
		/* Windows raw path: AutomateTrayCycle(); RefreshVolume(); SetDriveSpeedMax(). */
		disc_xbox_refresh_volume (dmp -> dsk);
		disc_set_speed (dmp -> dsk, 0xFFFF);
	} else if (disc_is_xbox_vendor_unlock_drive (dmp -> dsk)) {
		/* GDR-3120L/Kreon-style FF 08 01 profiles can explicitly select
		 * state 0 for the visible DVD-video view. */
		if (disc_xbox_lock (dmp -> dsk) < 0)
			warning ("Could not set Xbox vendor lock state 0; continuing with the current visible view");
	}

	disc_xbox_read_capacity_10 (dmp -> dsk, &locked_sectors, &sector_size);
	meta.locked_visible_sectors = locked_sectors;
	xbox_capture_pfi_dmi (dmp -> dsk, &meta);

	locked_view_is_xdfs = xbox_probe_xdfs_volume (dmp -> dsk, XBOX_XISO_STANDARD_GAME_LBA, NULL, NULL);
	if (locked_view_is_xdfs && disc_is_xbox_challenge_drive (dmp -> dsk)) {
		error ("Current GDR-8050L view still exposes XDVDFS at LBA 32 after media cycle; cannot capture visible DVD-video view for redump-style ISO");
		goto cleanup;
	}

	if (!xbox_redump_layout_constants_match_dic ()) {
		error ("Xbox redump layout constants do not match the DiscImageCreator-compatible XGD1 layout");
		goto cleanup;
	}

	meta.output_sectors = XBOX_XGD1_FULL_REDUMP_SECTORS;
	if (dmp -> progress)
		dmp -> progress (true, 0, meta.output_sectors, dmp -> progress_data);

	debug ("Xbox redump-style layout: video L0=%u, pregame padding=%u, game view=%u, postgame padding=%u, video L1=%u",
		XBOX_XGD1_VIDEO_L0_SECTORS, pregame_padding, XBOX_XGD1_UNLOCKED_GAME_VIEW_SECTORS, postgame_padding, XBOX_XGD1_VIDEO_L1_SECTORS);

	video_l1 = (u_int8_t *) malloc ((size_t) XBOX_XGD1_VIDEO_L1_SECTORS * SECTOR_SIZE);
	if (!video_l1) {
		error ("Could not allocate Xbox VIDEO-L1 capture buffer");
		goto cleanup;
	}

	/* Capture the tail of the visible DVD-video view before switching to the game view. */
	if (!xbox_read_range_to_memory (dmp, XBOX_XGD1_VIDEO_L0_SECTORS, XBOX_XGD1_VIDEO_L1_SECTORS,
			video_l1, (size_t) XBOX_XGD1_VIDEO_L1_SECTORS * SECTOR_SIZE, "VIDEO-L1"))
		goto cleanup;

	if (!xbox_dump_read10_iso_range (dmp, 0, XBOX_XGD1_VIDEO_L0_SECTORS, 0, meta.output_sectors, "VIDEO-L0", current_sector))
		goto cleanup;

	if (current_sector) *current_sector = XBOX_XGD1_VIDEO_L0_SECTORS;
	if (!xbox_write_zero_iso_range (dmp, pregame_padding, XBOX_XGD1_VIDEO_L0_SECTORS, meta.output_sectors, "PREGAME-PADDING"))
		goto cleanup;

	xbox_ref_log_fprintf (stderr, "[XBOX] Windows sequence 4/4: re-establish and verify the game view for XDVDFS data.\n");
	if (disc_is_xbox_challenge_drive (dmp -> dsk)) {
		if (disc_xbox_prepare_game_view (dmp -> dsk, &unlocked_sectors, &sector_size) < 0) {
			xbox_ref_log_fprintf (stderr, "[XBOX][FATAL] Final Windows-sequence game-view preparation failed.\n");
			goto cleanup;
		}
	} else {
		if (disc_xbox_unlock (dmp -> dsk) < 0)
			xbox_ref_log_fprintf (stderr, "[XBOX][WARN] Xbox vendor unlock returned failure; XDVDFS probe remains authoritative.\n");
		disc_xbox_read_capacity_10 (dmp -> dsk, &unlocked_sectors, &sector_size);
	}
	meta.unlocked_visible_sectors = unlocked_sectors;
	xbox_capture_pfi_dmi (dmp -> dsk, &meta);

	if (xbox_probe_xdfs_volume (dmp -> dsk, XBOX_XISO_STANDARD_GAME_LBA, &root_lba, &volume_size_raw))
		game_source_lba = XBOX_XISO_STANDARD_GAME_LBA;
	else if (xbox_probe_xdfs_volume (dmp -> dsk, XBOX_XISO_START_LBA_MAGIC, &root_lba, &volume_size_raw))
		game_source_lba = XBOX_XISO_START_LBA_MAGIC;
	else {
		error ("Could not locate Xbox XDVDFS header after unlock at LBA %u or %u", XBOX_XISO_STANDARD_GAME_LBA, XBOX_XISO_START_LBA_MAGIC);
		goto cleanup;
	}

	meta.game_source_lba = game_source_lba;
	meta.game_source_sectors = XBOX_XGD1_GAME_SOURCE_SECTORS;
	xbox_portable_result_capture_metadata (dmp);
	if (game_source_lba != XBOX_XISO_STANDARD_GAME_LBA && disc_is_xbox_challenge_drive (dmp -> dsk))
		warning ("Xbox XDVDFS was detected at LBA %u, not the expected GDR-8050L game-view LBA 32", game_source_lba);

	/* FriiDump 0.5.3.13 cache-aligned raw-ID validation proved that the
	 * unlocked GDR-8050L view exposes source LBA 0..31 as the physical
	 * game-region lead-in immediately preceding the XDVDFS header at LBA 32.
	 * Read those sectors from the drive; retain the existing per-sector
	 * zero-fill fallback only for genuinely unreadable sectors. */
	xbox_ref_log_fprintf (stderr,
		"[XBOX] Capturing 32-sector game lead-in from unlocked source LBA %u..%u.\n",
		(game_source_lba >= XBOX_XISO_LEADIN_SECTORS) ? game_source_lba - XBOX_XISO_LEADIN_SECTORS : 0,
		(game_source_lba >= XBOX_XISO_LEADIN_SECTORS) ? game_source_lba - 1 : XBOX_XISO_LEADIN_SECTORS - 1);
	if (!xbox_write_leadin_iso_range (dmp,
			(game_source_lba >= XBOX_XISO_LEADIN_SECTORS) ? game_source_lba - XBOX_XISO_LEADIN_SECTORS : 0,
			XBOX_XGD1_GAME_OUTPUT_START_LBA, meta.output_sectors, &meta, "redump game", current_sector))
		goto cleanup;

	if (!xbox_dump_read10_iso_range (dmp, game_source_lba, XBOX_XGD1_GAME_SOURCE_SECTORS,
			XBOX_XGD1_GAME_OUTPUT_START_LBA + XBOX_XISO_LEADIN_SECTORS, meta.output_sectors, "GAME-XDVDFS", current_sector))
		goto cleanup;

	if (current_sector) *current_sector = XBOX_XGD1_GAME_OUTPUT_START_LBA + XBOX_XGD1_UNLOCKED_GAME_VIEW_SECTORS;
	if (!xbox_write_zero_iso_range (dmp, postgame_padding, XBOX_XGD1_GAME_OUTPUT_START_LBA + XBOX_XGD1_UNLOCKED_GAME_VIEW_SECTORS,
			meta.output_sectors, "POSTGAME-PADDING"))
		goto cleanup;

	if (!xbox_write_iso_sectors (dmp, video_l1, XBOX_XGD1_VIDEO_L1_SECTORS, XBOX_XGD1_VIDEO_L1_OUTPUT_START_LBA, meta.output_sectors))
		goto cleanup;

	if (dmp -> hashing)
		multihash_finish (&(dmp -> hash_iso));
	if (dmp -> fp_iso) {
		fclose (dmp -> fp_iso);
		dmp -> fp_iso = NULL;
	}

	xbox_write_redump_metadata (dmp, &meta);
	out = true;

cleanup:
	if (!out) {
		/* A failed or cancelled Xbox reconstruction still owns an open output
		 * stream. Finalize the partial streaming hash and close the file so the
		 * native report can observe the exact completed byte count and the caller
		 * never leaks the handle. */
		if (dmp -> hashing)
			multihash_finish (&(dmp -> hash_iso));
		if (dmp -> fp_iso) {
			fflush (dmp -> fp_iso);
			fclose (dmp -> fp_iso);
			dmp -> fp_iso = NULL;
		}
	}
	if (video_l1)
		free (video_l1);
	xbox_portable_result_finish (dmp, out, meta.output_sectors, portable_start, false);
	/* Keep current_sector at the last phase-specific LBA on failure so the
	 * CLI can report a useful range instead of collapsing every setup failure
	 * back to 0..15. */
	return out;
}

int dumper_dump (dumper *dmp, u_int32_t *current_sector) {
	bool out;
	u_int8_t *rawbuf, *isobuf;
	u_int32_t i, sectors_no, last_sector;
	disc_type type_id;
	char *type_string = NULL;
#ifdef DEBUGaa
	bool no_unscrambling;
#endif

#ifdef DEBUGaa
	no_unscrambling = dd -> no_unscrambling;
	if (fp_iso && no_unscrambling) {
		warning ("Output to ISO format requested, ignoring no_unscrambling!");
		no_unscrambling = false;
	}
#endif

	disc_get_type (dmp -> dsk, &type_id, &type_string);
	if (type_id == DISC_TYPE_XBOX && dmp -> fp_iso)
		return dumper_dump_xbox_redump_iso (dmp, current_sector);

	sectors_no = disc_get_sectors_no (dmp -> dsk);
#if 0
	if (dd -> start_sector != -1) {
		if (dd -> start_sector >= sectors_no) {
			error ("Cannot start dumping from sector %u as the inserted disc only has %u sectors\n", dd -> start_sector, sectors_no);
			out = false;
		} else {
			warning ("Start sector forced to %u\n", dd -> start_sector);
			ss = dd -> start_sector;

			if (fp_iso)
				fseek (fp_iso, ss * 2048, SEEK_SET);
			if (fp_raw)
				fseek (fp_raw, ss * 2064, SEEK_SET);
		}
	}
#endif
	
	if (true) {
		debug ("Starting dump process from sector %u...\n", dmp -> start_sector);

		/* First call to progress function */
		if (dmp -> progress)
			dmp -> progress (true, dmp -> start_sector, sectors_no, dmp -> progress_data);

		last_sector=sectors_no-1;
		
		for (i = dmp -> start_sector, out = true; i < sectors_no && out; i++) {
			if (dumper_cancel_requested (dmp)) {
				out = false;
				if (current_sector)
					*current_sector = i;
				break;
			}
			if (!dumper_read_sector_with_retry (dmp, i, &isobuf, &rawbuf)) {
				error ("Read failed after dump-level retries at sectors %u..%u", i, i + SECTORS_PER_BLOCK - 1);
				out = false;
				*(current_sector) = i;
			}

			if (out && dmp -> fp_raw) {
				clearerr (dmp -> fp_raw);

				if (!rawbuf) {
					error ("NULL buffer");
					out = false;
					*(current_sector) = i;
				}
				else fwrite (rawbuf, RAW_SECTOR_SIZE, 1, dmp -> fp_raw);
				if (ferror (dmp -> fp_raw)) {
					error ("fwrite() to raw output file failed");
					out = false;
					*(current_sector) = i;
				}

				if (dmp -> flushing)
					fflush (dmp -> fp_raw);

				if (dmp -> hashing && out)
					multihash_update (&(dmp -> hash_raw), rawbuf, RAW_SECTOR_SIZE);
			}

			if (out && dmp -> fp_iso) {
				clearerr (dmp -> fp_iso);

				if (!isobuf) {
					error ("NULL buffer");
					out = false;
					*(current_sector) = i;
				}
				else fwrite (isobuf, SECTOR_SIZE, 1, dmp -> fp_iso);

				if (ferror (dmp -> fp_iso)) {
					error ("fwrite() to ISO output file failed");
					out = false;
					*(current_sector) = i;
				}

				if (dmp -> flushing)
					fflush (dmp -> fp_iso);

				if (dmp -> hashing && out)
					multihash_update (&(dmp -> hash_iso), isobuf, SECTOR_SIZE);
			}

			if ((i % 320 == 0) || (i == last_sector)) { //speedhack
				if (dmp -> progress)
					dmp -> progress (false, i + 1, sectors_no, dmp -> progress_data);		/* i + 1 'cause sectors range from 0 to N */
			}
		}

		if (dmp -> hashing) {
			multihash_finish (&(dmp -> hash_raw));
			multihash_finish (&(dmp -> hash_iso));
		}

		if (dmp -> fp_raw)
			fclose (dmp -> fp_raw);
		if (dmp -> fp_iso)
			fclose (dmp -> fp_iso);
		if (out) {


		}
	}

	return (out);
}


int dumper_dump_xiso (dumper *dmp, u_int32_t *current_sector) {
	bool out;
	u_int8_t *rawbuf, *isobuf;
	u_int8_t sector_buf[SECTOR_SIZE];
	u_int32_t source_lba, source_sectors, target_sectors, i, source_sector, last_sector, leadin_source_lba;
	bool leadin_fallback_warned;
	double portable_start = 0.0;

	if (!dmp) {
		error ("XISO dumper is not prepared");
		return false;
	}

#ifdef WIN32
	if (disc_is_xbox_challenge_drive (dmp -> dsk)) {
		int ref_status;
		if (dmp -> fp_xiso) {
			fclose (dmp -> fp_xiso);
			dmp -> fp_xiso = NULL;
		}
		if (current_sector)
			*current_sector = 0;
		xbox_ref_log_fprintf (stderr, "[XBOX] Dispatching GDR-8050L -X directly to copied original dumper code using FriiDump's existing drive HANDLE.\n");
		ref_status = xbox_ref_gdr8050l_dump_with_handle (disc_get_native_handle (dmp -> dsk), disc_get_device (dmp -> dsk), dmp -> outfile_xiso, '2', dumper_xbox_ref_cancel_requested, dmp, &(dmp -> xbox_ref_result));
		if (current_sector)
			*current_sector = dmp -> xbox_ref_result.completed_sectors;
		return ref_status == 0;
	}
#endif

	if (!dmp -> fp_xiso) {
		error ("XISO dumper is not prepared");
		return false;
	}

	portable_start = xbox_portable_now_seconds ();
	xbox_portable_result_begin (dmp, dmp -> outfile_xiso, '2');

	if (!xbox_get_xiso_range (dmp -> dsk, &source_lba, &source_sectors, &target_sectors)) {
		if (current_sector)
			*current_sector = dmp -> start_sector;
		xbox_portable_result_finish (dmp, false, 0, portable_start, true);
		return false;
	}
	xbox_portable_result_capture_metadata (dmp);

	if (dmp -> start_sector >= target_sectors) {
		error ("Cannot resume Xbox XISO at output sector %u; target has only %u sectors", dmp -> start_sector, target_sectors);
		if (current_sector)
			*current_sector = dmp -> start_sector;
		xbox_portable_result_finish (dmp, false, target_sectors, portable_start, true);
		return false;
	}

	memset (sector_buf, 0, sizeof (sector_buf));
	leadin_source_lba = (source_lba >= XBOX_XISO_LEADIN_SECTORS) ? (source_lba - XBOX_XISO_LEADIN_SECTORS) : 0;
	leadin_fallback_warned = false;
	out = true;
	last_sector = target_sectors - 1;

	debug ("Starting Xbox XISO dump from output sector %u...", dmp -> start_sector);
	debug ("Xbox XISO lead-in source LBA %u..%u will be attempted before zero-fill fallback", leadin_source_lba, leadin_source_lba + XBOX_XISO_LEADIN_SECTORS - 1);
	if (dmp -> progress)
		dmp -> progress (true, dmp -> start_sector, target_sectors, dmp -> progress_data);

	for (i = dmp -> start_sector; i < target_sectors && out; i++) {
		if (dumper_cancel_requested (dmp)) {
			out = false;
			if (current_sector)
				*current_sector = i;
			break;
		}
		if (i < XBOX_XISO_LEADIN_SECTORS) {
			isobuf = NULL;
			rawbuf = NULL;
			if (disc_xbox_read_10 (dmp -> dsk, leadin_source_lba + i, 1, sector_buf, sizeof (sector_buf)) >= 0) {
				isobuf = sector_buf;
			} else {
				if (!leadin_fallback_warned) {
					warning ("Drive could not read one or more Xbox XISO lead-in sectors; using zero-fill fallback for unreadable lead-in sectors");
					leadin_fallback_warned = true;
				}
				memset (sector_buf, 0, sizeof (sector_buf));
				isobuf = sector_buf;
			}

			clearerr (dmp -> fp_xiso);
			fwrite (isobuf, SECTOR_SIZE, 1, dmp -> fp_xiso);
			if (ferror (dmp -> fp_xiso)) {
				error ("fwrite() to XISO output file failed");
				out = false;
				if (current_sector)
					*current_sector = i;
			}

			if (dmp -> hashing && out)
				multihash_update (&(dmp -> hash_xiso), isobuf, SECTOR_SIZE);
		} else {
			source_sector = source_lba + (i - XBOX_XISO_LEADIN_SECTORS);
			isobuf = sector_buf;
			rawbuf = NULL;
			if (disc_xbox_read_10 (dmp -> dsk, source_sector, 1, sector_buf, sizeof (sector_buf)) < 0) {
				error ("Xbox XISO READ(10) failed at source LBA %u", source_sector);
				out = false;
				if (current_sector)
					*current_sector = i;
			} else {
				clearerr (dmp -> fp_xiso);
				fwrite (isobuf, SECTOR_SIZE, 1, dmp -> fp_xiso);
				if (ferror (dmp -> fp_xiso)) {
					error ("fwrite() to XISO output file failed");
					out = false;
					if (current_sector)
						*current_sector = i;
				}

				if (dmp -> hashing && out)
					multihash_update (&(dmp -> hash_xiso), isobuf, SECTOR_SIZE);
			}
		}

		if (dmp -> flushing)
			fflush (dmp -> fp_xiso);

		if ((i % 320 == 0) || (i == last_sector)) {
			if (dmp -> progress)
				dmp -> progress (false, i + 1, target_sectors, dmp -> progress_data);
		}
	}

	if (dmp -> hashing)
		multihash_finish (&(dmp -> hash_xiso));

	if (dmp -> fp_xiso) {
		fclose (dmp -> fp_xiso);
		dmp -> fp_xiso = NULL;
	}

	xbox_portable_result_finish (dmp, out, target_sectors, portable_start, true);

	return (out);
}


dumper *dumper_new (disc *d) {
	dumper *dmp;

	dmp = (dumper *) malloc (sizeof (dumper));
	memset (dmp, 0, sizeof (dumper));
	dmp -> dsk = d;
	xbox_ref_dump_result_init (&(dmp -> xbox_ref_result));
	dumper_set_hashing (dmp, true);
	dumper_set_flushing (dmp, true);

	return (dmp);
}


void dumper_set_progress_callback (dumper *dmp, progress_func progress, void *progress_data) {
	dmp -> progress = progress;
	dmp -> progress_data = progress_data;

	return;
}


void dumper_set_cancel_callback (dumper *dmp, dumper_cancel_func cancel, void *cancel_data) {
	if (!dmp)
		return;
	dmp -> cancel = cancel;
	dmp -> cancel_data = cancel_data;
}


bool dumper_was_cancelled (dumper *dmp) {
	return dmp && dmp -> cancelled;
}


void dumper_set_hashing (dumper *dmp, bool h) {
	dmp -> hashing = h;
	debug ("Hashing %s", h ? "enabled" : "disabled");

	return;
}


void dumper_set_flushing (dumper *dmp, bool f) {
	dmp -> flushing = f;
	debug ("Flushing %s", f ? "enabled" : "disabled");

	return;
}

void *dumper_destroy (dumper *dmp) {
	my_free (dmp -> outfile_raw);
	my_free (dmp -> outfile_iso);
	my_free (dmp -> outfile_xiso);
	my_free (dmp);

	return (NULL);
}


char *dumper_get_iso_crc32 (dumper *dmp) {
	return ((dmp -> hash_iso).crc32_s);
}


char *dumper_get_raw_crc32 (dumper *dmp) {
	return ((dmp -> hash_raw).crc32_s);
}

char *dumper_get_xiso_crc32 (dumper *dmp) {
	return ((dmp -> hash_xiso).crc32_s);
}


char *dumper_get_iso_md4 (dumper *dmp) {
	return ((dmp -> hash_iso).md4_s);
}


char *dumper_get_raw_md4 (dumper *dmp) {
	return ((dmp -> hash_raw).md4_s);
}

char *dumper_get_xiso_md4 (dumper *dmp) {
	return ((dmp -> hash_xiso).md4_s);
}


char *dumper_get_iso_md5 (dumper *dmp) {
	return ((dmp -> hash_iso).md5_s);
}


char *dumper_get_raw_md5 (dumper *dmp) {
	return ((dmp -> hash_raw).md5_s);
}

char *dumper_get_xiso_md5 (dumper *dmp) {
	return ((dmp -> hash_xiso).md5_s);
}


char *dumper_get_iso_ed2k (dumper *dmp) {
	return ((dmp -> hash_iso).ed2k_s);
}


char *dumper_get_raw_ed2k (dumper *dmp) {
	return ((dmp -> hash_raw).ed2k_s);
}


char *dumper_get_iso_sha1 (dumper *dmp) {
	return ((dmp -> hash_iso).sha1_s);
}


char *dumper_get_raw_sha1 (dumper *dmp) {
	return ((dmp -> hash_raw).sha1_s);
}

char *dumper_get_xiso_sha1 (dumper *dmp) {
	return ((dmp -> hash_xiso).sha1_s);
}


char *dumper_get_iso_sha2 (dumper *dmp) {
	return ((dmp -> hash_iso).sha2_s);
}


char *dumper_get_raw_sha2 (dumper *dmp) {
	return ((dmp -> hash_raw).sha2_s);
}

char *dumper_get_xiso_sha2 (dumper *dmp) {
	return ((dmp -> hash_xiso).sha2_s);
}


bool dumper_get_xbox_reference_result (dumper *dmp, xbox_ref_dump_result *result) {
	if (!dmp || !result || !(dmp -> xbox_ref_result).attempted)
		return false;
	*result = dmp -> xbox_ref_result;
	return true;
}

u_int32_t dumper_get_start_sector (dumper *dmp) {
	return dmp ? dmp -> start_sector : 0;
}
