+ /* 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]));
+}
+
+/*----- Moving average machinery ------------------------------------------*
+ *
+ * We're using an exponential moving average with a weighting factor of α
+ * (`alpha', above); larger values are more sensitive to recent changes. If
+ * the old average was v_1, and the measurement in the current interval is x,
+ * then the new average after this interval is
+ *
+ * v = α x + (1 − α) v_1 .
+ *
+ * Write β = 1 − α; so
+ *
+ * v = α x + β v_1 .
+ *
+ * Let x_0 = x, let x_1 be the measurement from the previous interval, and,
+ * in general, let x_i be the measurement from i intervals ago. Then another
+ * way to write the above would be
+ *
+ * v = α (x_0 + β x_1 + ⋯ + β^i x_i + ⋯) .
+ *
+ * Alas, our time intervals are not regular. Suppose that we get our next
+ * measurement after a gap of t intervals, for some integer t. We can
+ * compensate approximately by pretending that all of the missed intervals --
+ * and our new one -- had the same mean rate. Then we'd have calculated
+ *
+ * v = α (x + β x + ⋯ + β^{t−1} x) + β^t v_1
+ *
+ * 1 − β^t
+ * = α x ------- + β^t v_1
+ * 1 − β
+ *
+ * = x (1 − β^t) + β^t v_1 (since α = 1 − β)
+ *
+ * = x + β^t (v_1 − x) .
+ *
+ * Does this work in general? It's clearly correct in the case t = 1.
+ *
+ * Suppose the old average was v_2, and that over a period of t intervals
+ * (where t is not necessarily an integer) we measured a mean rate of x, and
+ * then after u intervals we measured a mean rate of x /again/. Then we'd
+ * firstly determine
+ *
+ * v_1 = x + β^t (v_2 − x)
+ *
+ * and then
+ *
+ * v = x + β^u (v_1 − x)
+ *
+ * = x + β^u (x + β^t (v_2 − x) − x)
+ *
+ * = x + β^{t+u} (v_2 − x) ,
+ *
+ * which is exactly what we'd have done if we'd calculated the same mean rate
+ * over the combined span of t + u intervals.
+ *
+ * One final wrinkle, in case that wasn't enough. There's a problem with the
+ * initial setup of an exponential moving average. Apparently
+ * (https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average)
+ * explains that we can do this better by calculating the average after k
+ * intervals as
+ *
+ * x_0 + β x_1 + β^2 x_2 + ⋯ + β^{k−1} x_{k−1}
+ * v′ = ------------------------------------------- .
+ * 1 + β + β^2 + ⋯ + β^{k−1}
+ *
+ * The numerator is our existing v/α; the denominator is (1 − β^k)/α; the
+ * factors of α cancel, and we find that v′ = v/(1 − β^k). This still holds
+ * in our situation, where k may not be an integer.
+ *
+ * To apply all of this:
+ *
+ * * we maintain the moving average v in `avg';
+ *
+ * * we maintain the total β^k in `corr'; and
+ *
+ * * we compute v′ = v/(1 − β^k) on demand up in `render_perfstats'.
+ */
+
+struct avg {
+ double avg, corr;
+};
+#define AVG_INIT { 0.0, 1.0 }
+
+static double alpha = 0.1; /* weighting factor for average */
+
+static void update_avg(struct avg *a, double t, double n)
+{
+ double rate = n/t, beta_t = pow(1 - alpha, t);
+
+ a->avg = rate + beta_t*(a->avg - rate);
+ a->corr *= beta_t;
+}
+
+static inline double current_avg(const struct avg *a)
+ { return (a->avg/(1 - a->corr)); }
+
+/*----- The nonlinear progress model --------------------------------------*/
+
+/* The recorded portion of a single-layer DVD (i.e., DVD-5) can hold 2298496
+ * sectors of user data. This is preceded by 0x30000 = 196608 sectors of
+ * lead-in information, for a totoal of 2495104 sectors.
+ *
+ * The readable portion of a disc is an annulus with respective internal and
+ * external diameters of 44 mm and 117 mm. This annulus has an area of
+ * 9230.8 mm^2, so DVD has a storage density of about 270.3 sectors/mm^2. If
+ * the interior of the annulus were used for data storage rather than leaving
+ * a hole for a spindle and a clamping area, then it would be 10751 mm^2 and
+ * could store 2906107 sectors. (That means that the portion of the disc
+ * that's actually used to make it spin could have stored an additional
+ * 411003 sectors.)
+ *
+ * Sectors aren't stored on the surface willy-nilly, but arranged into a
+ * single archimedean spiral; bits are stored along this spiral at a
+ * more-or-less constant pitch. We are therefore led into an investigation
+ * of the arc-length of archimedean spirals.
+ *
+ * It's best to start with the polar equation of the spiral, which is simply
+ *
+ * r = k θ
+ *
+ * for a given constant k. The arc length of a curve expressed using polar
+ * coordinates is given by
+ *
+ * s = ∫ √(r^2 + (dr/dθ)^2) dθ
+ *
+ * = ∫ √(k^2 θ^2 + k^2) dθ
+ *
+ * = k ∫ √(1 + θ^2) dθ
+ *
+ * k
+ * = - [ θ √(1 + θ^2) + log(θ + √(1 + θ^2)) ] - s_0.
+ * 2
+ *
+ * We're assuming that the sectors are spaced out at a constant linear
+ * density along the spiral. We don't know the units for s, but there's some
+ * constant L such that A = s/L; so
+ *
+ * k
+ * A = --- [ θ √(1 + θ^2) + log(θ + √(1 + θ^2)) ] - A_0
+ * 2 L
+ *
+ * for some suitable constant A_0.
+ *
+ * Finally, we're assuming that the disc is spinning with some approximately
+ * constant angular velocity ω, so θ = ω t, giving
+ *
+ * k
+ * A = --- [ ω t √(1 + ω^2 t^2) + log(ω + √(1 + ω^2 t^2)) ] + A_0 .
+ * 2 L
+ *
+ * We can calculate approximate values for k/(2 L) and A_0. As stated above,
+ * the track pitch is about 0.75 µm; our inside and outside diameters of
+ * 44 mm and 117 mm correspond to angles of 184306 and 490088 radians
+ * respectively. Feeding those into the above equation for s gives arc
+ * lengths of 16984492558 and 120093346360 respectively, in unknown units.
+ * The difference is 103108853802, which should correspond to 2495104
+ * sectors, giving us 41324 arc-length units per sector. As a cross-check,
+ * the arc length corresponding to the inside diameter yields 411003 sectors,
+ * which is the same as we calculated above. This will be our A_0
+ */
+
+#ifdef unusef
+static double archimedes_arclen(double t)
+ /* Given an angle T, return the arc length of the canonical
+ * archimedean spiral r = θ, from θ = 0 up to θ = T.
+ */
+{
+ double u;
+
+ u = sqrt(1 + t*t);
+ return (t*u + log(t + u))/2;
+}
+#endif
+
+static double inv_archimedes_arclen(double s)
+ /* Given an arc length S, return the angle T such that the arc length
+ * of the canonical archimedean spiral r = θ, from θ = 0 up to θ = T,
+ * is equal to S.
+ */
+{
+ /* There is no closed-form solution, so we're going to invert the arc-
+ * length formula above numerically, using the Newton--Raphson method.
+ *
+ * Given an incorrect guess x_0 of a zero of some function f, we refine the
+ * guess by approximating f by its tangent at the point (x_0, f(x_0)).
+ * This will be a line with an equation like
+ *
+ * y = f'(x_0) x + c .
+ *
+ * We know that y = f(x_0) when x = x_0, so we can calculate
+ *
+ * c = f(x_0) - f'(x_0) x_0 .
+ *
+ * This will be zero when
+ *
+ * y = f'(x_0) x + f(x_0) - f'(x_0) x_0 = 0
+ *
+ * hwnce
+ *
+ * f'(x_0) x_0) - f(x_0) f(x_0)
+ * x = --------------------- = x_0 - ------- .
+ * f'(x_0) f'(x_0)
+ */
+
+ double t, ss, u, e;
+
+ /* We need to choose an initial estimate. This seems to work well in
+ * practice.
+ */
+ t = 1.5*sqrt(s);
+
+ for (;;) {
+ /* Compute s' = f(t). We open-code this calculation because the
+ * intermediate value √(1 + t^2) is also the gradient.
+ */
+ u = sqrt(1 + t*t);
+ ss = (t*u + log(t + u))/2;
+
+ /* Determine the error in f(t). We don't actually need much precision
+ * here, but 2 ulp seems achievable in practice with minimal cost: the
+ * usually sequence converges after only five iterations.
+ */
+ e = fabs(s/ss - 1);
+ if (e <= 2*DBL_EPSILON) return (t);
+
+ /* Not good enough. Refine the guess and go around again. */
+ t -= (ss - s)/u;
+ }
+}
+
+static double sectors_to_angle(secaddr base, secaddr low, secaddr high)
+ /* Return the angle, in radians, subtended by the range LOW up to
+ * HIGH of user sector addresses, given the physical sector address
+ * BASE of the first user-data sectors.
+ */
+{
+#define A0 411003.262489
+#define K 41324.4713654
+
+ return (inv_archimedes_arclen(K*(A0 + base + high)) -
+ inv_archimedes_arclen(K*(A0 + base + low)));
+
+#undef A0
+#undef K
+}
+
+enum {
+ FLAT, /* not actually a real DVD */
+ SINGLE, /* disc with only one layer */
+ PTP, /* two layers, parallel track path */
+ OTP /* two layers, opposite track path */
+};
+
+struct geometry {
+ unsigned shape; /* one of the four codes above */
+ secaddr start0, start1; /* initial physical sector */
+ secaddr midpoint; /* sector address of layer switch */
+};
+
+#define GF_BLKDEV 1u
+static void setup_geometry(struct geometry *g, int fd, unsigned f,
+ secaddr sz)
+ /* Initialize G with information about the disc structure. FD is a
+ * file descriptor for the device; SZ is the size of the disc in
+ * sectors. If `GF_BLKDEV' is clear in F then assume that FD refers
+ * to a regular file; G is populated with a `FLAT' performance model.
+ * If `GF_BLKDEV' is set, then FD refers to a block device, so try to
+ * retreive detailed structure information from the drive.
+ */
+{
+#ifdef __linux__
+ dvd_struct ds;
+ const struct dvd_layer *ly;
+#endif
+ secaddr t;
+
+#define LAYER_LIMIT 2298496 /* maximum (user) sectors on layer */
+#define DVDROM_OFFSET 0x30000 /* usual initial physical sector */
+
+ if (!(f&GF_BLKDEV)) {
+ /* We're reading from a regular file. Assume that progress will be
+ * linear.
+ */
+
+ g->shape = FLAT;
+ g->midpoint = SECLIMIT;
+ return;
+ }
+
+#ifdef __linux__
+ /* We have Linux and its DVD ioctl(2) calls. Interrogate the disc to
+ * discover its structure.
+ */
+
+ ds.type = DVD_STRUCT_PHYSICAL;
+ ds.physical.layer_num = 0;
+ if (ioctl(fd, DVD_READ_STRUCT, &ds)) {
+ moan_syserr(errno, "failed to read physical disc structure");
+ goto guess_structure;
+ }
+ ly = &ds.physical.layer[0];
+ switch (ly->nlayers) {
+ case 0:
+ g->shape = SINGLE;
+ g->start0 = g->start1 = 0;
+ g->midpoint = SECLIMIT;
+ break;
+ case 1:
+ g->start0 = ly->start_sector;
+ if (ly->track_path) {
+ g->shape = OTP;
+ g->start1 = 0;
+ g->midpoint = ly->end_sector_l0 - ly->start_sector + 1;
+ } else {
+ g->shape = PTP;
+ g->midpoint = ly->end_sector - ly->start_sector + 1;
+ ds.physical.layer_num = 1;
+ if (ioctl(fd, DVD_READ_STRUCT, &ds)) {
+ moan_syserr(errno, "failed to read layer 1 physical structure");
+ goto guess_structure;
+ }
+ g->start1 = ly->start_sector;
+ }
+ break;
+ default:
+ moan("unexpected layer count %d", ly->nlayers + 1);
+ goto guess_structure;
+ }
+ return;
+guess_structure:
+#endif
+
+ /* Either we don't have Linux, or we found something confusing. Let's try
+ * to guess at what's going on.
+ *
+ * If the volume size is small enough to fit on a single layer then assume
+ * that's what's happened; otherwise assume opposite track path with a cut
+ * at the midpoint, rounded up to an ECC block (16 sectors).
+ */
+ g->start0 = DVDROM_OFFSET; g->start1 = 0;
+ if (sz <= LAYER_LIMIT) {
+ g->shape = SINGLE;
+ g->midpoint = SECLIMIT;
+ } else {
+ g->shape = OTP;
+ t = (sz + DVDROM_OFFSET)/2;
+ t += 15; t &= -16;
+ t -= DVDROM_OFFSET;
+ g->midpoint = t;
+ }
+
+#undef LAYER_LIMIT
+#undef DVDROM_OFFSET
+}
+
+static double linear_progress(const struct geometry *g,
+ secaddr a0, secaddr a1)
+ /* Convert the sector range from A0 to A1 into a progress measurement
+ * which is, by intention, approximately linearly related to time,
+ * given a geometry description G.
+ */
+{
+ double theta = 0.0;
+
+ switch (g->shape) {
+ case FLAT:
+ theta = a1 - a0;
+ break;
+ case SINGLE:
+ theta = sectors_to_angle(g->start0, a0, a1);
+ break;
+ case PTP:
+ if (a0 < g->midpoint)
+ theta += sectors_to_angle(g->start0,
+ a0, a1 < g->midpoint ? a1 : g->midpoint);
+ if (a1 > g->midpoint)
+ theta += sectors_to_angle(g->start1,
+ a0 > g->midpoint ? a0 : g->midpoint, a1);
+ break;
+ case OTP:
+ if (a0 < g->midpoint)
+ theta += sectors_to_angle(g->start0,
+ a0, a1 < g->midpoint ? a1 : g->midpoint);
+ if (a1 > g->midpoint)
+ theta += sectors_to_angle(g->start0,
+ 2*g->midpoint - a1,
+ a0 > g->midpoint ?
+ 2*g->midpoint - a0 : g->midpoint);
+ break;
+ default:
+ abort();
+ }
+ return (theta);
+}
+
+/*----- Common variables used by the copying machinery --------------------*/
+
+/* General reading state. */
+static dvd_reader_t *dvd; /* `libdvdread' state for device */
+static int dvdfd = -1, outfd = -1; /* input device and output image */
+static struct file *file; /* currently active file */
+static dvd_file_t *vob; /* current `.VOB' file, or null */
+static const char *mapfile; static FILE *mapfp; /* skipped regions map */
+static const char *errfile; static FILE *errfp; /* bad-sector log */
+static secaddr limit; /* upper bound on sectors */
+
+static secaddr bad_start; /* start of current bad region */
+static unsigned retry, max_retries = 4; /* retry state */
+
+/*----- Progress reporting ------------------------------------------------*/
+
+static secaddr nsectors, ndone; /* number of sectors done/to do */
+static double total_linear, done_linear; /* linear progress tracking */
+static secaddr last_pos; /* position last time we updated */
+static struct timeval last_time; /* time last time we updated */
+static struct geometry geom; /* disc geometry for progress */
+static struct avg avg_rate = AVG_INIT, avg_linear = AVG_INIT;
+static int bad_err; /* most recent error code */
+static FILE *progressfp; /* file on which to trace progress data */
+
+static const char throbber[] = "|<-<|>->"; /* throbber pattern */
+static unsigned throbix = 0; /* current throbber index */
+
+static struct progress_item /* stock progress items */
+ copy_progress, disc_progress,
+ file_progress, badblock_progress;
+
+static double scale_bytes(double n, const char **unit_out)
+ /* Determine a human-readable representation for N bytes. Divide N
+ * by some power of 1024, and store in *UNIT_OUT a string
+ * representing the conventional unit-prefix for that power of 1024.
+ */
+{
+ const char *unit = "";
+
+ if (n > 1600) { n /= 1024; unit = "k"; }
+ 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);
+}
+
+#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);
+ else sprintf(buf, "%ld:%02ld:%02ld", t/3600, (t/60)%60, t%60);
+ return (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, linrate;
+ const char *unit;
+
+ /* 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 ",
+ 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;
+
+ progress_putleft(render, " %c copied %.1f%%",
+ throbber[throbix], 100.0*frac);
+ render_perfstats(render);
+ progress_putleft(render, " (%"PRIuSEC" of %"PRIuSEC")", ndone, nsectors);
+
+ progress_showbar(render, frac);
+}
+
+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;
+
+ progress_putleft(render, " disc %.1f%% (%"PRIuSEC" of %"PRIuSEC")",
+ 100.0*frac, last_pos, limit);
+ progress_showbar(render, frac);
+}
+
+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];
+ double frac;
+
+ store_filename(fn, file->id);
+ frac = (double)off/len;
+ progress_putleft(render, " `%s' %.1f%% (%"PRIuSEC" of %"PRIuSEC")",
+ fn, 100.0*frac, off, len);
+ progress_showbar(render, frac);
+}
+
+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;
+
+ if (!n) {
+ progress_putleft(render, " Retrying bad sector %"PRIuSEC"", bad_start);
+ progress_putright(render, "attempt %u/%u ", retry + 1, max_retries);
+ bg = 4;
+ } else {
+ progress_putleft(render, " Found %"PRIuSEC" bad %s",
+ n, n == 1 ? "sector" : "sectors");
+ progress_putright(render, "%"PRIuSEC" .. %"PRIuSEC" ",
+ bad_start, last_pos);
+ bg = 1;
+ }
+ if (bad_err && bad_err != EIO)
+ progress_putleft(render, " (%s)", strerror(bad_err));
+ progress_shownotice(render, bg, 7);
+}
+
+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, delta_r;
+
+ /* 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) {
+ /* 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;
+ }
+
+ /* 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); }
+
+/*----- 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, 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;
+ int fakeerr = 0;
+ 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);
+ 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; 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",
+ best->start, best->end);
+#endif
+ if (best && pos + want > best->start)
+ { want = best->start - pos; fakeerr = EIO; sit(bad_block_delay); }
+ }
+
+ /* 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) {
+ if (lseek(dvdfd, (off_t)pos*SECTORSZ, SEEK_SET) < 0)
+ bail_syserr(errno, "failed to seek to sector %"PRIuSEC"", pos);
+ errno = 0; n = read(dvdfd, p, want*SECTORSZ);
+ if (n >= 0) n /= SECTORSZ;
+ } else {
+ memset(p, 0, want*SECTORSZ);
+ 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) {
+ open_file_on_demand(errfile, &errfp, "bad-sector error log");
+ fprintf(errfp, "%"PRIuSEC" %"PRIuSEC"\n", pos, pos + 1);
+ check_write(errfp, "bad-sector error log");
+ 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);
+}
+
+/*----- 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 {
+ /* 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);
+ memmove(r->buf + dest*SECTORSZ, r->buf + src*SECTORSZ, len*SECTORSZ);
+}
+
+#ifdef DEBUG
+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);
+ printf(";; recovery buffer (");
+ vprintf(what, ap);
+ printf("): "
+ "(%"PRIuSEC") ..%"PRIuSEC".. "
+ "[%"PRIuSEC" ..%"PRIuSEC".. %"PRIuSEC"] "
+ "..%"PRIuSEC".. (%"PRIuSEC")\n",
+ r->pos, r->start,
+ r->pos + r->start, r->end - r->start, r->pos + r->end,
+ r->sz - r->end, r->pos + r->sz);
+ va_end(ap);
+ assert(r->start <= r->end);
+ assert(r->end <= r->sz);
+}
+#endif
+
+static ssize_t recovery_read_sectors(struct recoverybuf *r,
+ secaddr pos, secaddr off, secaddr want)
+ /* Try to read WANT sectors starting at sector address POS from the
+ * current file into R's buffer, at offset OFF sectors from the start
+ * of the buffer. Return the number of sectors read, zero if at end
+ * of file, or -1 in the event of a system error.
+ *
+ * This is a trivial wrapper around `read_sectors' with some
+ * additional range checking, used only by `recovery_read_buffer'
+ * below.
+ */
+{
+ ssize_t n;
+
+ assert(off <= r->sz); assert(want <= r->sz - off);
+ assert(pos == r->pos + off);
+ n = read_sectors(pos, r->buf + off*SECTORSZ, want);
+ return (n);
+}
+
+static ssize_t recovery_read_buffer(struct recoverybuf *r,
+ secaddr pos, secaddr want)
+ /* Try to read WANT sectors, starting at sector address POS, from the
+ * current file into the buffer R, returning a count of the number of
+ * sectors read, or 0 if at end of file, or -1 in the case of a
+ * system error, as for `read_sectors'. The data will end up
+ * /somewhere/ in the buffer, but not necessarily at the start.
+ */
+{
+ secaddr diff, pp, nn;
+ ssize_t n;
+
+ /* This is the main piece of the short-range tracking machinery. It's
+ * rather complicated, so hold on tight. (It's much simpler -- and less
+ * broken -- than earlier versions were, though.)
+ */
+
+#ifdef DEBUG
+ progress_clear(&progress);
+ show_recovery_buffer_map(r, "begin(%"PRIuSEC", %"PRIuSEC")", pos, want);
+#endif
+
+ /* The first order of business is to make space in the buffer for this new
+ * data. We therefore start with a case analysis.
+ */
+ if (pos < r->pos) {
+ /* The new position is before the current start of the buffer, so we have
+ * no choice but to decrease the buffer position, which will involve
+ * shifting the existing material upwards.
+ */
+
+ /* Determine how far up we'll need to shift. */
+ diff = r->pos - pos;
+
+ if (r->start + diff >= r->sz) {
+ /* The material that's currently in the buffer would be completely
+ * shifted off the end, so we have no choice but to discard it
+ * completely.
+ */
+
+ r->pos = pos; r->start = r->end = 0;
+#ifdef DEBUG
+ show_recovery_buffer_map(r, "cleared; shift up by %"PRIuSEC"", diff);
+#endif
+ } else {
+ /* Some of the material in the buffer will still be there. We might
+ * lose some stuff off the end: start by throwing that away, and then
+ * whatever's left can be moved easily.
+ */
+
+ if (r->end + diff > r->sz) r->end = r->sz - diff;
+ rearrange_sectors(r, r->start + diff, r->start, r->end - r->start);
+ r->pos -= diff; r->start += diff; r->end += diff;
+#ifdef DEBUG
+ show_recovery_buffer_map(r, "shifted up by %"PRIuSEC"", diff);
+#endif
+ }
+ } else if (pos > r->pos + r->end) {
+ /* The new position is strictly beyond the old region. We /could/ maybe
+ * keep this material, but it turns out to be better not to. To keep it,
+ * we'd have to also read the stuff that's in between the end of the old
+ * region and the start of the new one, and that might contain bad
+ * sectors which the caller is specifically trying to skip. We just
+ * discard the entire region here so as not to subvert the caller's
+ * optimizations.
+ */
+
+ r->pos = pos; r->start = r->end = 0;
+#ifdef DEBUG
+ show_recovery_buffer_map(r, "cleared; beyond previous region");
+#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;
+#ifdef DEBUG
+ show_recovery_buffer_map(r, "shifted down by %"PRIuSEC"", diff);
+#endif
+ }
+ }
+
+ /* 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);
+#endif
+ n = recovery_read_sectors(r, pos, pp, nn);
+#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");
+#endif
+ }
+
+ 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);
+ fflush(stdout);
+#endif
+ n = recovery_read_sectors(r, r->pos + pp, pp, nn);
+#ifdef DEBUG
+ printf(" -> %zd\n", n);
+#endif
+
+ /* See whether it worked. */
+ if (n > 0) {
+ /* We read something, so add it onto the existing region. */
+
+ r->end += n;
+#ifdef DEBUG
+ show_recovery_buffer_map(r, "joined new region");
+#endif
+ }
+ }
+
+ /* 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
+ return (n);
+}
+
+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);