+/* -*-c-*-
+ *
+ * Make an unscrambled copy of a DVD.
+ *
+ * (c) 2022 Mark Wooding
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of the DVD ripping toolset.
+ *
+ * DVDrip 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 3 of the License, or (at your
+ * option) any later version.
+ *
+ * DVDrip 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 DVDrip. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/*----- Header files ------------------------------------------------------*/
+
#include "lib.h"
+#ifdef __linux__
+# include <linux/cdrom.h>
+#endif
+
+/*----- Program usage summary ---------------------------------------------*/
+
static void usage(FILE *fp)
{
fprintf(fp,
prog);
}
-static double tvdiff(const struct timeval *tv_lo,
- const struct timeval *tv_hi)
+/*----- Random utilities --------------------------------------------------*/
+
+#define PRF_HYPHEN 1u
+static int parse_range(const char *p, unsigned f,
+ secaddr *start_out, secaddr *end_out)
+ /* Parse a range of sectors from the string P. If successful, store
+ * the specified start sector address in *START_OUT and the end
+ * address in *END_OUT, and return zero. On failure, return -1;
+ * *START_OUT and/or *END_OUT are clobbered.
+ *
+ * The acceptable syntax depends on the flags.
+ *
+ * * The `PRF_HYPHEN' syntax is intended for use on the
+ * command-line. It accepts `[START]-[END]'; if the start and/or
+ * end addresses are omitted then *START_OUT and/or *END_OUT are
+ * left unchanged.
+ *
+ * * The default syntax matches what's written to the bad-sector
+ * output files. It accepts `START END [# COMMENT]'.
+ */
{
- return ((tv_hi->tv_sec - tv_lo->tv_sec) +
- (tv_hi->tv_usec - tv_lo->tv_usec)/1.0e6);
+ char *q;
+ int err, rc;
+ unsigned long start, end;
+
+ /* Save any existing error code. */
+ err = errno;
+
+ /* Parse the start address. */
+ if (ISDIGIT(*p)) {
+ /* We found a digit: this is a good start. Convert the integer, check
+ * that it's in range, save it.
+ */
+
+ start = strtoul(p, &q, 0);
+ if (errno || start >= SECLIMIT) { rc = -1; goto end; }
+ *start_out = start; p = q;
+ } else if (!(f&PRF_HYPHEN)) {
+ /* No digit. We're parsing the map-file syntax, so this is an error. */
+
+ rc = -1; goto end;
+ } else {
+ /* We're parsing the command-line syntax, so this is OK. Set our
+ * internal idea of the position for the range check later, but don't
+ * alter the caller's variables.
+ */
+
+ start = 0;
+ }
+
+ /* Parse the delimiter. */
+ if (f&PRF_HYPHEN) {
+ if (*p != '-') { rc = -1; goto end; }
+ p++;
+ } else {
+ if (!ISSPACE(*p)) { rc = -1; goto end; }
+ do p++; while (ISSPACE(*p));
+ }
+
+ /* Parse the end address. */
+ if (ISDIGIT(*p)) {
+ /* We found a digit. Parse the integer and check that it's strictly
+ * larger than the start address.
+ */
+
+ end = strtoul(p, &q, 0);
+ if (errno || end > SECLIMIT || end < start) { rc = -1; goto end; }
+ *end_out = end; p = q;
+ } else if (!(f&PRF_HYPHEN)) {
+ /* No digit. We're parsing the file syntax, so this is an error. */
+
+ rc = -1; goto end;
+ }
+
+ /* In the file syntax, we're now allowed whitespace, so skip past that. */
+ if (!(f&PRF_HYPHEN)) while (ISSPACE(*p)) p++;
+
+ /* Check that there's nothing else. The file syntax allows a trailing
+ * comment here, but the command-line syntax doesn't.
+ */
+ if (*p && ((f&PRF_HYPHEN) || *p != '#')) { rc = -1; goto end; }
+
+ /* All done! */
+ rc = 0;
+end:
+ errno = err;
+ return (rc);
}
-#define DEFVEC(vtype, etype) \
- typedef struct { etype *v; size_t n, sz; } vtype
-#define VEC_INIT { 0, 0, 0 }
-#define VEC_FREE(vv) do { \
- free((vv)->v); (vv)->v 0; (vv)->n = (vv)->sz = 0; \
-} while (0)
-#define VEC_PUSH(p, vv) do { \
- size_t _want; \
- if ((vv)->n >= (vv)->sz) { \
- (vv)->sz = (vv)->sz ? 2*(vv)->sz : 32; \
- _want = (vv)->sz*sizeof(*(vv)->v); \
- (vv)->v = realloc((vv)->v, _want); \
- if (!(vv)->v) bail("out of memory allocating %zu bytes", _want); \
- } \
- (p) = &(vv)->v[(vv)->n++]; \
-} while (0)
+/*----- A few words about the overall approach ----------------------------*
+ *
+ * The objective is to produce a working copy of the input (commercial,
+ * pressed) DVD disc, only with all of the scrambled video data unscrambled
+ * so that it can be read without the need for cracking CSS keys, which, in
+ * the absence of a cooperative drive with access to the key tables in the
+ * disc lead-in data -- which we /don't/ copy -- is often slow and prone to
+ * failure. Producing a sector-by-sector image preserves all of the menus
+ * and special features, and also any other bonus data stored in the
+ * filesystem for use by computers, such as PDF scripts. DVD images are
+ * large because DVD video is inefficiently compressed by modern standards,
+ * but disk space is cheap and the tradeoff seems worthwhile to me.
+ *
+ * The approach is, in essence, simple: start at the beginning of the disc,
+ * reading sectors into a buffer and writing them to the output file, and
+ * continue until we reach the end. But we must cope with scrambled video
+ * files. Fortunately, `libdvdread' knows how to deal with these, and will
+ * tell us where they are on the disc.
+ *
+ * Given this information, we build a table of `events', with the sector
+ * numbers at which they occur. An `event' might be something like `such-
+ * and-such a video file began' or `such-and-such a file ended'. Chunks of
+ * disc between events can be read using the same strategy -- either reading
+ * unscrambled sectors directly from the block device, or decrypting
+ * scrambled sectors through `libdvdread' -- while at sector boundaries we
+ * might need to change strategy.
+ *
+ * Note that files can /overlap/. The DVD spec says that this can't happen,
+ * and that the data for video titles is laid out with higher-numbered
+ * titlesets occupying higher-numbered sectors, but it does anyway. I think
+ * this is intended to frustrate copiers like `dvdbackup' which try to copy
+ * the DVD files into a directory on the filesystem. The result is that they
+ * copy the same sectors into multiple, very large files, and turn an 8 GB
+ * DVD image into a 60 GB directory. (The reused regions often also contain
+ * intentionally bad sectors, so you have to wait for the drive to fail the
+ * same sectors over and over again. This is no fun.) As far as I know,
+ * files are either disjoint or coincident, but more complex arrangements are
+ * possible in principle. Also, I guess it's possible that the same sector
+ * should be decrypted with different keys depending on which titleset we're
+ * considering it being part of, but (a) DVD CSS keys aren't long enough to
+ * do this very well, and (b) I'm not aware of this actually being a thing.
+ * (Indeed, `libdvdcss' indexes keys by start sector, so such a disc probably
+ * wouldn't play back properly through VLC or `mpv'.)
+ *
+ * There's an additional consideration. We want to be able to fill in an
+ * ouptut image file incrementally, in several runs. A run can be
+ * interrupted for lots of reasons (e.g., a faster drive might have become
+ * available; it might be beneficial to switch to a more forgiving drive; it
+ * might be necessary to stop and clean the disc; the output filesystem might
+ * have become full; ...). And discs don't always read perfectly: some discs
+ * are damaged and have areas which can't be read; some discs (I'm looking at
+ * you, Sony, Disney, Lionsgate, and E-One) have intentional bad sectors,
+ * presumably specifically to make my life annoying. So we have other events
+ * which say things like `start writing stuff to the output' or `stop writing
+ * things to the output'. And we have a rather elaborate algorithm for
+ * trying to skip past a region of bad blocks, because drives get /really/
+ * slow when reading bad sectors.
+ */
+
+/*----- The file and event tables -----------------------------------------*/
#define MAXFILES (1 + 2*99 + 1)
+ /* How many (interesting) files there can be. This counts the
+ * magical `raw' file which refers to direct disc access, the master
+ * menu file, and 99 possible menu and titleset pairs. (A titleset
+ * can be split into 9 parts in order to keep each file below a
+ * gigabyte in size, but the rules require that the parts together
+ * form a single contiguous chunk on the disc, in the right order, so
+ * we treat them as a single file. We check this in `put_title'
+ * below, just in case some disc somewhere tries to be awkward, but I
+ * don't have a disc like that in my collection, and I doubt it would
+ * work very well.)
+ */
+
struct file {
- ident id;
- secaddr start, end;
+ /* An interesting DVD file. It has a name, encoded as an `ident'
+ * (see `lib.h'), and start and end sectors. (The `end' here, as
+ * everywhere in this code, is /exclusive/, so that the file's length
+ * is simply end - start.)
+ */
+
+ ident id; /* file name */
+ secaddr start, end; /* start (inclusive) and end
+ * (exclusive) sector numbers */
+};
+DEFVEC(file_v, struct file); /* a vector of files */
+static file_v filetab = VEC_INIT; /* the file table */
+
+enum {
+ /* Event codes. The ordering of these is important, because we use
+ * them to tie-break comparisons of events happening at the same
+ * sector when we sort the event queue.
+ */
+
+ EV_STOP, /* stop copying stuff to output */
+ EV_BEGIN, /* a (maybe scrambled) file begins */
+ EV_END, /* a file ends */
+ EV_WRITE /* start copying stuff to output */
};
-DEFVEC(file_v, struct file);
-static file_v filetab = VEC_INIT;
-enum { EV_STOP, EV_BEGIN, EV_END, EV_WRITE };
struct event {
- unsigned char ev, file;
- secaddr pos;
+ /* An event. */
+
+ unsigned char ev; /* event code (`EV_...') */
+ unsigned char file; /* the file (`EV_BEGIN', `EV_END');
+ * index into `filetab' */
+ secaddr pos; /* the sector at which it happens */
};
-DEFVEC(event_v, struct event);
-static event_v eventq = VEC_INIT;
+DEFVEC(event_v, struct event); /* a vector of events */
+static event_v eventq = VEC_INIT; /* the event queue */
static int compare_event(const void *a, const void *b)
+ /* A `qsort' comparison function for events. Event A sorts earlier
+ * than event B iff A's sector number is smaller than B's, or A's
+ * event code is less than B's.
+ */
{
const struct event *eva = a, *evb = b;
+ /* Primary ordering by position. */
if (eva->pos < evb->pos) return (-1);
else if (eva->pos > evb->pos) return (+1);
+ /* Secondary ordering by event code. */
if (eva->ev < evb->ev) return (-1);
else if (eva->ev > evb->ev) return (+1);
+ /* We currently have a final tie-break on file numbers so that the ordering
+ * is deterministic, but this is an arbitrary choice that shouldn't be
+ * relied upon.
+ */
if (eva->file < evb->file) return (-1);
else if (eva->file > evb->file) return (+1);
+ /* These events are equal. */
return (0);
}
+#ifdef DEBUG
+static void dump_eventq(const char *what)
+ /* Dump the event queue, labelling the output with WHAT. */
+{
+ unsigned i;
+ const struct event *ev;
+ char fn[MAXFNSZ];
+
+ printf("\n;; event dump (%s):\n", what);
+ for (i = 0; i < eventq.n; i++) {
+ ev = &eventq.v[i];
+ switch (ev->ev) {
+ case EV_BEGIN:
+ store_filename(fn, filetab.v[ev->file].id);
+ printf(";; %8"PRIuSEC": begin %s\n", ev->pos, fn);
+ break;
+ case EV_END:
+ store_filename(fn, filetab.v[ev->file].id);
+ printf(";; %8"PRIuSEC": end %s\n", ev->pos, fn);
+ break;
+ case EV_WRITE:
+ printf(";; %8"PRIuSEC": write\n", ev->pos);
+ break;
+ case EV_STOP:
+ printf(";; %8"PRIuSEC": stop\n", ev->pos);
+ break;
+ default:
+ printf(";; %8"PRIuSEC": ?%u\n", ev->pos, ev->ev);
+ break;
+ }
+ }
+}
+#endif
+
typedef uint_least32_t bits;
static bits live[(MAXFILES + 31)/32];
+ /* A bitmap which keeps track of which files are currently `active',
+ * i.e., that contain the sector we're currently thinking about. We
+ * set and clear these bits as we encounter `EV_BEGIN' and `EV_END'
+ * events.
+ */
static inline int livep(unsigned i)
+ /* Return whether file I is active. */
{ return (live[i/32]&((bits)1 << (i%32))); }
+
static inline void set_live(unsigned i)
+ /* Note that we've seen the start of file I. */
{ live[i/32] |= (bits)1 << (i%32); }
+
static inline void clear_live(unsigned i)
+ /* Note that we've seen the end of file I. */
{ live[i/32] &= ~((bits)1 << (i%32)); }
+
static inline int least_live(void)
+ /* Return the smallest index for any active file. This is going to
+ * be the file that we ask `libdvdread' to unscramble for us. This
+ * is important: the imaginary `raw' file that represents the entire
+ * block device has the highest index, and we want any actual video
+ * file to be used in preference so that we unscramble the data.
+ */
{
unsigned i, n = (filetab.n + 32)/32;
bits b;
+ /* First part: find the first nonzero word in the `live' table. */
for (i = 0; i < n; i++) { b = live[i]; if (b) goto found; }
return (-1);
+
found:
+ /* Second part: identify which bit in this word is nonzero. First, see if
+ * the bottom 16 bits are clear: if so, shift down and add 16 to the
+ * total. Now we know that the first set bit is indeed in the low 16
+ * bits, so see whether the low 8 bits are clear, and so on.
+ */
i *= 32;
if (!(b&0x0000ffff)) { b >>= 16; i += 16; }
if (!(b&0x000000ff)) { b >>= 8; i += 8; }
if (!(b&0x00000003)) { b >>= 2; i += 2; }
if (!(b&0x00000001)) { b >>= 1; i += 1; }
assert(b&1);
+
+ /* Done. */
return (i);
}
static void put_event(unsigned evtype, unsigned file, secaddr pos)
+ /* Add an event to the queue, with type EVTYPE, for the given FILE,
+ * and at sector POS. You can add events in any order because we'll
+ * sort them later. For `EV_WRITE' and `EV_STOP' events, the FILE
+ * doesn't matter: use zero for concreteness.
+ */
{
struct event *ev;
}
static void put_file(ident id, secaddr start, secaddr end)
+ /* Add a (VOB) file to the file table and event queue, with ident ID,
+ * starting at sector START and ending just before sector END.
+ */
{
struct file *f;
size_t i;
}
static void put_menu(dvd_reader_t *dvd, unsigned title)
+ /* Add the menu file for the given TITLE number to the file table and
+ * event queue; use the reader DVD to find out which sectors it
+ * occupies, if it even exists.
+ */
{
ident id = mkident(VOB, title, 0);
char fn[MAXFNSZ];
secaddr start, len;
+ /* Find out where the file is. */
store_filename(fn, id);
start = UDFFindFile(dvd, fn, &len); if (!start) return;
+
#ifdef DEBUG
- printf(";; %8"PRIuSEC" .. %-8"PRIuSEC": %s\n",
- start, start + SECTORS(len), fn);
+ /* Print out what we've discovered. */
+ printf(";; %8"PRIuSEC" .. %-8"PRIuSEC": %s\n",
+ start, start + SECTORS(len), fn);
#endif
+
+ /* Register the file and boundary events. */
put_file(id, start, start + SECTORS(len));
}
static void put_title(dvd_reader_t *dvd, unsigned title)
+ /* Add the titleset file for the given TITLE number to the file table
+ * and event queue; use the reader DVD to find out which sectors it
+ * occupies, if it even exists.
+ */
{
char fn[MAXFNSZ];
secaddr start[9], len[9];
unsigned i, npart;
+ /* First step: find out where all of the parts of the titleset are. I'm
+ * assuming that there aren't gaps in the numbering.
+ */
for (i = 0; i < 9; i++) {
store_filename(fn, mkident(VOB, title, i + 1));
start[i] = UDFFindFile(dvd, fn, &len[i]); if (!start[i]) break;
npart = i; if (!npart) return;
#ifdef DEBUG
+ /* Print out what we've discovered. */
for (i = 0; i < npart; i++) {
store_filename(fn, mkident(VOB, title, i + 1));
printf(";; %8"PRIuSEC" .. %-8"PRIuSEC": %s\n",
}
#endif
+ /* Second step: check that the parts all butt up against each other in the
+ * correct order. For this to work, the lengths, which are expressed in
+ * /bytes/ by `UDFFindFile', of all but the last part must be a whole
+ * number of sectors.
+ */
if (npart > 1)
for (i = 0; i < npart - 1; i++) {
if (len[i]%SECTORSZ)
title, i, start[i] + len[i]/SECTORSZ, i + 1, start[i + 1]);
}
+ /* All good: register a single file and its boundary events. */
put_file(mkident(VOB, title, 1),
start[0], start[npart - 1] + SECTORS(len[npart - 1]));
}
-static secaddr last_pos, limit, nsectors, ndone;
-static struct timeval last_time;
-static double wsum, wcount;
-static struct file *file;
-static secaddr bad_start;
-static unsigned retry, max_retries = 4;
-static int bad_err;
+/*----- Moving average machinery ------------------------------------------*
+ *
+ * We're using an exponential moving average with a weighting factor of α
+ * (`alpha', above); larger values are more sensitive to recent changes. If
+ * the old average was v_1, and the measurement in the current interval is x,
+ * then the new average after this interval is
+ *
+ * v = α x + (1 − α) v_1 .
+ *
+ * Write β = 1 − α; so
+ *
+ * v = α x + β v_1 .
+ *
+ * Let x_0 = x, let x_1 be the measurement from the previous interval, and,
+ * in general, let x_i be the measurement from i intervals ago. Then another
+ * way to write the above would be
+ *
+ * v = α (x_0 + β x_1 + ⋯ + β^i x_i + ⋯) .
+ *
+ * Alas, our time intervals are not regular. Suppose that we get our next
+ * measurement after a gap of t intervals, for some integer t. We can
+ * compensate approximately by pretending that all of the missed intervals --
+ * and our new one -- had the same mean rate. Then we'd have calculated
+ *
+ * v = α (x + β x + ⋯ + β^{t−1} x) + β^t v_1
+ *
+ * 1 − β^t
+ * = α x ------- + β^t v_1
+ * 1 − β
+ *
+ * = x (1 − β^t) + β^t v_1 (since α = 1 − β)
+ *
+ * = x + β^t (v_1 − x) .
+ *
+ * Does this work in general? It's clearly correct in the case t = 1.
+ *
+ * Suppose the old average was v_2, and that over a period of t intervals
+ * (where t is not necessarily an integer) we measured a mean rate of x, and
+ * then after u intervals we measured a mean rate of x /again/. Then we'd
+ * firstly determine
+ *
+ * v_1 = x + β^t (v_2 − x)
+ *
+ * and then
+ *
+ * v = x + β^u (v_1 − x)
+ *
+ * = x + β^u (x + β^t (v_2 − x) − x)
+ *
+ * = x + β^{t+u} (v_2 − x) ,
+ *
+ * which is exactly what we'd have done if we'd calculated the same mean rate
+ * over the combined span of t + u intervals.
+ *
+ * One final wrinkle, in case that wasn't enough. There's a problem with the
+ * initial setup of an exponential moving average. Apparently
+ * (https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average)
+ * explains that we can do this better by calculating the average after k
+ * intervals as
+ *
+ * x_0 + β x_1 + β^2 x_2 + ⋯ + β^{k−1} x_{k−1}
+ * v′ = ------------------------------------------- .
+ * 1 + β + β^2 + ⋯ + β^{k−1}
+ *
+ * The numerator is our existing v/α; the denominator is (1 − β^k)/α; the
+ * factors of α cancel, and we find that v′ = v/(1 − β^k). This still holds
+ * in our situation, where k may not be an integer.
+ *
+ * To apply all of this:
+ *
+ * * we maintain the moving average v in `avg';
+ *
+ * * we maintain the total β^k in `corr'; and
+ *
+ * * we compute v′ = v/(1 − β^k) on demand up in `render_perfstats'.
+ */
+
+struct avg {
+ double avg, corr;
+};
+#define AVG_INIT { 0.0, 1.0 }
+
+static double alpha = 0.1; /* weighting factor for average */
+
+static void update_avg(struct avg *a, double t, double n)
+{
+ double rate = n/t, beta_t = pow(1 - alpha, t);
+
+ a->avg = rate + beta_t*(a->avg - rate);
+ a->corr *= beta_t;
+}
+
+static inline double current_avg(const struct avg *a)
+ { return (a->avg/(1 - a->corr)); }
+
+/*----- The nonlinear progress model --------------------------------------*/
+
+/* The recorded portion of a single-layer DVD (i.e., DVD-5) can hold 2298496
+ * sectors of user data. This is preceded by 0x30000 = 196608 sectors of
+ * lead-in information, for a totoal of 2495104 sectors.
+ *
+ * The readable portion of a disc is an annulus with respective internal and
+ * external diameters of 44 mm and 117 mm. This annulus has an area of
+ * 9230.8 mm^2, so DVD has a storage density of about 270.3 sectors/mm^2. If
+ * the interior of the annulus were used for data storage rather than leaving
+ * a hole for a spindle and a clamping area, then it would be 10751 mm^2 and
+ * could store 2906107 sectors. (That means that the portion of the disc
+ * that's actually used to make it spin could have stored an additional
+ * 411003 sectors.)
+ *
+ * Sectors aren't stored on the surface willy-nilly, but arranged into a
+ * single archimedean spiral; bits are stored along this spiral at a
+ * more-or-less constant pitch. We are therefore led into an investigation
+ * of the arc-length of archimedean spirals.
+ *
+ * It's best to start with the polar equation of the spiral, which is simply
+ *
+ * r = k θ
+ *
+ * for a given constant k. The arc length of a curve expressed using polar
+ * coordinates is given by
+ *
+ * s = ∫ √(r^2 + (dr/dθ)^2) dθ
+ *
+ * = ∫ √(k^2 θ^2 + k^2) dθ
+ *
+ * = k ∫ √(1 + θ^2) dθ
+ *
+ * k
+ * = - [ θ √(1 + θ^2) + log(θ + √(1 + θ^2)) ] - s_0.
+ * 2
+ *
+ * We're assuming that the sectors are spaced out at a constant linear
+ * density along the spiral. We don't know the units for s, but there's some
+ * constant L such that A = s/L; so
+ *
+ * k
+ * A = --- [ θ √(1 + θ^2) + log(θ + √(1 + θ^2)) ] - A_0
+ * 2 L
+ *
+ * for some suitable constant A_0.
+ *
+ * Finally, we're assuming that the disc is spinning with some approximately
+ * constant angular velocity ω, so θ = ω t, giving
+ *
+ * k
+ * A = --- [ ω t √(1 + ω^2 t^2) + log(ω + √(1 + ω^2 t^2)) ] + A_0 .
+ * 2 L
+ *
+ * We can calculate approximate values for k/(2 L) and A_0. As stated above,
+ * the track pitch is about 0.75 µm; our inside and outside diameters of
+ * 44 mm and 117 mm correspond to angles of 184306 and 490088 radians
+ * respectively. Feeding those into the above equation for s gives arc
+ * lengths of 16984492558 and 120093346360 respectively, in unknown units.
+ * The difference is 103108853802, which should correspond to 2495104
+ * sectors, giving us 41324 arc-length units per sector. As a cross-check,
+ * the arc length corresponding to the inside diameter yields 411003 sectors,
+ * which is the same as we calculated above. This will be our A_0
+ */
+
+#ifdef unusef
+static double archimedes_arclen(double t)
+ /* Given an angle T, return the arc length of the canonical
+ * archimedean spiral r = θ, from θ = 0 up to θ = T.
+ */
+{
+ double u;
+
+ u = sqrt(1 + t*t);
+ return (t*u + log(t + u))/2;
+}
+#endif
+
+static double inv_archimedes_arclen(double s)
+ /* Given an arc length S, return the angle T such that the arc length
+ * of the canonical archimedean spiral r = θ, from θ = 0 up to θ = T,
+ * is equal to S.
+ */
+{
+ /* There is no closed-form solution, so we're going to invert the arc-
+ * length formula above numerically, using the Newton--Raphson method.
+ *
+ * Given an incorrect guess x_0 of a zero of some function f, we refine the
+ * guess by approximating f by its tangent at the point (x_0, f(x_0)).
+ * This will be a line with an equation like
+ *
+ * y = f'(x_0) x + c .
+ *
+ * We know that y = f(x_0) when x = x_0, so we can calculate
+ *
+ * c = f(x_0) - f'(x_0) x_0 .
+ *
+ * This will be zero when
+ *
+ * y = f'(x_0) x + f(x_0) - f'(x_0) x_0 = 0
+ *
+ * hwnce
+ *
+ * f'(x_0) x_0) - f(x_0) f(x_0)
+ * x = --------------------- = x_0 - ------- .
+ * f'(x_0) f'(x_0)
+ */
+
+ double t, ss, u, e;
+
+ /* We need to choose an initial estimate. This seems to work well in
+ * practice.
+ */
+ t = 1.5*sqrt(s);
+
+ for (;;) {
+ /* Compute s' = f(t). We open-code this calculation because the
+ * intermediate value √(1 + t^2) is also the gradient.
+ */
+ u = sqrt(1 + t*t);
+ ss = (t*u + log(t + u))/2;
+
+ /* Determine the error in f(t). We don't actually need much precision
+ * here, but 2 ulp seems achievable in practice with minimal cost: the
+ * usually sequence converges after only five iterations.
+ */
+ e = fabs(s/ss - 1);
+ if (e <= 2*DBL_EPSILON) return (t);
+
+ /* Not good enough. Refine the guess and go around again. */
+ t -= (ss - s)/u;
+ }
+}
+
+static double sectors_to_angle(secaddr base, secaddr low, secaddr high)
+ /* Return the angle, in radians, subtended by the range LOW up to
+ * HIGH of user sector addresses, given the physical sector address
+ * BASE of the first user-data sectors.
+ */
+{
+#define A0 411003.262489
+#define K 41324.4713654
+
+ return (inv_archimedes_arclen(K*(A0 + base + high)) -
+ inv_archimedes_arclen(K*(A0 + base + low)));
+
+#undef A0
+#undef K
+}
+
+enum {
+ FLAT, /* not actually a real DVD */
+ SINGLE, /* disc with only one layer */
+ PTP, /* two layers, parallel track path */
+ OTP /* two layers, opposite track path */
+};
+
+struct geometry {
+ unsigned shape; /* one of the four codes above */
+ secaddr start0, start1; /* initial physical sector */
+ secaddr midpoint; /* sector address of layer switch */
+};
+
+#define GF_BLKDEV 1u
+static void setup_geometry(struct geometry *g, int fd, unsigned f,
+ secaddr sz)
+ /* Initialize G with information about the disc structure. FD is a
+ * file descriptor for the device; SZ is the size of the disc in
+ * sectors. If `GF_BLKDEV' is clear in F then assume that FD refers
+ * to a regular file; G is populated with a `FLAT' performance model.
+ * If `GF_BLKDEV' is set, then FD refers to a block device, so try to
+ * retreive detailed structure information from the drive.
+ */
+{
+#ifdef __linux__
+ dvd_struct ds;
+ const struct dvd_layer *ly;
+#endif
+ secaddr t;
+
+#define LAYER_LIMIT 2298496 /* maximum (user) sectors on layer */
+#define DVDROM_OFFSET 0x30000 /* usual initial physical sector */
+
+ if (!(f&GF_BLKDEV)) {
+ /* We're reading from a regular file. Assume that progress will be
+ * linear.
+ */
+
+ g->shape = FLAT;
+ g->midpoint = SECLIMIT;
+ return;
+ }
+
+#ifdef __linux__
+ /* We have Linux and its DVD ioctl(2) calls. Interrogate the disc to
+ * discover its structure.
+ */
+
+ ds.type = DVD_STRUCT_PHYSICAL;
+ ds.physical.layer_num = 0;
+ if (ioctl(fd, DVD_READ_STRUCT, &ds)) {
+ moan_syserr(errno, "failed to read physical disc structure");
+ goto guess_structure;
+ }
+ ly = &ds.physical.layer[0];
+ switch (ly->nlayers) {
+ case 0:
+ g->shape = SINGLE;
+ g->start0 = g->start1 = 0;
+ g->midpoint = SECLIMIT;
+ break;
+ case 1:
+ g->start0 = ly->start_sector;
+ if (ly->track_path) {
+ g->shape = OTP;
+ g->start1 = 0;
+ g->midpoint = ly->end_sector_l0 - ly->start_sector + 1;
+ } else {
+ g->shape = PTP;
+ g->midpoint = ly->end_sector - ly->start_sector + 1;
+ ds.physical.layer_num = 1;
+ if (ioctl(fd, DVD_READ_STRUCT, &ds)) {
+ moan_syserr(errno, "failed to read layer 1 physical structure");
+ goto guess_structure;
+ }
+ g->start1 = ly->start_sector;
+ }
+ break;
+ default:
+ moan("unexpected layer count %d", ly->nlayers + 1);
+ goto guess_structure;
+ }
+ return;
+guess_structure:
+#endif
+
+ /* Either we don't have Linux, or we found something confusing. Let's try
+ * to guess at what's going on.
+ *
+ * If the volume size is small enough to fit on a single layer then assume
+ * that's what's happened; otherwise assume opposite track path with a cut
+ * at the midpoint, rounded up to an ECC block (16 sectors).
+ */
+ g->start0 = DVDROM_OFFSET; g->start1 = 0;
+ if (sz <= LAYER_LIMIT) {
+ g->shape = SINGLE;
+ g->midpoint = SECLIMIT;
+ } else {
+ g->shape = OTP;
+ t = (sz + DVDROM_OFFSET)/2;
+ t += 15; t &= -16;
+ t -= DVDROM_OFFSET;
+ g->midpoint = t;
+ }
+
+#undef LAYER_LIMIT
+#undef DVDROM_OFFSET
+}
+
+static double linear_progress(const struct geometry *g,
+ secaddr a0, secaddr a1)
+ /* Convert the sector range from A0 to A1 into a progress measurement
+ * which is, by intention, approximately linearly related to time,
+ * given a geometry description G.
+ */
+{
+ double theta = 0.0;
+
+ switch (g->shape) {
+ case FLAT:
+ theta = a1 - a0;
+ break;
+ case SINGLE:
+ theta = sectors_to_angle(g->start0, a0, a1);
+ break;
+ case PTP:
+ if (a0 < g->midpoint)
+ theta += sectors_to_angle(g->start0,
+ a0, a1 < g->midpoint ? a1 : g->midpoint);
+ if (a1 > g->midpoint)
+ theta += sectors_to_angle(g->start1,
+ a0 > g->midpoint ? a0 : g->midpoint, a1);
+ break;
+ case OTP:
+ if (a0 < g->midpoint)
+ theta += sectors_to_angle(g->start0,
+ a0, a1 < g->midpoint ? a1 : g->midpoint);
+ if (a1 > g->midpoint)
+ theta += sectors_to_angle(g->start0,
+ 2*g->midpoint - a1,
+ a0 > g->midpoint ?
+ 2*g->midpoint - a0 : g->midpoint);
+ break;
+ default:
+ abort();
+ }
+ return (theta);
+}
+
+/*----- Common variables used by the copying machinery --------------------*/
+
+/* General reading state. */
+static dvd_reader_t *dvd; /* `libdvdread' state for device */
+static int dvdfd = -1, outfd = -1; /* input device and output image */
+static struct file *file; /* currently active file */
+static dvd_file_t *vob; /* current `.VOB' file, or null */
+static const char *mapfile; static FILE *mapfp; /* skipped regions map */
+static const char *errfile; static FILE *errfp; /* bad-sector log */
+static secaddr limit; /* upper bound on sectors */
+
+static secaddr bad_start; /* start of current bad region */
+static unsigned retry, max_retries = 4; /* retry state */
+
+/*----- Progress reporting ------------------------------------------------*/
-static const char throbber[] = "|<-<|>->";
-static unsigned throbix = 0;
+static secaddr nsectors, ndone; /* number of sectors done/to do */
+static double total_linear, done_linear; /* linear progress tracking */
+static secaddr last_pos; /* position last time we updated */
+static struct timeval last_time; /* time last time we updated */
+static struct geometry geom; /* disc geometry for progress */
+static struct avg avg_rate = AVG_INIT, avg_linear = AVG_INIT;
+static int bad_err; /* most recent error code */
+static FILE *progressfp; /* file on which to trace progress data */
+
+static const char throbber[] = "|<-<|>->"; /* throbber pattern */
+static unsigned throbix = 0; /* current throbber index */
+
+static struct progress_item /* stock progress items */
+ copy_progress, disc_progress,
+ file_progress, badblock_progress;
static double scale_bytes(double n, const char **unit_out)
+ /* Determine a human-readable representation for N bytes. Divide N
+ * by some power of 1024, and store in *UNIT_OUT a string
+ * representing the conventional unit-prefix for that power of 1024.
+ */
{
const char *unit = "";
if (n > 1600) { n /= 1024; unit = "M"; }
if (n > 1600) { n /= 1024; unit = "G"; }
if (n > 1600) { n /= 1024; unit = "T"; }
-
*unit_out = unit; return (n);
}
-static struct progress_item
- copy_progress, disc_progress,
- file_progress, badblock_progress;
-
-#define TIMESTRMAX 16
+#define TIMESTRMAX 16 /* maximum length of a duration string */
static char *fmttime(unsigned long t, char *buf)
+ /* Format a count T of seconds. Write a suitable string to BUF,
+ * which will be no longer than `TIMESTRMAX' bytes including the
+ * terminating zero. Return BUF.
+ */
{
if (t < 60) sprintf(buf, "%ld s", t);
else if (t < 3600) sprintf(buf, "%ld:%02ld", t/60, t%60);
}
static void render_perfstats(struct progress_render_state *render)
+ /* Add performance statistics to RENDER.
+ *
+ * Specifically: the average transfer rate, and the estimated time to
+ * completion. (See `update_progress' for how the average
+ * computation works.)
+ */
{
int eta;
char timebuf[TIMESTRMAX];
- double rate;
+ double rate, linrate;
const char *unit;
- if (!wsum || !wcount) { rate = 0; eta = -1; }
- else { rate = wsum/wcount; eta = (int)((nsectors - ndone)/rate + 0.5); }
+ /* If there's no average computed yet, then use some placeholder values. */
+ rate = current_avg(&avg_rate);
+ linrate = current_avg(&avg_linear);
+ eta = (int)((total_linear - done_linear)/linrate + 0.5);
+ /* Write out the statistics. */
rate = scale_bytes(rate*SECTORSZ, &unit);
- progress_putright(render, "ETA %s ", rate ? fmttime(eta, timebuf) : "???");
+ progress_putright(render, "ETA %s ",
+ avg_linear.avg ? fmttime(eta, timebuf) : "???");
progress_putright(render, "%.1f %sB/s, ", rate, unit);
}
static void render_copy_progress(struct progress_item *item,
struct progress_render_state *render)
+ /* Render the progress for the copy, i.e., the number of sectors
+ * copied against the total number to be copied.
+ */
{
double frac = (double)ndone/nsectors;
static void render_disc_progress(struct progress_item *item,
struct progress_render_state *render)
+ /* Render the progress for the disc, i.e., the current position
+ * against the total number of sectors on the disc.
+ */
{
double frac = (double)last_pos/limit;
static void render_file_progress(struct progress_item *item,
struct progress_render_state *render)
+ /* Render the progress for the current file, i.e., the current
+ * position within the file against the file size.
+ */
{
secaddr off = last_pos - file->start, len = file->end - file->start;
char fn[MAXFNSZ];
static void render_badblock_progress(struct progress_item *item,
struct progress_render_state *render)
+ /* Render a notice about the progress through the current bad block
+ * region.
+ */
{
secaddr n = last_pos - bad_start;
int bg;
progress_shownotice(render, bg, 7);
}
-static double alpha = 0.1;
-
static void update_progress(secaddr pos)
+ /* Recompute the data displayed by the progress renderer functions
+ * above, based on the new current sector POS.
+ */
{
struct timeval now;
- double t, f, g;
-
- gettimeofday(&now, 0);
- t = tvdiff(&last_time, &now);
+ double t, delta_r;
-#define BETA (1 - alpha)
+ /* Find the current time and the delta since the last time we updated.
+ * This will be the length of the current interval.
+ */
+ gettimeofday(&now, 0); t = tvdiff(&last_time, &now);
+ /* If no time at all has passed (unlikely!) then skip the rate
+ * calculation. (The moving average wouldn't be affected anyway.)
+ */
if (t) {
- g = wcount ? pow(BETA, t) : 0.0; f = (1 - g)/(1 - BETA);
- wsum = f*(pos - last_pos)/t + g*wsum;
- wcount = f + g*wcount;
- ndone += pos - last_pos;
+ /* Update the moving average and the correction term, and start the next
+ * interval.
+ */
+
+ delta_r = linear_progress(&geom, last_pos, pos);
+ update_avg(&avg_rate, t, pos - last_pos);
+ update_avg(&avg_linear, t, delta_r);
+ ndone += pos - last_pos; done_linear += delta_r;
last_time = now; last_pos = pos;
}
-#undef BETA
+ /* Trace progress state if requested. */
+ if (progressfp) {
+ fprintf(progressfp, "%10ju.%06ld %"PRIuSEC" %f %f\n",
+ (uintmax_t)now.tv_sec, now.tv_usec,
+ ndone, done_linear,
+ (total_linear - done_linear)/current_avg(&avg_linear));
+ check_write(progressfp, "progress trace file");
+ }
+ /* Advance the throbber character. */
throbix++; if (!throbber[throbix]) throbix = 0;
}
static void report_progress(secaddr pos)
+ /* Update the progress variables (as `update_progress') and redraw
+ * the progress display.
+ */
{ update_progress(pos); progress_update(&progress); }
-static dvd_reader_t *dvd;
-static int dvdfd = -1, outfd = -1;
-static dvd_file_t *vob;
-static const char *mapfile; static FILE *mapfp;
-static const char *errfile; static FILE *errfp;
+/*----- Basic disc I/O ----------------------------------------------------*/
struct badblock { secaddr start, end; };
DEFVEC(badblock_v, struct badblock);
static badblock_v badblocks = VEC_INIT;
+ /* This is a list of /fake/ bad-block ranges, used to test the
+ * recovery algorithm. It's a rule that the ranges in this table
+ * mustn't overlap -- though it's OK if they abut.
+ */
static int compare_badblock(const void *a, const void *b)
+ /* A `qsort' comparison function for the fake bad-blocks list.
+ * Ranges which start earlier are sorted before rangers which start
+ * later.
+ */
{
const struct badblock *ba = a, *bb = b;
+ /* Order by start sector. */
if (ba->start < bb->start) return (-1);
else if (ba->start > bb->start) return (+1);
+ /* Order by end sector as a tiebreak. This shouldn't be possible. */
if (ba->end < bb->end) return (-1);
else if (ba->end > bb->end) return (+1);
+ /* They're equal. This shouldn't be possible either. */
return (0);
}
-static double bad_block_delay = 0.0;
-static double good_block_delay = 0.0;
+static double bad_block_delay = 0.0, good_block_delay = 0.0;
+ /* delay parameters for performance testing */
static ssize_t read_sectors(secaddr pos, void *buf, secaddr want)
+ /* Try to read WANT sectors from the input, starting with sector POS,
+ * and write the contents to BUF. Return the number of /whole
+ * sectors/ read; this will be 0 at end-of-file (though that
+ * shouldn't happen). The returned length will be smaller than WANT
+ * only if end-of-file or a system error prevents reading further.
+ * Returns -1 on a system error if that prevented us from reading
+ * anything at all.
+ *
+ * This function is where the fake bad-blocks list is handled.
+ */
{
ssize_t n, done;
size_t lo, mid, hi;
struct badblock *bad, *best;
unsigned char *p = buf;
+ /* See whether the requested range intersects a bad-blocks range. */
if (badblocks.n) {
+ /* Since the list is sorted, we use a binary search. We're looking for
+ * the earliest-starting range which /ends after/ POS. If this starts
+ * /at or before/ POS, then POS itself is a bad sector, and we should
+ * pretend an I/O error; otherwise, if the bad range /starts/ somewhere
+ * in the range we're trying to read then we must pretend a short read;
+ * and otherwise there's nothing to do.
+ */
+
+ /* Throughout, `best' points to the earliest-starting range we've found
+ * which (starts and) finishes after POS. Ranges with indices below LO
+ * end too early to be interesting; similarly, ranges with indices HI or
+ * above start later than POS. If we find a range which actually covers
+ * POS exactly then we'll stop early.
+ */
best = 0; lo = 0; hi = badblocks.n;
#ifdef DEBUG
progress_clear(&progress);
pos, pos + want);
#endif
while (lo < hi) {
+ /* Standard binary-search loop: we continue until the pointers
+ * converge.
+ */
+
+ /* Try the midpoint between the two bounds. */
mid = lo + (hi - lo)/2; bad = &badblocks.v[mid];
#ifdef DEBUG
printf(";; try %zu (%"PRIuSEC" .. %"PRIuSEC")... ",
mid, bad->start, bad->end);
#endif
+
+ /* Follow our invariant. If the range starts strictly after POS, then
+ * it's too late to overlap, so bring down HI to cover it; but it must
+ * be closer than any previous block we've found, so remember it in
+ * `best'. Similarly, if the range ends /at or before/ POS then it
+ * stops too early, so bring up LO to cover it (but otherwise forget
+ * about it because it can't affect what we're doing).
+ *
+ * If we get a match then we stop immediately and fake a bad block.
+ */
if (pos < bad->start) { D( printf("high\n"); ) best = bad; hi = mid; }
else if (pos >= bad->end) { D( printf("low\n"); ) lo = mid + 1; }
- else { D( printf("match!\n"); ) errno = EIO; return (-1); }
+ else {
+ D( printf("match!\n"); )
+ errno = EIO; sit(bad_block_delay); return (-1);
+ }
}
+
+ /* We're done. Check to see whether the bad range starts early enough.
+ * If so, remember that we're simulating an error, apply the delay, and
+ * bamboozle the rest of the code into performing a short read.
+ */
#ifdef DEBUG
if (best)
printf(";; next is %"PRIuSEC" .. %"PRIuSEC"\n",
if (best && pos + want > best->start)
{ want = best->start - pos; fakeerr = EIO; sit(bad_block_delay); }
}
- done = 0;
+
+ /* Try to read stuff into the buffer until we find a reason why we can't
+ * continue. Obviously we need to keep track of how much stuff we've read
+ * on previous iterations.
+ */
+ done = 0; errno = 0;
while (want) {
+
+ /* Read from the current file's input source. If that's a scrambled
+ * video file, then use `libdvdread'; if it's the `raw' file, then go to
+ * the block device; if it's nothing at all, then fill with zeros.
+ * Always force a seek to the right place, in case things got messed up
+ * by some previous error.
+ */
if (vob)
{ errno = 0; n = DVDReadBlocks(vob, pos - file->start, want, p); }
else if (file) {
n = want;
}
+ /* If we read some stuff then update the buffer pointer and lengths. If
+ * we hit end-of-file then stop. If we hit a bad sector then maybe make
+ * a note of it in the bad-sector log. On any other kind of error, just
+ * stop.
+ */
if (n > 0) { done += n; pos += n; p += n*SECTORSZ; want -= n; }
else if (!n) break;
else if (errno == EIO && errfile) {
break;
} else if (errno != EINTR) break;
}
+
+ /* We made it. If we saved up a fake error, and there wasn't a real error
+ * (which should obviously take priority) then present the fake error to
+ * the caller. If there wasn't an error, then everything must have been
+ * good so impose the good-block delay -- note that a bad-block delay will
+ * already have been imposed above. Finally, return the accumulated count
+ * of sectors successfully read, or report the end-of-file or error
+ * condition as applicable.
+ */
if (fakeerr && !errno) errno = fakeerr;
else if (done > 0 && good_block_delay) sit(done*good_block_delay);
return (!done && errno ? -1 : done);
}
-static void record_bad_sectors(secaddr bad_lo, secaddr bad_hi)
-{
- char fn[MAXFNSZ];
-
- if (!mapfile) return;
-
- open_file_on_demand(mapfile, &mapfp, "bad-sector region map");
- fprintf(mapfp, "%"PRIuSEC" %"PRIuSEC" # %"PRIuSEC" sectors",
- bad_lo, bad_hi, bad_hi - bad_lo);
-
- if (file && id_kind(file->id) != RAW) {
- store_filename(fn, file->id);
- fprintf(mapfp, "; `%s' %"PRIuSEC" .. %"PRIuSEC" of %"PRIuSEC"",
- fn, bad_lo - file->start, bad_hi - file->start,
- file->end - file->start);
- }
-
- fputc('\n', mapfp);
- check_write(mapfp, "bad-sector region map");
-}
-
-static void recovered(secaddr bad_lo, secaddr bad_hi)
-{
- char fn[MAXFNSZ];
-
- progress_clear(&progress);
-
- if (!file || id_kind(file->id) == RAW)
- moan("skipping %"PRIuSEC" bad sectors (%"PRIuSEC" .. %"PRIuSEC")",
- bad_hi - bad_lo, bad_lo, bad_hi);
- else {
- store_filename(fn, file->id);
- moan("skipping %"PRIuSEC" bad sectors (%"PRIuSEC" .. %"PRIuSEC"; "
- "`%s' %"PRIuSEC" .. %"PRIuSEC" of %"PRIuSEC")",
- bad_hi - bad_lo, bad_lo, bad_hi,
- fn, bad_lo - file->start, bad_hi - file->start,
- file->end - file->start);
- }
-
- record_bad_sectors(bad_lo, bad_hi);
-
- if (lseek(outfd, (off_t)(bad_hi - bad_lo)*SECTORSZ, SEEK_CUR) < 0)
- bail_syserr(errno, "failed to seek past bad sectors");
-
- progress_removeitem(&progress, &badblock_progress);
- progress_update(&progress);
-}
+/*----- Tracking machinery for the bad-sector algorithm -------------------*
+ *
+ * While we're probing around trying to find the end of the bad region, we'll
+ * have read some good data. We want to try to keep as much good data as we
+ * can, and avoid re-reading it because (a) it's pointless I/O work, but more
+ * importantly (b) it might not work the second time. The machinery here
+ * is for making this work properly.
+ *
+ * There are two parts to this which don't really intersect, but for
+ * convenience the tracking information for them is kept in the same
+ * `recoverybuf' structure.
+ *
+ * * The `short-range' machinery keeps track of a contiguous region of good
+ * data stored in the caller's buffer.
+ *
+ * * The `long-range' machinery keeps track of a contiguous region of good
+ * data that's beyond the range of the buffer.
+ */
struct recoverybuf {
- unsigned char *buf;
- secaddr sz, pos, start, end;
- secaddr good_lo, good_hi;
+ /* Information used to keep track of where good and bad sectors are
+ * while we're trying to find the end of a region of bad sectors.
+ */
+
+ /* Short-range buffer tracking. */
+ unsigned char *buf; /* pointer to the actual buffer */
+ secaddr sz; /* size of the buffer in sectors */
+ secaddr pos; /* sector address corresponding to
+ * the start of the buffer */
+ secaddr start, end; /* bounds of the live region within
+ * the buffer, as offsets in
+ * sectors from the buffer start */
+
+ /* Long-range tracking. */
+ secaddr good_lo, good_hi; /* known-good region, as absolute
+ * sector addresses */
};
static void rearrange_sectors(struct recoverybuf *r,
secaddr dest, secaddr src, secaddr len)
+ /* Shuffle data about in R's buffer. Specifically, move LEN sectors
+ * starting SRC sectors from the start of the buffer to a new
+ * position DEST sectors from the start.
+ *
+ * Unsurprisingly, this is a trivial wrapper around `memmove', with
+ * some range checking thrown in; it's only used by `recovery_read_-
+ * buffer' and `find_good_sector' below.
+ */
{
- assert(dest + len <= r->sz);
- assert(src + len <= r->sz);
+ assert(dest + len <= r->sz); assert(src + len <= r->sz);
memmove(r->buf + dest*SECTORSZ, r->buf + src*SECTORSZ, len*SECTORSZ);
}
static PRINTF_LIKE(2, 3)
void show_recovery_buffer_map(const struct recoverybuf *r,
const char *what, ...)
+ /* Dump a simple visualization of the short-range tracking state. */
{
va_list ap;
static ssize_t recovery_read_sectors(struct recoverybuf *r,
secaddr pos, secaddr off, secaddr want)
+ /* Try to read WANT sectors starting at sector address POS from the
+ * current file into R's buffer, at offset OFF sectors from the start
+ * of the buffer. Return the number of sectors read, zero if at end
+ * of file, or -1 in the event of a system error.
+ *
+ * This is a trivial wrapper around `read_sectors' with some
+ * additional range checking, used only by `recovery_read_buffer'
+ * below.
+ */
{
ssize_t n;
assert(off <= r->sz); assert(want <= r->sz - off);
+ assert(pos == r->pos + off);
n = read_sectors(pos, r->buf + off*SECTORSZ, want);
return (n);
}
static ssize_t recovery_read_buffer(struct recoverybuf *r,
secaddr pos, secaddr want)
+ /* Try to read WANT sectors, starting at sector address POS, from the
+ * current file into the buffer R, returning a count of the number of
+ * sectors read, or 0 if at end of file, or -1 in the case of a
+ * system error, as for `read_sectors'. The data will end up
+ * /somewhere/ in the buffer, but not necessarily at the start.
+ */
{
secaddr diff, pp, nn;
ssize_t n;
+ /* This is the main piece of the short-range tracking machinery. It's
+ * rather complicated, so hold on tight. (It's much simpler -- and less
+ * broken -- than earlier versions were, though.)
+ */
+
#ifdef DEBUG
progress_clear(&progress);
show_recovery_buffer_map(r, "begin(%"PRIuSEC", %"PRIuSEC")", pos, want);
#endif
+ /* The first order of business is to make space in the buffer for this new
+ * data. We therefore start with a case analysis.
+ */
if (pos < r->pos) {
+ /* The new position is before the current start of the buffer, so we have
+ * no choice but to decrease the buffer position, which will involve
+ * shifting the existing material upwards.
+ */
+
+ /* Determine how far up we'll need to shift. */
diff = r->pos - pos;
+
if (r->start + diff >= r->sz) {
+ /* The material that's currently in the buffer would be completely
+ * shifted off the end, so we have no choice but to discard it
+ * completely.
+ */
+
r->pos = pos; r->start = r->end = 0;
#ifdef DEBUG
show_recovery_buffer_map(r, "cleared; shift up by %"PRIuSEC"", diff);
#endif
} else {
+ /* Some of the material in the buffer will still be there. We might
+ * lose some stuff off the end: start by throwing that away, and then
+ * whatever's left can be moved easily.
+ */
+
if (r->end + diff > r->sz) r->end = r->sz - diff;
rearrange_sectors(r, r->start + diff, r->start, r->end - r->start);
r->pos -= diff; r->start += diff; r->end += diff;
#endif
}
} else if (pos > r->pos + r->end) {
- r->pos = pos; r->start = r->end = 0;
+ /* The new position is strictly beyond the old region. We /could/ maybe
+ * keep this material, but it turns out to be better not to. To keep it,
+ * we'd have to also read the stuff that's in between the end of the old
+ * region and the start of the new one, and that might contain bad
+ * sectors which the caller is specifically trying to skip. We just
+ * discard the entire region here so as not to subvert the caller's
+ * optimizations.
+ */
+
+ r->pos = pos; r->start = r->end = 0;
#ifdef DEBUG
- show_recovery_buffer_map(r, "cleared; beyond previous region");
+ show_recovery_buffer_map(r, "cleared; beyond previous region");
#endif
} else if (pos + want > r->pos + r->sz) {
+ /* The requested range of sectors extends beyond the region currently
+ * covered by the buffer. We must therefore increase the buffer position
+ * which will involve shifting the existing material downwards.
+ */
+
+ /* Determine how far down we'll need to shift. */
diff = (pos + want) - (r->pos + r->sz);
+
if (r->end <= diff) {
+ /* The material that's currently in the buffer would be completely
+ * shifted off the beginning, so we have no choice but to discard it
+ * completely.
+ */
+
r->pos = pos; r->start = r->end = 0;
#ifdef DEBUG
show_recovery_buffer_map(r, "cleared; shift down by %"PRIuSEC"", diff);
#endif
} else {
+ /* Some of the material in the buffer will still be there. We might
+ * lose some stuff off the beginning: start by throwing that away, and
+ * then whatever's left can be moved easily.
+ */
+
if (r->start < diff) r->start = diff;
rearrange_sectors(r, r->start - diff, r->start, r->end - r->start);
r->pos += diff; r->start -= diff; r->end -= diff;
}
}
+ /* We now have space in the buffer in which to put the new material.
+ * However, the buffer already contains some stuff. We may need to read
+ * some data from the input file into an area before the existing
+ * material, or into an area following the existing stuff, or both, or
+ * (possibly) neither.
+ */
+
if (pos < r->pos + r->start) {
+ /* The requested position is before the current good material, so we'll
+ * need to read some stuff there.
+ */
+
+ /* Determine the place in the buffer where this data will be placed, and
+ * how long it will need to be. Try to extend it all the way to the
+ * existing region even if this is more than the caller wants, because it
+ * will mean that we can join it onto the existing region rather than
+ * having to decide which of two disconnected parts to throw away.
+ */
pp = pos - r->pos; nn = r->start - pp;
+
+ /* Read the data. */
#ifdef DEBUG
printf(";; read low (%"PRIuSEC"@%"PRIuSEC", %"PRIuSEC")", pos, pp, nn);
fflush(stdout);
#ifdef DEBUG
printf(" -> %zd\n", n);
#endif
+
+ /* See whether it worked. */
if (n != nn) {
+ /* We didn't get everything we wanted. */
+
+ /* If we got more than the caller asked for then technically this is
+ * good; but there must be some problem lurking up ahead, and the
+ * caller will want to skip past that. So we don't update the tracking
+ * information to reflect our new data; even though this /looks/ like a
+ * success, it isn't really.
+ */
if (n >= 0 && n > want) n = want;
+
+ /* We're done. */
goto end;
}
+
+ /* Extend the region to include the new piece. */
r->start = pp;
#ifdef DEBUG
show_recovery_buffer_map(r, "joined new region");
}
if (pos + want > r->pos + r->end) {
+ /* The requested region extends beyond the current region, so we'll need
+ * to read some stuff there.
+ */
+
+ /* Determine the place in the buffer where this data will be placed, and
+ * how long it will need to be. Note that pos <= r->pos + r->end, so
+ * there won't be a gap between the old good region and the material
+ * we're trying to read.
+ */
pp = r->end; nn = (pos + want) - (r->pos + r->end);
+
+ /* Read the data. */
#ifdef DEBUG
printf(";; read high (%"PRIuSEC"@%"PRIuSEC", %"PRIuSEC")",
r->pos + pp, pp, nn);
#ifdef DEBUG
printf(" -> %zd\n", n);
#endif
+
+ /* See whether it worked. */
if (n > 0) {
+ /* We read something, so add it onto the existing region. */
+
r->end += n;
#ifdef DEBUG
show_recovery_buffer_map(r, "joined new region");
}
}
+ /* Work out the return value to pass back to the caller. The newly read
+ * material has been merged with the existing region (the case where we
+ * didn't manage to join the two together has been handled already), so we
+ * can easily work out how much stuff is available by looking at the
+ * tracking information. It only remains to bound the region size by the
+ * requested length.
+ */
n = r->pos + r->end - pos;
if (!n && want) n = -1;
else if (n > want) n = want;
end:
+ /* Done. */
#ifdef DEBUG
show_recovery_buffer_map(r, "done; return %zd", n);
#endif
static ssize_t recovery_read_multiple(struct recoverybuf *r,
secaddr pos, secaddr want)
+ /* Try to read WANT sectors, starting at sector address POS, from the
+ * current file, returning a count of the number of sectors read, or
+ * 0 if at end of file, or -1 in the case of a system error, as for
+ * `read_sectors'. Some data might end up in R's buffer, but if WANT
+ * is larger than R->sz then a lot will be just thrown away.
+ *
+ * This is only used by `recovery_read' below.
+ */
{
ssize_t n;
secaddr skip, want0 = want;
+ /* If the request is larger than the buffer, then we start at the /end/ and
+ * work backwards. If we encounter a bad sector while we're doing this,
+ * then we report a short read as far as the bad sector: the idea is to
+ * find the /latest/ bad sector we can. The caller will want to skip past
+ * the bad sector, so the fact that we implicitly lied about the earlier
+ * data as being `good' won't matter.
+ */
+
while (want > r->sz) {
+ /* There's (strictly!) more than a buffer's worth. Fill the buffer with
+ * stuff and reduce the requested size.
+ */
+
skip = want - r->sz;
n = recovery_read_buffer(r, pos + skip, r->sz);
+
+ /* If it failed, then we always return a positive result, because we're
+ * pretending we managed to read all of the (nonempty) preceding
+ * material.
+ */
if (n < r->sz) return (skip + (n >= 0 ? n : 0));
+
+ /* Cross off a buffer's worth and go around again. */
want -= r->sz;
}
+
+ /* Read the last piece. If it fails or comes up short, then we don't need
+ * to mess with the return code this time.
+ */
n = recovery_read_buffer(r, pos, want);
if (n < 0 || n < want) return (n);
+
+ /* It all worked. Return the full original amount requested. */
return (want0);
}
static ssize_t recovery_read(struct recoverybuf *r,
secaddr pos, secaddr want)
+ /* Try to read WANT sectors, starting at sector address POS, from the
+ * current file, returning a count of the number of
+ * sectors read, or 0 if at end of file, or -1 in the case of a
+ * system error, as for `read_sectors'. Some data might end up in
+ * R's buffer, but if WANT is larger than R->sz then a lot will be
+ * just thrown away.
+ */
{
- secaddr lo = pos, hi = pos + want, span;
+ secaddr lo = pos, hi = pos + want, span; /* calculate the request bounds */
ssize_t n;
+ /* This is the main piece of the long-range tracking machinery.
+ * Fortunately, it's much simpler than the short-range stuff that we've
+ * just dealt with.
+ */
+
if (hi < r->good_lo || lo > r->good_hi) {
+ /* The requested region doesn't abut or overlap with the existing good
+ * region, so it's no good to us. Just read the requested region; if it
+ * worked at all, then replace the current known-good region with the
+ * region that was successfully read.
+ */
+
n = recovery_read_multiple(r, lo, hi - lo);
if (n > 0) { r->good_lo = lo; r->good_hi = lo + n; }
return (n);
}
if (hi > r->good_hi) {
+ /* The requested region ends later than the current known-good region.
+ * Read the missing piece. We're doing this first so that we find later
+ * bad sectors.
+ */
+
span = hi - r->good_hi;
n = recovery_read_multiple(r, r->good_hi, span);
+
+ /* If we read anything at all, then extend the known-good region. */
if (n > 0) r->good_hi += n;
+
+ /* If we didn't read everything we wanted, then report this as a short
+ * read (so including some nonempty portion of the known-good region).
+ */
if (n < 0 || n < span) return (r->good_hi - lo);
}
if (lo < r->good_lo) {
+ /* The requested region begins earlier than the known-good region. */
+
span = r->good_lo - lo;
n = recovery_read_multiple(r, lo, span);
+
+ /* If we read everything we wanted, then extend the known-good region.
+ * Otherwise, we're better off keeping the stuff after the bad block.
+ */
if (n == span) r->good_lo = lo;
else return (n);
}
+ /* Everything read OK, and we've extended the known-good region to cover
+ * the requested region. So return an appropriate code by consulting the
+ * new known-good region.
+ */
n = r->good_hi - pos; if (n > want) n = want;
if (!n) { errno = EIO; n = -1; }
return (n);
}
-static double clear_factor = 1.5;
-static secaddr clear_min = 1, clear_max = SECLIMIT;
-static double step_factor = 2.0;
-static secaddr step_min = 1, step_max = 0;
-
-static secaddr run_length_wanted(secaddr pos, secaddr badlen, secaddr end)
-{
- secaddr want;
-
- want = clear_factor*badlen;
- if (want < clear_min) want = clear_min;
- if (want > end - pos) want = end - pos;
- if (clear_max && want > clear_max) want = clear_max;
- return (want);
-}
+/*----- Skipping past regions of bad sectors ------------------------------*/
-static void report_bad_blocks_progress(secaddr bad_hi, int err)
- { bad_err = err; report_progress(bad_hi); }
+static double clear_factor = 0.5; /* proportion of clear sectors needed */
+static secaddr clear_min = 1, clear_max = SECLIMIT; /* absolute bounds */
+static double step_factor = 2.0; /* factor for how far to look ahead */
+static secaddr step_min = 1, step_max = 0; /* and absolute bounds */
-static ssize_t find_good_sector(secaddr *pos_inout, secaddr end,
- unsigned char *buf, secaddr sz)
+static void recovered(secaddr bad_lo, secaddr bad_hi)
+ /* Do all of the things that are necessary when a region of bad
+ * sectors has been found between BAD_LO (inclusive) and BAD_HI
+ * (exclusive).
+ */
{
- secaddr pos = *pos_inout, bad_lo, bad_hi, good, step, want;
- struct recoverybuf r;
- ssize_t n;
+ char fn[MAXFNSZ];
- bad_start = pos; bad_err = errno;
+ /* Remove the progress display temporarily. */
+ progress_clear(&progress);
+
+ /* Print a message into the permanent output log. */
+ if (!file || id_kind(file->id) == RAW)
+ moan("skipping %"PRIuSEC" bad sectors (%"PRIuSEC" .. %"PRIuSEC")",
+ bad_hi - bad_lo, bad_lo, bad_hi);
+ else {
+ store_filename(fn, file->id);
+ moan("skipping %"PRIuSEC" bad sectors (%"PRIuSEC" .. %"PRIuSEC"; "
+ "`%s' %"PRIuSEC" .. %"PRIuSEC" of %"PRIuSEC")",
+ bad_hi - bad_lo, bad_lo, bad_hi,
+ fn, bad_lo - file->start, bad_hi - file->start,
+ file->end - file->start);
+ }
+
+ if (mapfile) {
+ /* The user requested a map of the skipped regions, so write an entry. */
+
+ /* Open the file, if it's not open already. */
+ open_file_on_demand(mapfile, &mapfp, "bad-sector region map");
+
+ /* Write the sector range. */
+ fprintf(mapfp, "%"PRIuSEC" %"PRIuSEC" # %"PRIuSEC" sectors",
+ bad_lo, bad_hi, bad_hi - bad_lo);
+
+ /* If we're currently reading from a file then note down the position in
+ * the file in the comment. (Intentional bad sectors are frequently at
+ * the start and end of titles, so this helps a reader to decide how
+ * concerned to be.)
+ */
+ if (file && id_kind(file->id) != RAW)
+ fprintf(mapfp, "; `%s' %"PRIuSEC" .. %"PRIuSEC" of %"PRIuSEC"",
+ fn, bad_lo - file->start, bad_hi - file->start,
+ file->end - file->start);
+
+ /* Done. Flush the output to the file so that we don't lose it if we
+ * crash!
+ */
+ fputc('\n', mapfp);
+ check_write(mapfp, "bad-sector region map");
+ }
+
+ /* Adjust the position in our output file to skip past the bad region.
+ * (This avoids overwriting anything that was there already, which is
+ * almost certainly less wrong than anything we could come up with here.)
+ */
+ if (lseek(outfd, (off_t)(bad_hi - bad_lo)*SECTORSZ, SEEK_CUR) < 0)
+ bail_syserr(errno, "failed to seek past bad sectors");
+
+ /* Remove our notice now that we're no longer messing about with bad
+ * sectors, and reinstate the progress display.
+ */
+ progress_removeitem(&progress, &badblock_progress);
+ progress_update(&progress);
+}
+
+static secaddr run_length_wanted(secaddr pos, secaddr badlen, secaddr end)
+ /* Return the number of good sectors that we want to see before
+ * we're happy, given that we're about to try to read sector POS,
+ * which is BADLEN sectors beyond where we found the first bad
+ * sector, and the current region ends at sector END (i.e., this is
+ * where the next event occurs).
+ */
+{
+ secaddr want;
+
+ /* Apply the factor to BADLEN to get an initial length. */
+ want = ceil(clear_factor*badlen);
+
+ /* Apply the user-configurable lower bound. */
+ if (want < clear_min) want = clear_min;
+
+ /* Cap this with the end of the region. */
+ if (want > end - pos) want = end - pos;
+
+ /* And apply the user-configurable upper bound. */
+ if (clear_max && want > clear_max) want = clear_max;
+
+ /* We're done. */
+ return (want);
+}
+
+static void report_bad_blocks_progress(secaddr bad_hi, int err)
+ /* Report progress while we're trying to work past a region of bad
+ * sectors. We're about to investigate BAD_HI, and the most recent
+ * error was ERR.
+ */
+ { bad_err = err; report_progress(bad_hi); }
+
+static ssize_t find_good_sector(secaddr *pos_inout, secaddr end,
+ unsigned char *buf, secaddr sz)
+ /* Work out a place to resume after finding a bad sector. The
+ * current position, where we found a problem, is in *POS_INOUT. The
+ * current input region goes up up sector END (i.e., this is where
+ * the next event occurs). The caller's buffer is at BUF, and can
+ * hold SZ sectors. On exit, update *POS_INOUT to be the start of a
+ * region of /good/ sector that we decided was worth exploring, and
+ * return the number of sectors we've already read at that position
+ * and left at the start of the buffer. (This number may be zero,
+ * depending on how things work out. That doesn't mean that we hit
+ * end-of-file.)
+ *
+ * Altough the return value is `ssize_t', this is only to fit in with
+ * other read functions; a negative return is not actually possible.
+ */
+{
+ secaddr pos = *pos_inout, bad_lo, bad_hi, good, step, want;
+ struct recoverybuf r;
+ ssize_t n;
+
+ /* Initial setup. Save the initial state and establish the bad-blocks
+ * progress notice.
+ */
+ bad_start = pos; bad_err = errno;
badblock_progress.render = render_badblock_progress;
progress_additem(&progress, &badblock_progress);
- r.buf = buf; r.sz = sz; r.pos = r.start = r.end = 0;
- r.good_lo = r.good_hi = 0;
-
+ /* First, retry the `bad' sector a few times. Sometimes, with damaged
+ * discs, this actually works. We'll try to read a full buffer, but we're
+ * not expecting much.
+ */
want = sz; if (want > end - pos) want = end - pos;
for (retry = 0; retry < max_retries; retry++) {
+
+ /* Show the progress report. */
report_bad_blocks_progress(pos, errno);
- n = recovery_read(&r, pos, want);
+
+ /* Try reading stuff. */
+ n = read_sectors(pos, buf, want);
#ifdef DEBUG
progress_clear(&progress);
printf(";; [retry] try reading %"PRIuSEC" .. %"PRIuSEC" -> %zd\n",
pos, pos + want, n);
#endif
+
if (n > 0) {
+ /* We won! Remove the progress display, and leave a permanent message
+ * to inform the user what happened.
+ */
progress_clear(&progress);
moan("sector %"PRIuSEC" read ok after retry", pos);
progress_removeitem(&progress, &badblock_progress);
}
}
+ /* We're going to have to be more creative. Set up the tracking state. */
+ r.buf = buf; r.sz = sz; r.pos = r.start = r.end = 0;
+ r.good_lo = r.good_hi = 0;
+
+ /* Set up the region bound. We know the bad area starts at POS, and that
+ * it covers at least one sector.
+ */
bad_lo = pos; bad_hi = pos + 1;
+
+ /* Second major step: try to find somewhere on the other side of the bad
+ * region.
+ */
for (;;) {
- report_bad_blocks_progress(bad_hi, errno);
#ifdef DEBUG
progress_clear(&progress);
printf(";; bounding bad-block region: "
"%"PRIuSEC" ..%"PRIuSEC".. %"PRIuSEC"\n",
bad_lo, bad_hi - bad_lo, bad_hi);
#endif
+
+ /* If our upper bound has reached all the way to the end of the input
+ * region then there's nowhere to recover to. Set the next position to
+ * the end of the region and return.
+ */
if (bad_hi >= end) {
progress_clear(&progress);
moan("giving up on this extent");
recovered(bad_lo, end); *pos_inout = end;
return (0);
}
+
+ /* Give a progress update. */
+ report_bad_blocks_progress(bad_hi, errno);
+
+ /* Choose a new place to look. Apply the step factor to the size of the
+ * current gap between the start and end of the bad region, and then
+ * bound by the user bounds and the input-region end.
+ *
+ * We make progress because `step' is at least 1: `step_min' is at least
+ * 1, and bad_hi < end or we'd have already bailed.
+ */
step = (step_factor - 1)*(bad_hi - bad_lo);
if (step < step_min) step = step_min;
if (step_max && step > step_max) step = step_max;
- if (step > end - bad_hi) step = end - bad_hi;
- pos = bad_hi + step - 1;
+ step += bad_hi - bad_lo;
+ if (step > end - bad_lo) step = end - bad_lo;
+
+ /* Now we look at the last sector of the new interval we've just marked
+ * out.
+ */
+ pos = bad_lo + step - 1;
want = run_length_wanted(pos, step, end);
n = recovery_read(&r, pos, want);
#ifdef DEBUG
printf(";; [bound] try reading %"PRIuSEC" .. %"PRIuSEC" -> %zd\n",
pos, pos + want, n);
#endif
+
+ /* If everything went OK then we're done with this phase. */
if (n == want) break;
+
+ /* If it failed then extend the bad region to cover (the end of) the bad
+ * sector which terminated the run, and go around again.
+ */
if (n < 0) n = 0;
bad_hi = pos + n + 1;
}
+ /* Third major step: identify exactly where the bad region ends. This is
+ * a binary search.
+ */
good = pos;
while (good > bad_hi) {
- report_bad_blocks_progress(bad_hi, errno);
#ifdef DEBUG
progress_clear(&progress);
printf(";; limiting bad-block region: "
"%"PRIuSEC" ..%"PRIuSEC".. %"PRIuSEC" ..%"PRIuSEC".. %"PRIuSEC"\n",
bad_lo, bad_hi - bad_lo, bad_hi, good - bad_hi, good);
#endif
- pos = bad_hi + (good - bad_hi)/2;
- step = pos - bad_lo;
+
+ /* Update the progress report. */
+ report_bad_blocks_progress(bad_hi, errno);
+
+ /* Pick a new place to try. */
+ pos = bad_hi + (good - bad_hi)/2; step = pos - bad_lo;
want = run_length_wanted(pos, step, end);
+
+ /* Try reading. */
n = recovery_read(&r, pos, want);
#ifdef DEBUG
printf(";; [limit] try reading %"PRIuSEC" .. %"PRIuSEC" -> %zd\n",
pos, pos + want, n);
#endif
+
+ /* If that worked -- i.e., we got all the data we wanted -- then bring
+ * down the `good' bound. If it failed, then bring up `bad_hi' to cover
+ * the bad sector which terminated our read attempt.
+ */
if (n < 0) n = 0;
if (n == want) good = pos;
else bad_hi = pos + n + 1;
}
- recovered(bad_lo, bad_hi); *pos_inout = good;
- if (good < r.pos + r.start || r.pos + r.end <= good)
+
+ /* We're done. It's time to tidy up.
+ *
+ * One subtle point: it's possible that, as a result of retrying previous
+ * bad blocks, that we ended up with bad_hi > good, so it's important that
+ * we make a consistent choice between the two. I've gone with `good'
+ * because (a) this gives us more of the original data from the disc and
+ * (b) hopefully any marginal sectors are now in our buffer
+ */
+ recovered(bad_lo, good); *pos_inout = good;
+
+ /* Figure out how much data we can return to the caller from our buffer. */
+ if (good < r.pos + r.start || r.pos + r.end <= good) {
+ /* Our new position is outside of the region covered by the short-range
+ * tracking, so there's nothing to return.
+ */
+
n = 0;
- else {
+ } else {
+ /* The new position is covered, so shuffle the data to the start of the
+ * buffer and return as much as we can.
+ */
+
n = r.pos + r.end - good;
rearrange_sectors(&r, 0, good - r.pos, n);
}
+
+ /* We're done. */
#ifdef DEBUG
show_recovery_buffer_map(&r, "returning %zd good sectors at %"PRIuSEC"",
n, good);
return (n);
}
+/*----- Copying data from a single input file -----------------------------*/
+
static void emit(secaddr start, secaddr end)
+ /* Copy sectors with absolute addresses from START (inclusive) to END
+ * (exclusive) to the output. The entire input region comes from the
+ * same source, already established as `file'.
+ */
{
-#define BUFSECTORS 512
+#define BUFSECTORS 512 /* this is a megabyte */
int least;
unsigned char buf[BUFSECTORS*SECTORSZ];
int i;
#endif
+ /* Choose an active file through which to read the source contents. We're
+ * guaranteed that this file will do for the entire input region. We
+ * choose the active file with the smallest index. The virtual `raw' file
+ * which represents the underlying block device has the largest index, so
+ * we'll always use a `.VOB' file if one is available. Looking at the
+ * protocol suggests that the host and drive identify the per-title CSS key
+ * by the start sector address of the `.VOB' file, so coincident files must
+ * all use the same key. I've not encountered properly overlapping files
+ * in the wild.
+ */
least = least_live();
-
#ifdef DEBUG
printf(";; %8"PRIuSEC" .. %"PRIuSEC"\n", start, end);
-
for (i = 0; i < filetab.n; i++) {
if (!livep(i)) continue;
if (act == -1) act = i;
assert(act == least);
#endif
- if (least == -1)
- { file = 0; vob = 0; }
- else {
+ /* Set the global variables up for reading from the file we decided on.
+ * These will be primarily used by `read_sectors' and `update_progress'.
+ */
+ if (least == -1) {
+ /* There's nothing at all. This can happen because the kernel reported
+ * the wrong block-device size for some reason but the filesystem has
+ * identified files which start beyond the reported size, leaving a gap.
+ */
+
+ file = 0; vob = 0;
+ } else {
+ /* There's a (possibly) virtual file. */
+
file = &filetab.v[least];
switch (id_kind(file->id)) {
+
case RAW:
+ /* It's the raw device. Clear `vob' to prompt `read_sectors' to read
+ * directly from `dvdfd'.
+ */
+
vob = 0;
break;
+
case VOB:
+ /* It's a `.VOB' file. We read these through `libdvdread', which
+ * handles CSS unscrambling for us.
+ */
+
+ /* The first time we open a `.VOB' file, `libdvdread' wants to spray
+ * a bunch of information about how it's getting on cracking the
+ * title keys. This will interfere with the progress display, so
+ * preemptively hide the display.
+ */
if (first_time) { progress_clear(&progress); first_time = 0; }
+
+ /* Open the `.VOB' file. */
vob = DVDOpenFile(dvd, id_title(file->id),
id_part(file->id)
? DVD_READ_TITLE_VOBS
bail("failed to open %s %u",
id_part(file->id) ? "title" : "menu",
id_title(file->id));
- progress_update(&progress);
break;
+
default:
+ /* Some other kind of thing; but there shouldn't be anything else in
+ * the file table, so there's a bug.
+ */
abort();
+
}
}
+ /* If we're not reading from the raw device then add an additional progress
+ * bar for the current file. This isn't completely pointless: having a
+ * ready visualization for whereabouts we are in a file is valuable when we
+ * encounter bad blocks, because regions of intentional bad blocks near the
+ * starts and and ends of VOBs are common on discs from annoying studios.
+ */
if (file && id_kind(file->id) != RAW) {
file_progress.render = render_file_progress;
progress_additem(&progress, &file_progress);
}
+ /* Put the progress display back, if we took it away, and show the file
+ * progress bar if we added one.
+ */
+ update_progress(start);
+
+ /* Read the input region and copy it to the disc. */
pos = start;
while (pos < end) {
+
+ /* Decide how much we want. Fill the buffer, unless there's not enough
+ * input left.
+ */
want = end - pos; if (want > BUFSECTORS) want = BUFSECTORS;
+
+ /* Try to read the input. */
n = read_sectors(pos, buf, want);
- if (n <= 0) n = find_good_sector(&pos, end, buf, BUFSECTORS);
- if (n > 0) { carefully_write(outfd, buf, n*SECTORSZ); pos += n; }
+ if (n <= 0) {
+ /* It didn't work. Time to deploy the skipping-past-bad-blocks
+ * machinery we worked so hard on. This will fill the buffer with
+ * stuff and return a new count of how much it read.
+ */
+
+ n = find_good_sector(&pos, end, buf, BUFSECTORS);
+ }
+ if (n > 0) {
+ /* We made some progress. Write the stuff that we read to the output
+ * file and update the position.
+ */
+
+ carefully_write(outfd, buf, n*SECTORSZ); pos += n;
+ }
+
+ /* Report our new progress. */
report_progress(pos);
}
+ /* Close the `libdvdread' file, if we opened one. */
if (vob) { DVDCloseFile(vob); vob = 0; }
+ /* If we added a per-file progress bar, then take it away again. */
if (file && id_kind(file->id) != RAW)
progress_removeitem(&progress, &file_progress);
+
+ /* Update the progress display to report our glorious success. */
progress_update(&progress);
#undef BUFSECTORS
}
-struct buf {
- char *p;
- size_t n, sz;
-};
-#define BUF_INIT { 0, 0, 0 }
-#define BUF_REWIND(b) do { (b)->n = 0; } while (0)
-#define BUF_FREE(b) do { \
- buf *_b = (b); \
- free(_b->p); _b->p = 0; _b->n = _b->sz = 0; \
-} while (0)
-#define BUF_PUTC(b, ch) do { \
- struct buf *_b = (b); \
- if (_b->n >= _b->sz) { \
- _b->sz = _b->sz ? 2*_b->sz : 32; \
- _b->p = realloc(_b->p, _b->sz); \
- if (!_b->p) bail("out of memory allocating %zu bytes", _b->sz); \
- } \
- _b->p[_b->n] = (ch); \
-} while (0)
-
-static int read_line(FILE *fp, struct buf *b)
-{
- int ch;
-
- ch = getc(fp);
- if (ch == EOF)
- return (-1);
- else if (ch != '\n') do {
- BUF_PUTC(b, ch); b->n++;
- ch = getc(fp);
- } while (ch != EOF && ch != '\n');
- BUF_PUTC(b, 0);
- return (0);
-}
-
-static double parse_float(const char **p_inout, double min, double max,
- const char *what)
-{
- const char *p;
- char *q;
- double x;
- int err;
-
- err = errno; errno = 0;
- p = *p_inout;
- x = strtod(p, &q);
- if (errno || x < min || x > max) bail("bad %s `%s'", what, p);
- *p_inout = q; errno = err;
- return (x);
-}
-
-static long parse_int(const char **p_inout, long min, long max,
- const char *what)
-{
- const char *p;
- char *q;
- long x;
- int err;
-
- err = errno; errno = 0;
- p = *p_inout;
- x = strtoul(p, &q, 0);
- if (errno || x < min || x > max) bail("bad %s `%s'", what, p);
- *p_inout = q; errno = err;
- return (x);
-}
-
-#define PRF_HYPHEN 1u
-static int parse_range(const char *p, unsigned f,
- secaddr *start_out, secaddr *end_out)
-{
- char *q;
- int err, rc;
- unsigned long start, end;
-
- err = errno;
-
- if (ISDIGIT(*p)) {
- start = strtoul(p, &q, 0);
- if (errno || start >= SECLIMIT) { rc = -1; goto end; }
- *start_out = start; p = q;
- } else if (!(f&PRF_HYPHEN))
- { rc = -1; goto end; }
- else
- start = 0;
-
- if (f&PRF_HYPHEN) {
- if (*p != '-') { rc = -1; goto end; }
- p++;
- } else {
- if (!ISSPACE(*p)) { rc = -1; goto end; }
- do p++; while (ISSPACE(*p));
- }
-
- if (ISDIGIT(*p)) {
- end = strtoul(p, &q, 0);
- if (errno || end > SECLIMIT || end < start) { rc = -1; goto end; }
- *end_out = end; p = q;
- } else if (!(f&PRF_HYPHEN))
- { rc = -1; goto end; }
-
- if (!(f&PRF_HYPHEN)) while (ISSPACE(*p)) p++;
- if (*p && ((f&PRF_HYPHEN) || *p != '#')) { rc = -1; goto end; }
-
- rc = 0;
-end:
- errno = err;
- return (rc);
-}
-
-#ifdef DEBUG
-static void dump_eventq(const char *what)
-{
- unsigned i;
- const struct event *ev;
- char fn[MAXFNSZ];
-
- printf("\n;; event dump (%s):\n", what);
- for (i = 0; i < eventq.n; i++) {
- ev = &eventq.v[i];
- switch (ev->ev) {
- case EV_BEGIN:
- store_filename(fn, filetab.v[ev->file].id);
- printf(";; %8"PRIuSEC": begin %s\n", ev->pos, fn);
- break;
- case EV_END:
- store_filename(fn, filetab.v[ev->file].id);
- printf(";; %8"PRIuSEC": end %s\n", ev->pos, fn);
- break;
- case EV_WRITE:
- printf(";; %8"PRIuSEC": write\n", ev->pos);
- break;
- case EV_STOP:
- printf(";; %8"PRIuSEC": stop\n", ev->pos);
- break;
- default:
- printf(";; %8"PRIuSEC": ?%u\n", ev->pos, ev->ev);
- break;
- }
- }
-}
-#endif
+/*----- Main program ------------------------------------------------------*/
int main(int argc, char *argv[])
{
#define f_fixup 4u
#define f_stats 8u
#define f_checkid 16u
+#define f_retry 32u
#define f_write 256u
+#define f_file 512u
set_prog(argv[0]);
+
+ /* First up, handle the command-line options. */
for (;;) {
- opt = getopt(argc, argv, "hB:E:FR:X:b:cir:s"); if (opt < 0) break;
+ opt = getopt(argc, argv, "hB:E:FP:R:X:b:cir:s"); if (opt < 0) break;
switch (opt) {
+
+ /* `-h': Help. */
case 'h': usage(stderr); exit(0);
+
+ /* `-B PARAM=VALUE[,...]': Setting internal parameters. */
case 'B':
+
+ /* Set up a cursor into the parameter string. */
p = optarg;
+
#define SKIP_PREFIX(s) \
(STRNCMP(p, ==, s "=", sizeof(s)) && (p += sizeof(s), 1))
+ /* If the text at P matches `S=' then advance P past that and
+ * evaluate nonzero; otherwise evaluate zero.
+ */
+
for (;;) {
+
if (SKIP_PREFIX("cf"))
- clear_factor = parse_float(&p, 0, DBL_MAX, "clear factor");
+ clear_factor = parse_float(&p, PNF_JUNK, 0, DBL_MAX,
+ "clear factor");
+
else if (SKIP_PREFIX("cmin"))
- clear_min = parse_int(&p, 1, SECLIMIT, "clear minimum");
+ clear_min = parse_int(&p, PNF_JUNK, 1, SECLIMIT,
+ "clear minimum");
+
else if (SKIP_PREFIX("cmax"))
- clear_max = parse_int(&p, 1, SECLIMIT, "clear maximum");
+ clear_max = parse_int(&p, PNF_JUNK, 1, SECLIMIT,
+ "clear maximum");
+
else if (SKIP_PREFIX("sf"))
- step_factor = parse_float(&p, 0, DBL_MAX, "step factor");
+ step_factor = parse_float(&p, PNF_JUNK, 0, DBL_MAX,
+ "step factor");
+
else if (SKIP_PREFIX("smin"))
- step_min = parse_int(&p, 1, SECLIMIT - 1, "step minimum");
+ step_min = parse_int(&p, PNF_JUNK, 1, SECLIMIT - 1,
+ "step minimum");
+
else if (SKIP_PREFIX("smax"))
- step_max = parse_int(&p, 1, SECLIMIT - 1, "step maximum");
+ step_max = parse_int(&p, PNF_JUNK, 1, SECLIMIT - 1,
+ "step maximum");
+
else if (SKIP_PREFIX("retry"))
- max_retries = parse_int(&p, 0, INT_MAX, "retries");
+ max_retries = parse_int(&p, PNF_JUNK, 0, INT_MAX, "retries");
+
else if (SKIP_PREFIX("alpha"))
- alpha = parse_float(&p, 0, 1, "average decay factor");
+ alpha = parse_float(&p, PNF_JUNK, 0, 1, "average decay factor");
+
else if (SKIP_PREFIX("_badwait"))
- bad_block_delay = parse_float(&p, 0, DBL_MAX, "bad-block delay");
+ bad_block_delay = parse_float(&p, PNF_JUNK, 0, DBL_MAX,
+ "bad-block delay");
+
else if (SKIP_PREFIX("_blkwait"))
- good_block_delay = parse_float(&p, 0, DBL_MAX, "good block delay");
+ good_block_delay = parse_float(&p, PNF_JUNK, 0, DBL_MAX,
+ "good block delay");
+
else
bail("unknown bad blocks parameter `%s'", p);
+
+ /* If we're now at the end of the string then we're done. */
if (!*p) break;
- else if (*p != ',') bail("unexpected junk in parameters");
+
+ /* We're not done yet, so there should now be a comma and another
+ * parameter setting.
+ */
+ if (*p != ',') bail("unexpected junk in parameters");
p++;
}
+
#undef SKIP_PREFIX
break;
+
+ /* `-E FILE' (undocumented): Log the bad sectors we encountered to
+ * FILE.
+ */
case 'E': errfile = optarg; break;
+
+ /* `-F' (undocumented): Hack for fixing up images that were broken by
+ * an old early-stop bug.
+ */
case 'F': f |= f_fixup; break;
+
+ /* `-P FILE' (undocumented): trace progress state to FILE. */
+ case 'P':
+ if (progressfp) bail("progress trace file already set");
+ progressfp = fopen(optarg, "w");
+ if (!progressfp)
+ bail_syserr(errno, "failed to open progress trace file `%s'",
+ optarg);
+ break;
+
+ /* `-R FILE': Read ranges to retry from FILE. Retry ranges are
+ * converted into `EV_WRITE' and `EV_STOP' events.
+ */
case 'R':
fp = fopen(optarg, "r");
if (!fp)
bail_syserr(errno, "failed to open ranges file `%s'", optarg);
+
+ /* We're going to try to coalesce adjacent ranges from the file.
+ * When we found a region to skip, we'd have stopped at the a file
+ * boundary, and possibly restarted again immediately afterwards,
+ * resulting in two adjacent regions in the file. To do that, and
+ * also to police the restriction that ranges occur in ascending
+ * order, we keep track of the upper bound for the most recent range
+ * -- but there isn't one yet, so we use a sentinel value.
+ */
i = 0; last = -1;
for (;;) {
- BUF_REWIND(&buf); if (read_line(fp, &buf)) break;
- p = buf.p; i++;
+
+ /* Read a line from the buffer. If there's nothing left then we're
+ * done.
+ */
+ buf_rewind(&buf); if (read_line(fp, &buf)) break;
+
+ /* Increment the line counter and establish a cursor. */
+ i++; p = buf.p;
+
+ /* Skip initial whitespace. */
while (ISSPACE(*p)) p++;
+
+ /* If this is a comment then ignore it and go round again. */
if (!*p || *p == '#') continue;
+
+ /* Parse the range. Check that the ranges are coming out in
+ * ascending order.
+ */
if (parse_range(p, 0, &start, &end) ||
(last <= SECLIMIT && start < last))
bail("bad range `%s' at `%s' line %zu", buf.p, optarg, i);
+
+ /* Ignore empty ranges: this is important (see below where we sort
+ * the event queue). If this abuts the previous range then just
+ * overwrite the previous end position. Otherwise, write a new
+ * pair of events.
+ */
if (start < end) {
if (start == last)
eventq.v[eventq.n - 1].pos = end;
last = end;
}
}
+
+ /* Check for read errors. */
if (ferror(fp))
bail_syserr(errno, "failed to read ranges file `%s'", optarg);
+ f |= f_retry;
break;
+
+ /* `-X FILE' (undocumented): Read ranges of bad-blocks from FILE to
+ * establish fake bad blocks: see `read_sectors' above for the details.
+ *
+ * This is very similar to the `-R' option above, except that it
+ * doesn't do the range coalescing thing.
+ */
case 'X':
fp = fopen(optarg, "r");
if (!fp)
bail_syserr(errno, "failed to open bad-blocks file `%s'", optarg);
i = 0; last = -1;
for (;;) {
- BUF_REWIND(&buf); if (read_line(fp, &buf)) break;
+ buf_rewind(&buf); if (read_line(fp, &buf)) break;
p = buf.p; i++;
while (ISSPACE(*p)) p++;
if (!*p || *p == '#') continue;
if (parse_range(p, 0, &start, &end) ||
(last <= SECLIMIT && start < last))
bail("bad range `%s' at `%s' line %zu", buf.p, optarg, i);
- if (start < end)
- { VEC_PUSH(bad, &badblocks); bad->start = start; bad->end = end; }
+ if (start < end) {
+ VEC_PUSH(bad, &badblocks);
+ bad->start = start; bad->end = end;
+ }
}
if (ferror(fp))
bail_syserr(errno, "failed to read bad-blocks file `%s'", optarg);
break;
+
+ /* Log regions skipped because of bad blocks to a file. */
case 'b':
if (mapfile) bail("can't have multiple map files");
mapfile = optarg;
break;
+
+ /* `-c': Continue copying where we left off last time. */
case 'c': f |= f_continue; break;
+
+ /* `-i': Check that we're copying from the right disc. */
case 'i': f |= f_checkid; break;
+
+ /* `-r [START]-[END]': Manually provide a range of sectors to retry. */
case 'r':
- start = 0; end = -1;
+ start = 0; end = -1; f |= f_retry;
if (parse_range(optarg, PRF_HYPHEN, &start, &end))
bail("bad range `%s'", optarg);
if (start < end) {
+ /* Again, ignore empty ranges. */
put_event(EV_WRITE, 0, start);
if (end <= SECLIMIT) put_event(EV_STOP, 0, end);
}
break;
+
+ /* `-s': Print statistics at the end. */
case 's': f |= f_stats; break;
+
+ /* Anything else is an error. */
default: f |= f_bogus; break;
}
}
+
+ /* We expect two arguments. Check this. Complain about bad usage if we
+ * have bad arguments or options.
+ */
if (argc - optind != 2) f |= f_bogus;
if (f&f_bogus) { usage(stderr); exit(2); }
-
- setlocale(LC_ALL, "");
- progress_init(&progress);
device = argv[optind]; outfile = argv[optind + 1];
+ /* If there are fake bad blocks (the `-X' option) then sort the list
+ * because `read_sectors' wants to use a binary search.
+ */
if (badblocks.n) {
qsort(badblocks.v, badblocks.n, sizeof(struct badblock),
compare_badblock);
#endif
}
- open_dvd(device, &dvdfd, &dvd);
+ /* Prepare to display progress information. */
+ setlocale(LC_ALL, "");
+ progress_init(&progress);
+
+ /* Open the input device. (This may pop up a notice if there's nothing in
+ * the drive.)
+ */
+ if (open_dvd(device, O_RDONLY, &dvdfd, &dvd)) exit(2);
- blksz = SECTORSZ; volsz = device_size(dvdfd, device, &blksz);
- if (blksz != SECTORSZ)
+ /* Determine the size of the input device and check the sector size. */
+ blksz = -1; volsz = device_size(dvdfd, device, &blksz);
+ if (blksz == -1)
+ { blksz = SECTORSZ; f |= f_file; }
+ else if (blksz != SECTORSZ)
bail("device `%s' block size %d /= %d", device, blksz, SECTORSZ);
if (volsz%SECTORSZ)
bail("device `%s' volume size %"PRIu64" not a multiple of %d",
device, volsz, SECTORSZ);
+ setup_geometry(&geom, dvdfd, f&f_file ? 0 : GF_BLKDEV, volsz/blksz);
+
+ if (progressfp) {
+ switch (geom.shape) {
+ case FLAT:
+ fprintf(progressfp, ":model flat-model\n");
+ break;
+ case SINGLE:
+ fprintf(progressfp, ":model single-layer-model :start %"PRIuSEC"\n",
+ geom.start0);
+ break;
+ case PTP:
+ fprintf(progressfp,
+ ":model parallel-track-path-model "
+ ":start0 %"PRIuSEC" :start1 %"PRIuSEC" "
+ ":midpoint %"PRIuSEC"\n",
+ geom.start0, geom.start1, geom.midpoint);
+ break;
+ case OTP:
+ fprintf(progressfp,
+ ":model opposite-track-path-model "
+ ":start %"PRIuSEC" :midpoint %"PRIuSEC"\n",
+ geom.start0, geom.midpoint);
+ break;
+ default:
+ abort();
+ }
+ }
+
+ /* Maybe check that we're copying from the right disc. This is intended to
+ * help avoid image corruption by from the wrong disc, but it obviously
+ * only works if the output file is mostly there.
+ */
if (f&f_checkid) {
- open_dvd(outfile, 0, &dvd_out);
+ if (open_dvd(outfile, O_RDONLY, 0, &dvd_out)) exit(2);
if (dvd_id(id_in, dvd, DIF_MUSTIFOHASH, device) ||
dvd_id(id_out, dvd_out, DIF_MUSTIFOHASH, device))
exit(2);
device, id_in, outfile, id_out);
}
+ /* Open the output file. */
outfd = open(outfile, O_WRONLY | O_CREAT, 0666);
if (outfd < 0)
bail_syserr(errno, "failed to create output file `%s'", outfile);
if (f&f_continue) {
+ /* If we're continuing from where we left off, then find out where that
+ * was and make a note to copy from there to the end of the disc. Note
+ * that we're not relying on this position: in particular, it might not
+ * even be sector-aligned (in which case we'll ignore the final partial
+ * sector). We'll seek to the right place again when we start writing.
+ */
+
off = lseek(outfd, 0, SEEK_END);
if (off < 0)
bail_syserr(errno, "failed to seek to end of output file `%s'",
outfile);
- put_event(EV_WRITE, 0, off/SECTORSZ);
- } else if (!eventq.n && !(f&f_fixup))
+ put_event(EV_WRITE, 0, off/SECTORSZ); f |= f_retry;
+ }
+
+ if (!(f&(f_retry | f_fixup))) {
+ /* If there are no ranges to retry and we're not fixing an ancient early-
+ * stop bug, then there's no range to retry and we should just copy
+ * everything.
+ */
+
put_event(EV_WRITE, 0, 0);
+ }
- /* It's fast enough just to check everything. */
+ /* Now it's time to figure out what the input looks like. Work through the
+ * titlesets in order, mapping out where the video-object files are. We
+ * could figure out how many there are properly, but it's fast enough just
+ * to try everything. That's the menu only for the special titleset 0, and
+ * menu and titles for the remaining titlesets 1 up to 99.
+ */
put_menu(dvd, 0);
for (i = 1; i < 100; i++) {
put_menu(dvd, i);
put_title(dvd, i);
}
+
+ /* Make a final virtual file for the raw device. (See `emit', which
+ * assumes that this is the last entry in the file table.) Check that we
+ * don't have more files than we expect, because the bitmap table has fixed
+ * size.
+ */
put_file(mkident(RAW, 0, 0), 0, volsz/SECTORSZ);
assert(filetab.n <= MAXFILES);
+ /* Find an upper limit for what we're supposed to copy. Since the `RAW'
+ * entry covers the reported size of the input device, this ought to cover
+ * all of our bases.
+ */
for (i = 0, limit = 0; i < filetab.n; i++)
if (filetab.v[i].end > limit) limit = filetab.v[i].end;
-
- if (end > limit) end = limit;
-
#ifdef DEBUG
printf("\n;; files:\n");
for (i = 0; i < filetab.n; i++) {
file = &filetab.v[i];
store_filename(fn, file->id);
- printf(";;\t%8"PRIuSEC" %s\n", file->start, fn);
+ printf(";;\t%8"PRIuSEC" .. %-8"PRIuSEC" %s\n",
+ file->start, file->end, fn);
}
#endif
+ /* Sort the event list.
+ *
+ * The event-code ordering is important here.
+ *
+ * * `EV_STOP' sorts /before/ `EV_WRITE'. If we have two abutting ranges
+ * to retry, then we should stop at the end of the first, and then
+ * immediately start again. If empty ranges were permitted then we'd
+ * stop writing and /then/ start, continuing forever, which is clearly
+ * wrong.
+ *
+ * * `EV_BEGIN' sorts before `EV_END'. If we have empty files then we
+ * should set the bit that indicates that it's started, and then clear
+ * it, in that order. If we have abutting files, then we'll just both
+ * bits for an instant, but that's not a problem.
+ */
qsort(eventq.v, eventq.n, sizeof(struct event), compare_event);
- f &= ~f_write; start = 0;
- for (i = 0; i < eventq.n; i++) {
+ /* Check that the event list is well-formed. We start out at the
+ * beginning, not writing anything.
+ */
+ for (i = 0, f &= ~f_write, start = 0; i < eventq.n; i++) {
ev = &eventq.v[i];
switch (ev->ev) {
+
case EV_WRITE:
+ /* Start writing. We shouldn't be writing yet! */
+
if (f&f_write)
- bail("overlapping ranges: range from %"PRIuSEC" still open at %"PRIuSEC"",
+ bail("overlapping ranges: range from %"PRIuSEC" "
+ "still open at %"PRIuSEC"",
start, ev->pos);
f |= f_write; start = ev->pos;
break;
+
case EV_STOP:
+ /* Stop writing. Make a note that we've done this. */
+
f &= ~f_write;
break;
}
}
-
#ifdef DEBUG
dump_eventq("initial");
#endif
- f &= ~f_write; start = 0;
- for (i = 0; i < eventq.n; i++) {
+
+ /* Now we make a second pass over the event queue to fix it up. Also
+ * count up how much work we'll be doing so that we can report progress.
+ */
+ for (i = 0, f &= ~f_write, start = last = 0; i < eventq.n; i++) {
ev = &eventq.v[i];
- switch (ev->ev) {
- case EV_WRITE: start = ev->pos; f |= f_write; break;
- case EV_STOP: nsectors += ev->pos - start; f &= ~f_write; break;
- }
+
+ /* If we're supposed to start writing then make a note of the start
+ * position. We'll want this to count up how much work we're doing. The
+ * start position of the final range is also used by the logic below that
+ * determines the progress display.
+ */
+ if (ev->ev == EV_WRITE) { start = ev->pos; f |= f_write; }
+
+ /* If this event position is past our final limit then stop. Nothing
+ * beyond here can possibly be interesting. (Since `EV_WRITE' sorts
+ * before other events, we will notice an `EV_WRITE' exactly at the limit
+ * sector, but not any other kind of event.)
+ */
if (ev->pos >= limit) break;
- if (f&f_fixup) start = ev->pos;
+
+ /* If we're supposed to stop writing here, then add the size of the
+ * most recent range onto our running total.
+ */
+ if (ev->ev == EV_STOP) {
+ nsectors += ev->pos - start;
+ total_linear += linear_progress(&geom, start, ev->pos);
+ f &= ~f_write;
+ }
+
+ /* If we're fixing up images affected by the old early-stop bug, then
+ * remember this position.
+ */
+ if (f&f_fixup) last = ev->pos;
}
+
+ /* Truncate the event queue at the point we reached the sector limit. */
eventq.n = i;
#ifdef DEBUG
dump_eventq("trimmed");
#endif
+
+ /* Finally, the early-stop bug fix.
+ *
+ * The bug was caused by a broken version of the event-queue truncation
+ * logic: it trimmed the event queue, but didn't add a final event at the
+ * file limit. The effect was that the interval between the last event --
+ * likely `EV_END' for a VOB file -- and the overall end of the disc didn't
+ * get copied. We address this by starting to write at the position of
+ * this last event.
+ */
if (f&f_fixup) {
- put_event(EV_WRITE, 0, start);
+ put_event(EV_WRITE, 0, last);
f |= f_write;
}
+
+ /* If we're still writing then avoid the early-end bug by adding an
+ * `EV_STOP' event at the limit position. Include this range in the sector
+ * count.
+ */
if (f&f_write) {
nsectors += limit - start;
+ total_linear += linear_progress(&geom, start, limit);
put_event(EV_STOP, 0, limit);
}
#ifdef DEBUG
dump_eventq("final");
#endif
+ /* Set up the main progress display.
+ *
+ * If we're copying a single region from somewhere to the end of the disc
+ * then it seems more sensible to use a single progress bar for both. If
+ * we're reading multiple ranges, maybe because we're retrying bad blocks,
+ * then it's better to have separate bars for how much actual copying we've
+ * done, and which part of the disc we're currently working on.
+ */
copy_progress.render = render_copy_progress;
progress_additem(&progress, ©_progress);
- if (nsectors == limit - start)
- { ndone = start; nsectors = limit; }
+ if (nsectors == limit - start) {
+ ndone = start; nsectors = limit;
+ done_linear = linear_progress(&geom, 0, start);
+ total_linear += done_linear;
+ }
else {
disc_progress.render = render_disc_progress;
progress_additem(&progress, &disc_progress);
}
+ /* If we're producing overall statistics then make a note of the current
+ * time.
+ */
if (f&f_stats) gettimeofday(&tv0, 0);
+ /* We're now ready to start our sweep through the disc. */
#ifdef DEBUG
printf("\n;; event sweep:\n");
#endif
- f &= ~f_write;
- for (pos = 0, i = 0; i < eventq.n; i++) {
+
+ /* We start at the beginning of the disc, and the start of the event queue,
+ * not writing. We'll advance through the events one by one.
+ */
+ for (pos = 0, i = 0, f &= ~f_write; i < eventq.n; i++) {
+
+ /* Get the next event. */
ev = &eventq.v[i];
+
+ /* If there's a nonempty range between here and the previous event then
+ * we need to process this.
+ */
if (ev->pos > pos) {
+
+ /* If we're writing then copy the interval from the previous event to
+ * here to the output.
+ */
if (f&f_write) emit(pos, ev->pos);
+
+ /* Advance the current position now that the output is up-to-date. */
pos = ev->pos;
+
#ifdef DEBUG
progress_clear(&progress);
printf(";;\n");
#endif
}
+
+ /* Decide what to action to take in response to the event. */
switch (ev->ev) {
+
case EV_BEGIN:
+ /* A file has started. Set the appropriate bit in the active-files
+ * map.
+ */
set_live(ev->file);
#ifdef DEBUG
store_filename(fn, filetab.v[ev->file].id);
printf(";; %8"PRIuSEC": begin `%s'\n", pos, fn);
#endif
break;
+
case EV_WRITE:
+ /* We're supposed to start writing. */
+
+ /* Note the current time and position for the progress display. */
gettimeofday(&last_time, 0); last_pos = pos;
+
+ /* Seek to the right place in the output file. */
if (lseek(outfd, (off_t)ev->pos*SECTORSZ, SEEK_SET) < 0)
bail_syserr(errno,
"failed to seek to resume position "
"(sector %"PRIuSEC") in output file `%s'",
ev->pos, outfile);
+
+ /* Engage the write head. */
+ f |= f_write;
+
#ifdef DEBUG
progress_clear(&progress);
printf(";; %8"PRIuSEC": begin write\n", pos);
#endif
- f |= f_write;
break;
+
case EV_STOP:
+ /* We're supposed to stop writing. Disengage the write head. */
+
f &= ~f_write;
#ifdef DEBUG
progress_clear(&progress);
printf(";; %8"PRIuSEC": end write\n", pos);
#endif
break;
+
case EV_END:
+ /* We've found the end of a file. Clear its bit in the table. */
+
clear_live(ev->file);
#ifdef DEBUG
store_filename(fn, filetab.v[ev->file].id);
printf(";; %8"PRIuSEC": end `%s'\n", pos, fn);
#endif
break;
+
+ /* Something else. Clearly a bug. */
default: abort();
}
}
+ /* Take down the progress display because we're done. */
progress_clear(&progress);
+ /* Set the output file length correctly. */
if (ftruncate(outfd, (off_t)limit*SECTORSZ) < 0)
bail_syserr(errno, "failed to set output file `%s' length", outfile);
+ /* Report overall statistics. */
if (f&f_stats) {
gettimeofday(&tv1, 0); t = tvdiff(&tv0, &tv1);
if (nsectors == limit) { ndone -= start; nsectors -= start; }
tot, totunit, fmttime(t, timebuf), rate, rateunit);
}
+ /* Close files. */
if (dvd) DVDClose(dvd);
if (dvdfd >= 0) close(dvdfd);
if (outfd >= 0) close(outfd);
carefully_fclose(mapfp, "bad-sector region map");
carefully_fclose(errfp, "bad-sector error log");
+ carefully_fclose(progressfp, "progress trace file");
progress_free(&progress);
+ /* We're done! */
+ return (0);
+
#undef f_bogus
#undef f_continue
#undef f_fixup
#undef f_stats
#undef f_write
-
- return (0);
}
+
+/*----- That's all, folks -------------------------------------------------*/