Commit cb3afaea authored by David Zeuthen's avatar David Zeuthen

Use libdvdcss for creating disk images of DVDs, if available

Without this change, the archive process may fail for some DVD video
discs with "Add. Sense: Read of scrambled sector without
authentication".

This commit does NOT add a hard dependency on libdvdcss (which would
be bad as this library isn't even shipped in most OSes) since it's
accessed via dlopen(). If libdvdcss is not available on the system, we
simply fall back to reading data using the standard libc interfaces
(e.g. read(2)).

License-wise there is no problem with this approach: libdvdcss is
licensed under the GPLv2+ which is exactly the same license as Disks
itself.

This dlopen() approach is nothing new - it's also used in Brasero,
VLC, mplayer, Totem and many other applications.
Signed-off-by: 's avatarDavid Zeuthen <zeuthen@gmail.com>
parent 9da7ab4a
......@@ -87,13 +87,15 @@ GTK3_REQUIRED=3.5.8
LIBSECRET1_REQUIRED=0.7
PWQUALITY_REQUIRED=1.0.0
CANBERRA_REQUIRED=0.1
LIBDVDREAD_REQUIRED=4.2.0
PKG_CHECK_MODULES(GLIB2, [gmodule-2.0 gio-unix-2.0 >= $GLIB2_REQUIRED])
PKG_CHECK_MODULES(UDISKS2, [udisks2 >= $UDISKS2_REQUIRED])
PKG_CHECK_MODULES(GTK3, [gtk+-3.0 >= $GTK3_REQUIRED])
PKG_CHECK_MODULES(LIBSECRET1, [libsecret-1 >= $LIBSECRET1_REQUIRED])
PKG_CHECK_MODULES(PWQUALITY, [pwquality >= $PWQUALITY_REQUIRED])
PKG_CHECK_MODULES(CANBERRA, [libcanberra-gtk3 >= CANBERRA_REQUIRED])
PKG_CHECK_MODULES(CANBERRA, [libcanberra-gtk3 >= $CANBERRA_REQUIRED])
PKG_CHECK_MODULES(LIBDVDREAD, [dvdread >= $LIBDVDREAD_REQUIRED])
GLIB_GSETTINGS
......
......@@ -51,6 +51,7 @@ gnome_disks_SOURCES = \
gducreateraidarraydialog.h gducreateraidarraydialog.c \
gduselectdiskdialog.h gduselectdiskdialog.c \
gduerasemultipledisksdialog.h gduerasemultipledisksdialog.c \
gdudvdsupport.h gdudvdsupport.c \
$(enum_built_sources) \
$(NULL)
......@@ -70,6 +71,7 @@ gnome_disks_CFLAGS = \
$(LIBSYSTEMD_LOGIN_CFLAGS) \
$(PWQUALITY_CFLAGS) \
$(CANBERRA_CFLAGS) \
$(LIBDVDREAD_CFLAGS) \
$(WARN_CFLAGS) \
-lm \
$(NULL)
......@@ -82,6 +84,7 @@ gnome_disks_LDADD = \
$(LIBSYSTEMD_LOGIN_LIBS) \
$(PWQUALITY_LIBS) \
$(CANBERRA_LIBS) \
$(LIBDVDREAD_LIBS) \
$(top_builddir)/src/libgdu/libgdu.la \
$(NULL)
......
......@@ -13,7 +13,6 @@
#include <gio/gunixfdlist.h>
#include <gio/gunixinputstream.h>
#include <gmodule.h>
#include <glib-unix.h>
#include <sys/ioctl.h>
#include <linux/fs.h>
......@@ -27,6 +26,8 @@
#include "gducreatefilesystemwidget.h"
#include "gduestimator.h"
#include "gdudvdsupport.h"
/* TODOs / ideas for Disk Image creation
*
* - Be tolerant of I/O errors like dd_rescue(1), see http://www.gnu.org/s/ddrescue/ddrescue.html
......@@ -78,6 +79,7 @@ typedef struct
GMutex copy_lock;
GduEstimator *estimator;
gboolean retrieving_dvd_keys;
guint64 num_error_bytes;
gint64 start_time_usec;
gint64 end_time_usec;
......@@ -316,6 +318,10 @@ update_gui (DialogData *data,
g_free (s3);
g_free (s2);
}
else if (data->retrieving_dvd_keys)
{
s = g_strdup (_("Retrieving DVD keys"));
}
else if (bytes_per_sec > 0 && usec_remaining > 0)
{
s2 = g_format_size (bytes_completed);
......@@ -464,6 +470,7 @@ copy_span (int fd,
guint64 size,
guchar *buffer,
gboolean pad_with_zeroes,
GduDVDSupport *dvd_support,
GCancellable *cancellable,
GError **error)
{
......@@ -477,35 +484,45 @@ copy_span (int fd,
g_return_val_if_fail (-1, cancellable == NULL || G_IS_CANCELLABLE (cancellable));
g_return_val_if_fail (-1, error == NULL || *error == NULL);
if (lseek (fd, offset, SEEK_SET) == (off_t) -1)
{
g_set_error (error,
G_IO_ERROR, g_io_error_from_errno (errno),
"Error seeking to offset %" G_GUINT64_FORMAT ": %s",
offset, strerror (errno));
goto out;
}
copy_read_again:
num_bytes_read = read (fd, buffer, size);
if (num_bytes_read < 0)
if (dvd_support != NULL)
{
if (errno == EAGAIN || errno == EINTR)
goto copy_read_again;
/* do not consider this an error - treat as zero bytes read */
num_bytes_read = 0;
num_bytes_read = gdu_dvd_support_read (dvd_support, fd, buffer, offset, size);
}
else
{
/* EOF */
if (num_bytes_read == 0)
if (lseek (fd, offset, SEEK_SET) == (off_t) -1)
{
g_set_error (error,
G_IO_ERROR, G_IO_ERROR_FAILED,
"Reading from offset %" G_GUINT64_FORMAT " returned zero bytes",
offset);
G_IO_ERROR, g_io_error_from_errno (errno),
"Error seeking to offset %" G_GUINT64_FORMAT ": %s",
offset, strerror (errno));
goto out;
}
read_again:
num_bytes_read = read (fd, buffer, size);
if (num_bytes_read < 0)
{
if (errno == EAGAIN || errno == EINTR)
goto read_again;
}
else
{
/* EOF */
if (num_bytes_read == 0)
{
g_set_error (error,
G_IO_ERROR, G_IO_ERROR_FAILED,
"Reading from offset %" G_GUINT64_FORMAT " returned zero bytes",
offset);
goto out;
}
}
}
if (num_bytes_read < 0)
{
/* do not consider this an error - treat as zero bytes read */
num_bytes_read = 0;
}
num_bytes_to_write = num_bytes_read;
......@@ -554,6 +571,7 @@ static gpointer
copy_thread_func (gpointer user_data)
{
DialogData *data = user_data;
GduDVDSupport *dvd_support = NULL;
guchar *buffer_unaligned = NULL;
guchar *buffer = NULL;
guint64 block_device_size = 0;
......@@ -578,7 +596,29 @@ copy_thread_func (gpointer user_data)
*/
if (g_str_has_prefix (udisks_block_get_device (data->block), "/dev/sr"))
{
fd = open (udisks_block_get_device (data->block), O_RDONLY);
const gchar *device_file = udisks_block_get_device (data->block);
fd = open (device_file, O_RDONLY);
/* Use libdvdcss (if available on the system) on DVDs with UDF
* filesystems - otherwise the backup process may fail because
* of unreadable/scrambled sectors
*/
if (g_strcmp0 (udisks_block_get_id_usage (data->block), "filesystem") == 0 &&
g_strcmp0 (udisks_block_get_id_type (data->block), "udf") == 0 &&
g_str_has_prefix (udisks_drive_get_media (data->drive), "optical_dvd"))
{
g_mutex_lock (&data->copy_lock);
data->retrieving_dvd_keys = TRUE;
g_mutex_unlock (&data->copy_lock);
g_idle_add (on_update_ui, dialog_data_ref (data));
dvd_support = gdu_dvd_support_new (device_file, udisks_block_get_size (data->block));
g_mutex_lock (&data->copy_lock);
data->retrieving_dvd_keys = FALSE;
g_mutex_unlock (&data->copy_lock);
g_idle_add (on_update_ui, dialog_data_ref (data));
}
}
/* Otherwise, request the fd from udisks */
......@@ -673,6 +713,7 @@ copy_thread_func (gpointer user_data)
num_bytes_to_read,
buffer,
TRUE, /* pad_with_zeroes */
dvd_support,
data->cancellable,
&error);
if (num_bytes_read < 0)
......@@ -694,6 +735,9 @@ copy_thread_func (gpointer user_data)
}
out:
if (dvd_support != NULL)
gdu_dvd_support_free (dvd_support);
data->end_time_usec = g_get_real_time ();
/* in either case, close the stream */
......
/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
*
* Copyright (C) 2008-2012 Red Hat, Inc.
*
* Licensed under GPL version 2 or later.
*
* Author: David Zeuthen <zeuthen@gmail.com>
*/
#include "config.h"
#include <gmodule.h>
#include <glib-unix.h>
#include <sys/ioctl.h>
#include <linux/fs.h>
#include <dvdread/dvd_reader.h>
#include <dvdread/dvd_udf.h>
#include "gdudvdsupport.h"
/* ---------------------------------------------------------------------------------------------------- */
/* libdvdcss support - see http://www.videolan.org/developers/libdvdcss.html */
#define DVDCSS_BLOCK_SIZE 2048
#define DVDCSS_READ_DECRYPT (1 << 0)
#define DVDCSS_SEEK_KEY (1 << 1)
struct dvdcss_s;
typedef struct dvdcss_s* dvdcss_t;
static dvdcss_t (*dvdcss_open) (const char *psz_target) = NULL;
static int (*dvdcss_close) (dvdcss_t ctx) = NULL;
static int (*dvdcss_seek) (dvdcss_t ctx,
int i_blocks,
int i_flags ) = NULL;
static int (*dvdcss_read) (dvdcss_t ctx,
void *p_buffer,
int i_blocks,
int i_flags ) = NULL;
static int (*dvdcss_readv) (dvdcss_t ctx,
void *p_iovec,
int i_blocks,
int i_flags ) = NULL;
static char * (*dvdcss_error) (dvdcss_t ctx) = NULL;
static gboolean
have_dvdcss (void)
{
static gsize once = 0;
static gboolean available = FALSE;
if (g_once_init_enter (&once))
{
GModule *module = NULL;
module = g_module_open ("libdvdcss.so.2", G_MODULE_BIND_LOCAL);
if (module == NULL)
goto out;
if (!g_module_symbol (module, "dvdcss_open", (gpointer*) &dvdcss_open) || dvdcss_open == NULL)
goto out;
if (!g_module_symbol (module, "dvdcss_close", (gpointer*) &dvdcss_close) || dvdcss_close == NULL)
goto out;
if (!g_module_symbol (module, "dvdcss_seek", (gpointer*) &dvdcss_seek) || dvdcss_seek == NULL)
goto out;
if (!g_module_symbol (module, "dvdcss_read", (gpointer*) &dvdcss_read) || dvdcss_read == NULL)
goto out;
if (!g_module_symbol (module, "dvdcss_readv", (gpointer*) &dvdcss_readv) || dvdcss_readv == NULL)
goto out;
if (!g_module_symbol (module, "dvdcss_error", (gpointer*) &dvdcss_error) || dvdcss_error == NULL)
goto out;
available = TRUE;
out:
if (!available)
{
if (module != NULL)
g_module_close (module);
}
g_once_init_leave (&once, (gsize) 1);
}
return available;
}
/* ---------------------------------------------------------------------------------------------------- */
typedef struct Range
{
guint64 start;
guint64 end;
gboolean scrambled;
} Range;
static gint
range_compare_func (Range *a,
Range *b)
{
if (a->start > b->start)
return 1;
else if (a->start < b->start)
return -1;
return 0;
}
/* ---------------------------------------------------------------------------------------------------- */
struct GduDVDSupport
{
dvd_reader_t *dvd;
dvdcss_t dvdcss;
gboolean debug;
Range *ranges;
guint num_ranges;
Range *last_read_range;
};
/* ---------------------------------------------------------------------------------------------------- */
GduDVDSupport *
gdu_dvd_support_new (const gchar *device_file,
guint64 device_size)
{
GduDVDSupport *support = NULL;
guint title;
GList *scrambled_ranges = NULL;
GList *l;
guint64 pos;
GArray *a;
/* We use dlopen() to access libdvdcss since it's normally not
* shipped (so we can't hard-depend on it) but it may be installed
* on the user's system anyway
*/
if (!have_dvdcss ())
goto out;
support = g_new0 (GduDVDSupport, 1);
if (g_getenv ("GDU_DEBUG") != NULL)
support->debug = TRUE;
support->dvd = DVDOpen (device_file);
if (support->dvd == NULL)
goto fail;
support->dvdcss = dvdcss_open (device_file);
if (support->dvdcss == NULL)
goto fail;
/* It follows from "6.9.1 Constraints imposed on UDF by DVD-Video"
* of the OSTA UDF 2.60 spec (March 1, 2005) that
*
* o Video DVDs are using UDF 1.0.2
* o Only VOB files are encrypted
* o All VOB files of interest are in the VIDEO_TS/ directory
* o VOB files are at most 2^30 bytes = 1.0 GB
* o VOB files are a single extent.
* o The same key is used everywhere in a VOB files
*
* This means we can simply go through all VOB files in the
* VIDEO_TS/ directory and get their on-disc offset. Then for each
* file, we retrieve the CSS key at said offset. We then build a
* simple array of ranges
*
* {range_start, range_end, range_is_scrambled}
*
* that covers the entire disc. Then when we're reading we can
* consult this array to figure out when to change the key. Since
* keys are cached, no slowdown will happen.
*
* This approach was inspired by Brasero's dvdcss plug-in, see
*
* http://git.gnome.org/browse/brasero/tree/plugins/dvdcss/burn-dvdcss.c?id=BRASERO_3_6_0
*
* For the 'ls -l VIDEO_TS⁄*.VOB' part, we take advantage of the
* fact that VOB files are in a known format, e.g. title 0 is always
* VIDEO_TS.VOB and title 1 through 99 are always of the form
* VTS_NN_M.VOB where 01 <= N <= 99 and 0 <= M <= 9. This way we can
* simply use libdvdread's UDFFindFile() function on all 991
* possible filenames...
*
* See http://en.wikipedia.org/wiki/VOB for how VOB files work.
*/
for (title = 0; title <= 99; title++)
{
gint part;
Range *range;
for (part = 0; part <= 9; part++)
{
gchar vob_filename[64];
uint32_t vob_sector_offset;
uint32_t vob_size;
guint64 rounded_vob_size;
if (title == 0)
{
if (part > 0)
break;
snprintf (vob_filename, sizeof vob_filename, "/VIDEO_TS/VIDEO_TS.VOB");
}
else
{
snprintf (vob_filename, sizeof vob_filename, "/VIDEO_TS/VTS_%02d_%d.VOB", title, part);
}
vob_sector_offset = UDFFindFile (support->dvd, vob_filename, &vob_size);
if (vob_sector_offset == 0)
continue;
if (dvdcss_seek (support->dvdcss, vob_sector_offset, DVDCSS_SEEK_KEY) != (int) vob_sector_offset)
goto fail;
/* round up VOB size to nearest 2048-byte sector */
rounded_vob_size = vob_size + 0x7ff;
rounded_vob_size &= ~0x7ff;
range = g_new0 (Range, 1);
range->start = vob_sector_offset * 2048ULL;
range->end = range->start + rounded_vob_size;
range->scrambled = TRUE;
/*g_print ("%s: %10" G_GUINT64_FORMAT " -> %10" G_GUINT64_FORMAT ": scrambled=%d\n",
vob_filename, range->start, range->end, range->scrambled);*/
scrambled_ranges = g_list_prepend (scrambled_ranges, range);
}
}
/* If there are no VOB files on the disc, we don't need to decrypt - just bail */
if (scrambled_ranges == NULL)
goto fail;
/* Otherwise, build an array of ranges */
scrambled_ranges = g_list_sort (scrambled_ranges, (GCompareFunc) range_compare_func);
a = g_array_new (FALSE, /* zero-terminated */
FALSE, /* clear */
sizeof (Range));
pos = 0;
for (l = scrambled_ranges; l != NULL; l = l->next)
{
Range *range = l->data;
if (pos < range->start)
{
Range unscrambled_range = {0};
unscrambled_range.start = pos;
unscrambled_range.end = range->start;
g_array_append_val (a, unscrambled_range);
}
g_array_append_val (a, *range);
pos = range->end;
}
if (pos < device_size)
{
Range unscrambled_range = {0};
unscrambled_range.start = pos;
unscrambled_range.end = device_size;
g_array_append_val (a, unscrambled_range);
}
support->num_ranges = a->len;
support->ranges = (Range*) g_array_free (a, FALSE);
if (G_UNLIKELY (support->debug))
{
guint n;
for (n = 0; n < support->num_ranges; n++)
{
Range *range = support->ranges + n;
g_print ("range %02d: %10" G_GUINT64_FORMAT " -> %10" G_GUINT64_FORMAT ": scrambled=%d\n",
n, range->start, range->end, range->scrambled);
}
}
out:
g_list_free_full (scrambled_ranges, g_free);
return support;
fail:
gdu_dvd_support_free (support);
support = NULL;
goto out;
}
void
gdu_dvd_support_free (GduDVDSupport *support)
{
g_free (support->ranges);
if (support->dvdcss != NULL)
dvdcss_close (support->dvdcss);
if (support->dvd != NULL)
DVDClose (support->dvd);
g_free (support);
}
/* ---------------------------------------------------------------------------------------------------- */
gssize
gdu_dvd_support_read (GduDVDSupport *support,
int fd,
guchar *buffer,
guint64 offset,
guint64 size)
{
guint n;
gssize ret = -1;
guint64 cur_offset = offset;
guint64 num_left = size;
guchar *cur_buffer = buffer;
g_assert ((offset & 0x7ff) == 0);
/* First find the range we're in
*
* Since callers are typically looping sequentially over the entire
* data surface, first check if last used range still works
*/
if (support->last_read_range != NULL &&
offset >= support->last_read_range->start &&
offset < support->last_read_range->end)
{
n = support->last_read_range - support->ranges;
}
else
{
/* Otherwise look through all ranges, starting form the first */
for (n = 0; n < support->num_ranges; n++)
{
if (offset >= support->ranges[n].start &&
offset < support->ranges[n].end)
break;
}
}
/* Break the read request into multiple requests not crossing any of
* the ranges... we only want to use dvdcss_read() for the encrypted
* bits
*/
while (num_left > 0)
{
Range *r;
guint64 num_left_in_range;
guint64 num_to_read_in_range;
ssize_t num_bytes_read;
if (G_UNLIKELY (n == support->num_ranges))
{
g_warning ("Requested offset %" G_GUINT64_FORMAT " is out of range", offset);
ret = -1;
goto out;
}
r = support->ranges + n;
g_assert (cur_offset >= r->start && cur_offset < r->end);
num_left_in_range = r->end - cur_offset;
num_to_read_in_range = MIN (num_left_in_range, num_left);
if (G_UNLIKELY (support->debug))
{
g_print ("reading %" G_GUINT64_FORMAT " from %" G_GUINT64_FORMAT " (scrambled=%d)\n",
num_to_read_in_range, cur_offset, r->scrambled);
}
/* now read @num_to_read_in_range from @cur_offset into @cur_buffer */
if (r->scrambled)
{
int flags = 0;
int block_offset = cur_offset / 2048;
int num_blocks_to_request = num_to_read_in_range / 2048;
int num_blocks_read;
g_assert ((cur_offset & 0x7ff) == 0);
g_assert ((num_to_read_in_range & 0x7ff) == 0);
/* see if we need to change the key? */
if (support->last_read_range != r)
{
if (G_UNLIKELY (support->debug))
{
g_print ("setting CSS key at offset %" G_GUINT64_FORMAT "\n", cur_offset);
}
flags |= DVDCSS_SEEK_KEY;
support->last_read_range = r;
}
if (dvdcss_seek (support->dvdcss, block_offset, flags) != block_offset)
goto out;
dvdcss_read_again:
num_blocks_read = dvdcss_read (support->dvdcss,
cur_buffer,
num_blocks_to_request,
DVDCSS_READ_DECRYPT);
if (num_blocks_read < 0)
{
if (errno == EAGAIN || errno == EINTR)
goto dvdcss_read_again;
/* treat as partial read */
ret = size - num_left;
goto out;
}
if (num_blocks_read == 0)
{
/* treat as partial read */
ret = size - num_left;
goto out;
}
g_assert (num_blocks_read <= num_blocks_to_request);
num_bytes_read = num_blocks_read * 2048;
}
else
{
if (lseek (fd, cur_offset, SEEK_SET) == (off_t) -1)
goto out;
read_again:
num_bytes_read = read (fd, cur_buffer, num_to_read_in_range);
if (num_bytes_read < 0)
{
if (errno == EAGAIN || errno == EINTR)
goto read_again;
/* treat as partial read */
ret = size - num_left;
goto out;
}
if (num_bytes_read == 0)
{
/* treat as partial read */
ret = size - num_left;
goto out;
}
}
cur_offset += num_bytes_read;
cur_buffer += num_bytes_read;
num_left -= num_bytes_read;
/* the read could have been partial, in which case we're still in the same range */
if (cur_offset >= r->end)
n++;
}
ret = size - num_left;
out:
return ret;
}
/* ---------------------------------------------------------------------------------------------------- */
/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
*
* Copyright (C) 2008-2012 Red Hat, Inc.
*
* Licensed under GPL version 2 or later.
*
* Author: David Zeuthen <zeuthen@gmail.com>
*/
#ifndef __GDU_DVD_SUPPORT_H__
#define __GDU_DVD_SUPPORT_H__
#include <gtk/gtk.h>
#include "gdutypes.h"
G_BEGIN_DECLS
GduDVDSupport *gdu_dvd_support_new (const gchar *device_file,
guint64 device_size);
void gdu_dvd_support_free (GduDVDSupport *support);
gssize gdu_dvd_support_read (GduDVDSupport *support,
int fd,
guchar *buffer,
guint64 offset,
guint64 size);
G_END_DECLS
#endif /* __GDU_DVD_SUPPORT_H__ */
......@@ -40,6 +40,9 @@ typedef struct _GduPasswordStrengthWidget GduPasswordStrengthWidget;
struct _GduEstimator;
typedef struct _GduEstimator GduEstimator;
struct GduDVDSupport;
typedef struct GduDVDSupport GduDVDSupport;
G_END_DECLS
#endif /* __GDU_TYPES_H__ */
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment