diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ce4003a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "external/libdvdcss"] + path = external/libdvdcss + url = https://code.videolan.org/videolan/libdvdcss.git diff --git a/Cargo.toml b/Cargo.toml index 5120f2f..28df6aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,8 @@ publish = false # Workspace member crates # dvd = { path = "crates/dvd" } -libdvdcss = { path = "crates/libdvdcss" } +dvdcss-sys = { path = "crates/dvdcss-sys" } +dvdcss = { path = "crates/dvdcss" } mapping = { path = "crates/mapping" } media = { path = "crates/media" } plugins = { path = "crates/plugins" } diff --git a/crates/dvd/src/lib.rs b/crates/dvd/src/lib.rs deleted file mode 100644 index 8878a43..0000000 --- a/crates/dvd/src/lib.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::path::PathBuf; - -use media::Media; - -use crate::error::Error; - -pub type DVD = libdvdcss::DVD; - -// Probably wipe and restart this. -// Not thought out yet. - -impl TryFrom for DVD { - type Error = crate::Error; - - fn try_from(value: PathBuf) -> Result { - libdvdcss::DVD::new(value).ok_or(Error::LibDvdCssOpenError) - } -} - -impl Media for DVD {} - -mod error { - use derive_more::{Display, From}; - - pub type Result = std::result::Result; - - #[derive(Debug, From, Display)] - pub enum Error { - LibDvdCssOpenError, - } -} diff --git a/crates/dvd/Cargo.toml b/crates/dvdcss-sys/Cargo.toml similarity index 51% rename from crates/dvd/Cargo.toml rename to crates/dvdcss-sys/Cargo.toml index e2d785d..ecbd5b1 100644 --- a/crates/dvd/Cargo.toml +++ b/crates/dvdcss-sys/Cargo.toml @@ -1,13 +1,12 @@ [package] -name = "dvd" -version.workspace = true +name = "dvdcss-sys" +version = "0.1.0" edition.workspace = true authors.workspace = true repository.workspace = true license.workspace = true publish.workspace = true -[dependencies] -derive_more.workspace = true -libdvdcss.workspace = true -media.workspace = true +[build-dependencies] +bindgen = "0.72.1" +meson = "1.0" diff --git a/crates/dvdcss-sys/README.md b/crates/dvdcss-sys/README.md new file mode 100644 index 0000000..acdc72b --- /dev/null +++ b/crates/dvdcss-sys/README.md @@ -0,0 +1,3 @@ +# dvdcss-sys + +A crate for pure bindings against the system libdvdcss. diff --git a/crates/libdvdcss/build.rs b/crates/dvdcss-sys/build.rs similarity index 50% rename from crates/libdvdcss/build.rs rename to crates/dvdcss-sys/build.rs index c65ba31..34bc8e6 100644 --- a/crates/libdvdcss/build.rs +++ b/crates/dvdcss-sys/build.rs @@ -1,16 +1,25 @@ -use std::env; +use std::env::{self, current_dir}; use std::path::PathBuf; fn main() { - // Path from which to search for shared libraries. - // Is there no better (automated), platform-specific way? - println!("cargo:rustc-link-search=/usr/lib"); - // Locate and link the shared library. - println!("cargo:rustc-link-lib=libdvdcss"); + println!("cargo:rustc-link-lib=dvdcss"); + + // Because clang wants an absolute path and using canonicalize() fails. + let libdir_path = current_dir() + .expect("Cannot get CWD.") + .join("../../external/libdvdcss"); + + // Run the meson build to produce build/config.h + meson::build( + libdir_path.to_str().unwrap(), + libdir_path.join("build").to_str().unwrap(), + ); let bindings = bindgen::Builder::default() .header("src/wrapper.h") + .clang_arg(format!("-I{}/src", libdir_path.to_str().unwrap())) + .clang_arg(format!("-I{}/build", libdir_path.to_str().unwrap())) // Invalidate the build if a header has changed. .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) .generate() diff --git a/crates/libdvdcss/src/dvdcss.rs b/crates/dvdcss-sys/src/lib.rs similarity index 100% rename from crates/libdvdcss/src/dvdcss.rs rename to crates/dvdcss-sys/src/lib.rs diff --git a/crates/dvdcss-sys/src/wrapper.h b/crates/dvdcss-sys/src/wrapper.h new file mode 100644 index 0000000..3aaafa2 --- /dev/null +++ b/crates/dvdcss-sys/src/wrapper.h @@ -0,0 +1,2 @@ +#include "dvdcss/dvdcss.h" +#include "libdvdcss.h" diff --git a/crates/libdvdcss/Cargo.toml b/crates/dvdcss/Cargo.toml similarity index 71% rename from crates/libdvdcss/Cargo.toml rename to crates/dvdcss/Cargo.toml index 3e0fc7e..917fe51 100644 --- a/crates/libdvdcss/Cargo.toml +++ b/crates/dvdcss/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "libdvdcss" +name = "dvdcss" version = "0.1.0" edition.workspace = true authors.workspace = true @@ -10,6 +10,8 @@ publish.workspace = true [dependencies] derive_more.workspace = true semver.workspace = true +dvdcss-sys.workspace = true -[build-dependencies] -bindgen = "0.72.1" +[features] +semver = [] +encryption-type = [] diff --git a/crates/dvdcss/README.md b/crates/dvdcss/README.md new file mode 100644 index 0000000..17033a3 --- /dev/null +++ b/crates/dvdcss/README.md @@ -0,0 +1,11 @@ +# libdvdcss Rust Wrapper + +This crate assumes a baseline version of `libdvdcss` 1.5.0. + +If you wish to use the `dvdcss_get_encryption_type()` function introduced in +`libdvdcss` version 1.6.0, add the `encryption-type` feature. + +For convenience, the `semver` feature can be added for a `semver::Version` +representation of `DVDCSS_VERSION` instead of a tuple. + +`dvdcss_open_stream()` is not implemented. At least not yet. diff --git a/crates/dvdcss/src/lib.rs b/crates/dvdcss/src/lib.rs new file mode 100644 index 0000000..690c017 --- /dev/null +++ b/crates/dvdcss/src/lib.rs @@ -0,0 +1,180 @@ +use std::ffi::{CStr, c_int, c_void}; +use std::io::{self, Error, Read, Seek, SeekFrom}; +use std::path::Path; + +use derive_more::From; +use dvdcss_sys::*; + +pub const BLOCK_SIZE: u32 = DVDCSS_BLOCK_SIZE; + +#[cfg(feature = "semver")] +pub const DVDCSS_VERSION: semver::Version = semver::Version::new( + DVDCSS_VERSION_MAJOR as u64, + DVDCSS_VERSION_MINOR as u64, + DVDCSS_VERSION_MICRO as u64, +); + +#[cfg(not(feature = "semver"))] +pub const DVDCSS_VERSION: (u32, u32, u32) = ( + DVDCSS_VERSION_MAJOR, + DVDCSS_VERSION_MINOR, + DVDCSS_VERSION_MICRO, +); + +#[repr(u32)] +#[derive(Debug, Default)] +pub enum SeekFlag { + #[default] + None = DVDCSS_NOFLAGS, + Key = DVDCSS_SEEK_KEY, + Mpeg = DVDCSS_SEEK_MPEG, +} + +// I dislike this as technically, 2 should be Unknown/Reserved and 3 as CPRM. +#[repr(i32)] +#[derive(Debug, Default, From)] +pub enum EncryptionScheme { + #[default] + None = 0, + CssCppm = 1, + //Reserved, + Cprm = 2, + Unknown, +} + +pub struct Dvd { + ptr: dvdcss_t, + decrypt: bool, +} + +impl Dvd { + /// Open a DVD. + /// If libdvdcss returns a null pointer, this returns None. + /// No more about the error can be discerned than this. + pub fn open>(src: P) -> Option { + // Cast the path to a CStr (*const char), + // returning early if there's any issue. + let c_path = CStr::from_bytes_with_nul(src.as_ref().as_os_str().as_encoded_bytes()) + .ok()? + .as_ptr(); + + let ptr = unsafe { dvdcss_open(c_path) }; + + if ptr.is_null() { + None + } else { + Some(Self { + ptr, + decrypt: false, + }) + } + } + + /// Create a `Dvd` from a raw pointer to `dvdcss_t`. + pub unsafe fn from_raw_pointer(ptr: dvdcss_t) -> Self { + Self { + ptr, + decrypt: false, + } + } + + pub fn decrypt(&mut self, decrypt: bool) { + self.decrypt = decrypt; + } + + /// Return the error message string from dvdcss_error + pub fn error(&self) -> &CStr { + unsafe { CStr::from_ptr(dvdcss_error(self.ptr)) } + } + + /// Return the error message string from dvdcss_error with lossy conversion. + pub fn error_lossy(&self) -> String { + self.error().to_string_lossy().to_string() + } + + /// Detect whether or not the content is scrambled. + pub fn is_scrambled(&self) -> bool { + match unsafe { dvdcss_is_scrambled(self.ptr) } { + 0 => false, + 1 => true, + _ => unreachable!(), + } + } + + /// The length in bytes of the Dvd. + pub fn len(&self) { + self.ptr.i_fd + } + + /// Return the encryption scheme in use. + #[cfg(feature = "encryption-type")] + pub fn encryption_type(&self) -> EncryptionScheme { + unsafe { dvdcss_get_encryption_type(self.ptr) } + .try_into() + .unwrap_or_default() + } +} + +impl Read for Dvd { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let i_flags = if self.decrypt { + DVDCSS_READ_DECRYPT + } else { + DVDCSS_NOFLAGS + } as i32; + + let retval = unsafe { + dvdcss_read( + self.ptr, + buf.as_mut_ptr() as *mut c_void, + buf.len() as i32 / BLOCK_SIZE as c_int, + i_flags, + ) + }; + + if retval < 0 { + Err(Error::last_os_error()) + } else { + Ok(retval.unsigned_abs() as usize) + } + } +} + +impl Seek for Dvd { + /// Seek to some posititon in the disc. + /// + /// Contrary to Rust's standard for Seek implementations, + /// this is limited to seeking in multiples of BLOCK_SIZE (2048), + /// as defined by `dvdcss_seek()`. + fn seek(&mut self, pos: io::SeekFrom) -> io::Result { + // dvdcss_seek reports in blocks, rather than bytes as expected by Rust. + // + // As for what purpose the flags serve... I haven't a clue. + + let bytes = match pos { + SeekFrom::Start(p) => p as i64, + SeekFrom::Current(p) => self.stream_position()? as i64 + p, + SeekFrom::End(p) => self.stream_len()? as i64 - p, + } * BLOCK_SIZE as i64; + + // Although this call generally returns a negative value on fail, as of writing + // it can only return -1, so there's no point in expanding this fail result into + // a proper enum. + let retval = unsafe { dvdcss_seek(self.ptr, bytes as c_int, SeekFlag::None as c_int) }; + + if retval < 0 { + Err(Error::last_os_error()) + } else { + Ok((retval.unsigned_abs() * BLOCK_SIZE) as u64) + } + } +} + +impl Drop for Dvd { + fn drop(&mut self) { + // Right... so drop can't handle return values. + // So... best option is ...panic? + let retval = unsafe { dvdcss_close(self.ptr) }; + assert!(retval == 0); + } +} diff --git a/crates/libdvdcss/README.md b/crates/libdvdcss/README.md deleted file mode 100644 index 6412f3b..0000000 --- a/crates/libdvdcss/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# libdvdcss Rust Wrapper - -Minimum `libdvdcss` version: 1.6.0 diff --git a/crates/libdvdcss/src/error.rs b/crates/libdvdcss/src/error.rs deleted file mode 100644 index 5d46294..0000000 --- a/crates/libdvdcss/src/error.rs +++ /dev/null @@ -1,17 +0,0 @@ -use derive_more::{Display, From}; - -pub type Result = std::result::Result; - -#[derive(Debug, From, Display)] -pub enum Error { - #[display("I32ToEnumCastError {{ {}, {} }}", value, enum_)] - I32ToEnumCastError { - value: i32, - enum_: String, - }, - InsufficientBufferLength, - ReadError { - value: i32, - }, - SeekError, -} diff --git a/crates/libdvdcss/src/lib.rs b/crates/libdvdcss/src/lib.rs deleted file mode 100644 index 76dca2a..0000000 --- a/crates/libdvdcss/src/lib.rs +++ /dev/null @@ -1,162 +0,0 @@ -use std::ffi::{CStr, c_int, c_void}; -use std::path::Path; - -use crate::error::{Error, Result}; - -mod dvdcss; -mod error; - -use dvdcss::*; -use semver::Version; - -pub const BLOCK_SIZE: u32 = DVDCSS_BLOCK_SIZE; -pub const DVDCSS_VERSION: semver::Version = Version::new( - DVDCSS_VERSION_MAJOR as u64, - DVDCSS_VERSION_MINOR as u64, - DVDCSS_VERSION_MICRO as u64, -); - -#[repr(u32)] -#[derive(Debug, Default)] -pub enum SeekFlag { - #[default] - None = DVDCSS_NOFLAGS, - Key = DVDCSS_SEEK_KEY, - Mpeg = DVDCSS_SEEK_MPEG, -} - -// I dislike this as technically, 2 should be Unknown/Reserved and 3 as CPRM. -#[repr(i32)] -#[derive(Debug, Default)] -pub enum EncryptionScheme { - #[default] - None = 0, - CssCppm = 1, - //Reserved, - Cprm = 2, - Unknown, -} - -impl TryFrom for EncryptionScheme { - type Error = Error; - - fn try_from(value: i32) -> std::result::Result { - if !(0..=2).contains(&value) { - return Err(Error::I32ToEnumCastError { - value, - enum_: "EncryptionScheme".to_string(), - }); - } - - Ok(match value { - 0 => Self::None, - 1 => Self::CssCppm, - 2 => Self::Cprm, - _ => unreachable!(), - }) - } -} - -#[repr(transparent)] -pub struct DVD { - ptr: dvdcss_t, -} - -impl DVD { - pub fn new>(src: P) -> Option { - // Cast the path to a CStr (*const char), returning early if there's any issue. - let c_path = CStr::from_bytes_with_nul(src.as_ref().as_os_str().as_encoded_bytes()) - .ok()? - .as_ptr(); - - // Make the FFI call. - let ptr = unsafe { dvdcss_open(c_path) }; - - if ptr.is_null() { - None - } else { - Some(Self { ptr }) - } - } - - // Seek in blocks (See: `BLOCK_SIZE`). - pub fn seek(&self, blocks: u32, flag: SeekFlag) -> Result { - // Although this call generally returns a negative value on fail, as of writing - // it can only return -1, so there's no point in expanding this fail result into - // a proper enum. - let retval = unsafe { dvdcss_seek(self.ptr, blocks as c_int, flag as c_int) }; - - if retval < 0 { - Err(Error::SeekError) - } else { - Ok(retval as u32) - } - } - - pub fn read(&self, buf: &mut [u8], blocks: u32, decrypt: bool) -> Result { - // If the user has provided a wonk buffer size that's not a multiple of BLOCK_SIZE, - // I'm not fixing it for them. - // But ensure that buffer is big enough to safely read the data. - if buf.len() < (blocks * BLOCK_SIZE) as usize { - return Err(Error::InsufficientBufferLength); - } - - let i_flags = if decrypt { - DVDCSS_READ_DECRYPT - } else { - DVDCSS_NOFLAGS - } as i32; - - let retval = unsafe { - dvdcss_read( - self.ptr, - buf.as_mut_ptr() as *mut c_void, - blocks as c_int, - i_flags, - ) - }; - - if retval < 0 { - // I can't track back the error return values for pf_read, - // so this is good enough for now. - return Err(Error::ReadError { value: retval }); - } - - Ok(retval as u32) - } - - // Return the error message string from dvdcss_error - pub fn error(&self) -> &CStr { - unsafe { CStr::from_ptr(dvdcss_error(self.ptr)) } - } - - // Return the error message string from dvdcss_error with lossy conversion. - pub fn error_lossy(&self) -> String { - self.error().to_string_lossy().to_string() - } - - pub fn is_scrambled(&self) -> bool { - match unsafe { dvdcss_is_scrambled(self.ptr) } { - 0 => false, - 1 => true, - _ => unreachable!(), - } - } - - /* - pub fn encryption_type(&self) -> EncryptionScheme { - unsafe { dvdcss_get_encryption_type(self.ptr) } - .try_into() - .unwrap_or_default() - } - */ -} - -impl Drop for DVD { - fn drop(&mut self) { - // Right... so drop can't handle return values. - // So... best option is ...panic? - let retval = unsafe { dvdcss_close(self.ptr) }; - assert!(retval == 0); - } -} diff --git a/crates/libdvdcss/src/wrapper.h b/crates/libdvdcss/src/wrapper.h deleted file mode 100644 index e5d92ea..0000000 --- a/crates/libdvdcss/src/wrapper.h +++ /dev/null @@ -1 +0,0 @@ -#include diff --git a/external/libdvdcss b/external/libdvdcss new file mode 160000 index 0000000..2682a4a --- /dev/null +++ b/external/libdvdcss @@ -0,0 +1 @@ +Subproject commit 2682a4a7ed782e700a5b920f6f85c4f9736921c3