X-Git-Url: https://git.distorted.org.uk/~mdw/dvdrip/blobdiff_plain/88693140beeffa7d96b19e0910f53f8075f4748f..refs/heads/mdw/cleanup:/dvd-sector-copy.c diff --git a/dvd-sector-copy.c b/dvd-sector-copy.c index bed3cf6..8408f41 100644 --- a/dvd-sector-copy.c +++ b/dvd-sector-copy.c @@ -1,129 +1,348 @@ +/* -*-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 . + */ + +/*----- Header files ------------------------------------------------------*/ + #include "lib.h" +#ifdef __linux__ +# include +#endif + +/*----- Program usage summary ---------------------------------------------*/ + static void usage(FILE *fp) { fprintf(fp, - "usage: %s [-c] [-B PARAM=VALUE,...] [-R MAP]\n" + "usage: %s [-ci] [-B PARAM=VALUE,...] [-R MAP]\n" "\t[-b OUTMAP] [-r [START]-[END]] DEVICE OUTFILE\n", prog); } -static double tvdiff(const struct timeval *tv_lo, - const struct timeval *tv_hi) -{ - return ((tv_hi->tv_sec - tv_lo->tv_sec) + - (tv_hi->tv_usec - tv_lo->tv_usec)/1.0e6); -} +/*----- Random utilities --------------------------------------------------*/ -static void carefully_write(int fd, const void *buf, size_t sz) +#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]'. + */ { - const unsigned char *p = buf; - ssize_t n; + char *q; + int err, rc; + unsigned long start, end; - if (fd < 0) return; - while (sz) { - n = write(fd, p, sz); - if (n < 0) { - if (errno == EINTR) continue; - bail_syserr(errno, "failed to write to output file"); - } - if (!n) bail("unexpected short write to output file"); - p += n; sz -= n; + /* 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; } -} -static void open_file_on_demand(const char *file, FILE **fp_inout, - const char *what) -{ - FILE *fp; + /* 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. */ - if (!*fp_inout) { - fp = fopen(file, "w"); - if (!fp) - bail_syserr(errno, "failed to open %s file `%s'", what, file); - fprintf(fp, "## %s\n\n", what); - *fp_inout = fp; + rc = -1; goto end; } -} -static void check_write(FILE *fp, const char *what) -{ - fflush(fp); - if (ferror(fp)) bail_syserr(errno, "error writing %s file", what); -} + /* In the file syntax, we're now allowed whitespace, so skip past that. */ + if (!(f&PRF_HYPHEN)) while (ISSPACE(*p)) p++; -static void carefully_fclose(FILE *fp, const char *what) -{ - if (fp && (ferror(fp) || fclose(fp))) - bail_syserr(errno, "error writing %s file", what); + /* 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; } @@ -131,10 +350,17 @@ found: 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; @@ -143,6 +369,9 @@ static void put_event(unsigned evtype, unsigned file, secaddr pos) } 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; @@ -154,26 +383,42 @@ static void put_file(ident id, secaddr start, secaddr end) } 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; @@ -181,6 +426,7 @@ static void put_title(dvd_reader_t *dvd, unsigned title) 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", @@ -188,6 +434,11 @@ static void put_title(dvd_reader_t *dvd, unsigned title) } #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) @@ -199,22 +450,442 @@ static void put_title(dvd_reader_t *dvd, unsigned title) 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; -static const char throbber[] = "|<-<|>->"; -static unsigned throbix = 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 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 = ""; @@ -222,16 +893,15 @@ static double scale_bytes(double n, const char **unit_out) 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); @@ -240,22 +910,35 @@ static char *fmttime(unsigned long t, char *buf) } 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; @@ -269,6 +952,9 @@ static void render_copy_progress(struct progress_item *item, 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; @@ -279,6 +965,9 @@ static void render_disc_progress(struct progress_item *item, 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]; @@ -293,6 +982,9 @@ static void render_file_progress(struct progress_item *item, 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; @@ -314,60 +1006,96 @@ static void render_badblock_progress(struct progress_item *item, } 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 ALPHA 0.1 -#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 ALPHA -#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; @@ -375,23 +1103,61 @@ static ssize_t read_sectors(secaddr pos, void *buf, secaddr want) 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_clear(&progress); printf(";; searching badblocks for %"PRIuSEC" .. %"PRIuSEC"\n", 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", @@ -400,8 +1166,20 @@ static ssize_t read_sectors(secaddr pos, void *buf, secaddr want) 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) { @@ -414,6 +1192,11 @@ static ssize_t read_sectors(secaddr pos, void *buf, secaddr want) 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) { @@ -423,70 +1206,70 @@ static ssize_t read_sectors(secaddr pos, void *buf, secaddr want) 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); } @@ -494,11 +1277,12 @@ static void rearrange_sectors(struct recoverybuf *r, 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; va_start(ap, what); - progress_clear(); + progress_clear(&progress); printf(";; recovery buffer ("); vprintf(what, ap); printf("): " @@ -516,33 +1300,74 @@ static PRINTF_LIKE(2, 3) 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_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; @@ -551,18 +1376,44 @@ static ssize_t recovery_read_buffer(struct recoverybuf *r, #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 -p 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; @@ -572,8 +1423,27 @@ p show_recovery_buffer_map(r, "cleared; beyond previous region"); } } + /* 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); @@ -582,10 +1452,24 @@ p show_recovery_buffer_map(r, "cleared; beyond previous region"); #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"); @@ -593,7 +1477,18 @@ p show_recovery_buffer_map(r, "cleared; beyond previous 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); @@ -603,7 +1498,11 @@ p show_recovery_buffer_map(r, "cleared; beyond previous region"); #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"); @@ -611,11 +1510,19 @@ p show_recovery_buffer_map(r, "cleared; beyond previous 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 @@ -624,95 +1531,279 @@ end: 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; +/*----- Skipping past regions of bad sectors ------------------------------*/ + +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 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). + */ +{ + char fn[MAXFNSZ]; + + /* 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; - want = clear_factor*badlen; + /* 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); @@ -721,65 +1812,136 @@ static ssize_t find_good_sector(secaddr *pos_inout, secaddr end, } } + /* 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); @@ -787,9 +1949,15 @@ static ssize_t find_good_sector(secaddr *pos_inout, secaddr end, 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]; @@ -804,11 +1972,19 @@ static void emit(secaddr start, secaddr end) 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; @@ -820,16 +1996,43 @@ static void emit(secaddr start, secaddr end) 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 @@ -838,152 +2041,85 @@ static void emit(secaddr start, secaddr end) 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); -} +/*----- Main program ------------------------------------------------------*/ int main(int argc, char *argv[]) { unsigned f = 0; const char *p; - uint64_t volsz; + off_t volsz; secaddr pos; off_t off; secaddr start, end, last; @@ -991,15 +2127,14 @@ int main(int argc, char *argv[]) const char *device, *outfile; struct badblock *bad; int opt, blksz; - unsigned n; size_t i; FILE *fp; struct buf buf = BUF_INIT; struct timeval tv0, tv1; double t, rate, tot; const char *rateunit, *totunit; - char timebuf[TIMESTRMAX]; - struct stat st; + char timebuf[TIMESTRMAX], id_in[MAXIDSZ], id_out[MAXIDSZ]; + dvd_reader_t *dvd_out; #ifdef DEBUG const struct file *file; char fn[MAXFNSZ]; @@ -1009,59 +2144,153 @@ int main(int argc, char *argv[]) #define f_continue 2u #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:cr: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, 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; @@ -1072,53 +2301,83 @@ int main(int argc, char *argv[]) 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); @@ -1130,127 +2389,310 @@ int main(int argc, char *argv[]) #endif } - open_dvd(device, &dvdfd, &dvd); - if (fstat(dvdfd, &st)) - bail_syserr(errno, "failed to stat device `%s'", device); - if (S_ISREG(st.st_mode)) { - blksz = SECTORSZ; - volsz = st.st_size; - } else if (S_ISBLK(st.st_mode)) { - if (ioctl(dvdfd, BLKSSZGET, &blksz)) - bail_syserr(errno, "failed to get block size for `%s'", device); - if (ioctl(dvdfd, BLKGETSIZE64, &volsz)) - bail_syserr(errno, "failed to get volume size for `%s'", device); - } else - bail("can't use `%s' as source: expected file or block device", device); - - if (blksz != SECTORSZ) + /* 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); + + /* 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) { + 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); + if (STRCMP(id_in, !=, id_out)) + bail("DVD id mismatch: input `%s' is `%s'; output `%s' is `%s'", + 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; n = 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); - n++; f |= f_write; 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); - n++; f |= f_write; + 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); @@ -1258,27 +2700,42 @@ int main(int argc, char *argv[]) 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); @@ -1286,36 +2743,46 @@ int main(int argc, char *argv[]) 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 - start) { ndone -= start; nsectors -= start; } + if (nsectors == limit) { ndone -= start; nsectors -= start; } tot = scale_bytes((double)nsectors*SECTORSZ, &totunit); rate = scale_bytes((double)nsectors*SECTORSZ/t, &rateunit); moan("all done: %.1f %sB in %s -- %.1f %sB/s", 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 -------------------------------------------------*/