diff --git a/CMakeLists.txt b/CMakeLists.txt index b3908ca..5cf1808 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,8 +73,10 @@ ENDIF() # choose platform dependent source files IF(libdiscid_OS MATCHES "win32") - SET(libdiscid_OSDEP_SRCS src/toc.c src/disc_win32.c) + SET(libdiscid_OSDEP_SRCS src/toc.c src/disc_win32.c src/scsi.c) SET(libdiscid_RCS ${CMAKE_CURRENT_BINARY_DIR}/versioninfo.rc) +ELSEIF(libdiscid_OS MATCHES "linux") + SET(libdiscid_OSDEP_SRCS src/toc.c src/unix.c src/disc_linux.c src/scsi.c) ELSEIF(libdiscid_OS MATCHES "darwin") SET(libdiscid_OSDEP_SRCS src/toc.c src/unix.c src/disc_darwin.c) FIND_LIBRARY(COREFOUNDATION_LIBRARY CoreFoundation) diff --git a/HACKING b/HACKING new file mode 100644 index 0000000..6b8a0e3 --- /dev/null +++ b/HACKING @@ -0,0 +1,20 @@ +Libdiscid supports many platforms and the disc access code +for every platform is different. +Since you need physical machines with a CD drive to test a platform, +we do need your help! + +The platform specific code is in the src/disc_*.c files. +These have to implement the *_unportable functions given +in include/discid/discid_private.h +The main function being mb_disc_read_unportable, +where the mb_disc_private struct is filled with data. +Parts of the data are optional features that don't need to be +implemented (ISRC, MCN). The TOC always has to be read. + +You are free on how to implement the features defined in discid.h, +but it might help for many advanced features +when mb_scsi_cmd_unportable is implemented on the platform. +After this is done, the scsi functions available in src/scsi.h +can be used. +These functions are implemented as scsi commands, +which in itself should be platform independent. diff --git a/Makefile.am b/Makefile.am index 50704e4..139676e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -34,7 +34,7 @@ pc_DATA = libdiscid.pc discid_incdir = $(includedir)/discid discid_inc_HEADERS = include/discid/discid.h noinst_HEADERS = include/discid/discid_private.h src/base64.h src/sha1.h -noinst_HEADERS += test/test.h src/unix.h src/ntddcdrm.h +noinst_HEADERS += test/test.h src/scsi.h src/unix.h src/ntddcdrm.h if RUN_TESTS @@ -91,7 +91,7 @@ if OS_GENERIC libdiscid_la_SOURCES += src/disc_generic.c endif if OS_LINUX -libdiscid_la_SOURCES += src/toc.c src/unix.c src/disc_linux.c +libdiscid_la_SOURCES += src/toc.c src/scsi.c src/unix.c src/disc_linux.c endif if OS_NETBSD libdiscid_la_SOURCES += src/toc.c src/unix.c src/disc_netbsd.c @@ -103,7 +103,7 @@ if OS_SOLARIS libdiscid_la_SOURCES += src/toc.c src/unix.c src/disc_solaris.c endif if OS_WIN32 -libdiscid_la_SOURCES += src/toc.c src/disc_win32.c versioninfo.rc +libdiscid_la_SOURCES += src/toc.c src/scsi.c src/disc_win32.c versioninfo.rc endif diff --git a/include/discid/discid_private.h b/include/discid/discid_private.h index d3715b5..180eda6 100644 --- a/include/discid/discid_private.h +++ b/include/discid/discid_private.h @@ -136,4 +136,12 @@ LIBDISCID_INTERNAL int mb_disc_has_feature_unportable(enum discid_feature featur */ LIBDISCID_INTERNAL int mb_disc_load_toc(mb_disc_private *disc, mb_disc_toc *toc); +/* + * This is the code for discid_get_track_length(), implemented in disc.c. + * On Windows we can't use any LIBDISCID_API functions outside of disc.c, + * so we have this workaround for scsi.c usage + */ +LIBDISCID_INTERNAL int mb_disc_get_track_length(mb_disc_private *disc, + int track_num); + #endif /* MUSICBRAINZ_DISC_ID_PRIVATE_H */ diff --git a/src/disc.c b/src/disc.c index 391b7cf..7f22ea3 100644 --- a/src/disc.c +++ b/src/disc.c @@ -287,10 +287,8 @@ int discid_get_track_length(DiscId *d, int i) { if (!disc->success || !TRACK_NUM_IS_VALID(disc, i)) return -1; - else if (i < disc->last_track_num) - return disc->track_offsets[i+1] - disc->track_offsets[i]; else - return disc->track_offsets[0] - disc->track_offsets[i]; + return mb_disc_get_track_length(disc, i); } char *discid_get_mcn(DiscId *d) { @@ -356,6 +354,16 @@ char *discid_get_version_string(void) { * ****************************************************************************/ +/* This is used in scsi.c, which can't use discid_get_track_length + * on Windows due to declspec(dllexport) + */ +int mb_disc_get_track_length(mb_disc_private *disc, int i) { + if (i < disc->last_track_num) + return disc->track_offsets[i+1] - disc->track_offsets[i]; + else + return disc->track_offsets[0] - disc->track_offsets[i]; +} + /* * Create a DiscID based on the TOC data found in the DiscId object. * The DiscID is placed in the provided string buffer. diff --git a/src/disc_linux.c b/src/disc_linux.c index a06e4be..4865309 100644 --- a/src/disc_linux.c +++ b/src/disc_linux.c @@ -33,14 +33,18 @@ #include #include #include +#include #include #include "discid/discid.h" #include "discid/discid_private.h" +#include "scsi.h" #include "unix.h" +#define MB_DEFAULT_DEVICE "/dev/cdrom" + /* timeout better shouldn't happen for scsi commands -> device is reset */ #define DEFAULT_TIMEOUT 30000 /* in ms */ @@ -58,6 +62,7 @@ #endif static THREAD_LOCAL char default_device[MAX_DEV_LEN] = ""; +static THREAD_LOCAL int feature_warning_printed = 0; static int get_device(int number, char *device, int device_len) { @@ -110,6 +115,7 @@ static int get_device(int number, char *device, int device_len) { return return_value; } + int mb_disc_unix_read_toc_header(int fd, mb_disc_toc *toc) { struct cdrom_tochdr th; @@ -171,10 +177,12 @@ void mb_disc_unix_read_mcn(int fd, mb_disc_private *disc) { } /* Send a scsi command and receive data. */ -static int scsi_cmd(int fd, unsigned char *cmd, int cmd_len, - unsigned char *data, int data_len) { +mb_scsi_status mb_scsi_cmd_unportable(mb_scsi_handle handle, + unsigned char *cmd, int cmd_len, + unsigned char *data, int data_len) { unsigned char sense_buffer[SG_MAX_SENSE]; /* for "error situations" */ sg_io_hdr_t io_hdr; + int return_value; memset(&io_hdr, 0, sizeof io_hdr); @@ -192,53 +200,50 @@ static int scsi_cmd(int fd, unsigned char *cmd, int cmd_len, io_hdr.dxfer_len = data_len; io_hdr.dxfer_direction = SG_DXFER_FROM_DEV; - if (ioctl(fd, SG_IO, &io_hdr) != 0) { - return errno; + return_value = ioctl(handle.fd, SG_IO, &io_hdr); + + if (return_value == -1) { + /* failure */ + fprintf(stderr, "ioctl error: %d\n", errno); + return IO_ERROR; } else { - return io_hdr.status; /* 0 = success */ + /* check for potenticall informative success codes + * 1 seems to be what is mostly returned */ + if (return_value != 0) { + fprintf(stderr, "ioctl return value: %d\n", + return_value); + /* no error, but possibly informative */ + } + + /* check scsi status */ + if (io_hdr.masked_status != GOOD) { + fprintf(stderr, "scsi status: %d\n", io_hdr.status); + return STATUS_ERROR; + } else if (data_len > 0 && io_hdr.resid == data_len) { + /* not receiving data, when requested */ + fprintf(stderr, "data requested, but none returned\n"); + return NO_DATA_RETURNED; + } else { + return SUCCESS; + } } } void mb_disc_unix_read_isrc(int fd, mb_disc_private *disc, int track_num) { - int i; - unsigned char cmd[10]; - unsigned char data[24]; - char buffer[ISRC_STR_LENGTH+1]; - - memset(cmd, 0, sizeof cmd); - memset(data, 0, sizeof data); - memset(buffer, 0, sizeof buffer); - - /* data read from the last appropriate sector encountered - * by a current or previous media access operation. - * The Logical Unit accesses the media when there is/was no access. - * TODO: force access at a specific block? -> no duplicate ISRCs? - */ - cmd[0] = 0x42; /* READ SUB-CHANNEL */ - /* cmd[1] reserved / MSF bit (unused) */ - cmd[2] = 1 << 6; /* 6th bit set (SUBQ) -> get sub-channel data */ - cmd[3] = 0x03; /* get ISRC (ADR 3, Q sub-channel Mode-3) */ - /* 4+5 reserved */ - cmd[6] = track_num; - /* cmd[7] = upper byte of the transfer length */ - cmd[8] = sizeof data; /* transfer length in bytes (4 header, 20 data)*/ - /* cmd[9] = control byte */ - - if (scsi_cmd(fd, cmd, sizeof cmd, data, sizeof data) != 0) { - fprintf(stderr, "Warning: Cannot get ISRC code for track %d\n", - track_num); - return; - } - - /* data[1:4] = sub-q channel data header (audio status, data length) */ - if (data[8] & (1 << 7)) { /* TCVAL is set -> ISRCs valid */ - for (i = 0; i < ISRC_STR_LENGTH; i++) { - buffer[i] = data[9 + i]; + mb_scsi_handle handle; + mb_scsi_features features; + memset(&handle, 0, sizeof handle); + handle.fd = fd; + features = mb_scsi_get_features(handle); + if (features.raw_isrc) { + mb_scsi_read_track_isrc_raw(handle, disc, track_num); + } else { + if (!feature_warning_printed) { + fprintf(stderr, "Warning: raw ISRCs not available, using ISRCs given by subchannel read\n"); + feature_warning_printed = 1; } - buffer[ISRC_STR_LENGTH] = 0; - strncpy(disc->isrc[track_num], buffer, ISRC_STR_LENGTH); + mb_scsi_read_track_isrc(handle, disc, track_num); } - /* data[21:23] = zero, AFRAME, reserved */ } int mb_disc_has_feature_unportable(enum discid_feature feature) { diff --git a/src/disc_win32.c b/src/disc_win32.c index 4a13b60..b041b16 100644 --- a/src/disc_win32.c +++ b/src/disc_win32.c @@ -25,25 +25,35 @@ #define _CRT_SECURE_NO_WARNINGS #endif -#include -#include #include +#include +#include +#include #if defined(__CYGWIN__) #include +#include #elif defined(__MINGW32__) #include +#include #else #include "ntddcdrm.h" +#include "ntddscsi.h" #endif #include "discid/discid.h" #include "discid/discid_private.h" +#include "scsi.h" #define MB_DEFAULT_DEVICE "D:" #define MAX_DEV_LEN 3 +/* after that time a scsi command is considered timed out */ +#define DEFAULT_TIMEOUT 30 /* in seconds */ + +#define GOOD 0x00 /* scsi status code for success */ + #if defined(_MSC_VER) # define THREAD_LOCAL __declspec(thread) #elif (defined(__GNUC__) && (__GNUC__ >= 4)) || defined(__clang__) @@ -72,9 +82,13 @@ static HANDLE create_device_handle(mb_disc_private *disc, const char *device) { } strncat(filename, device, len > 120 ? 120 : len); - hDevice = CreateFile(filename, GENERIC_READ, + /* We are not actually "writing" to the device, + * but we are sending scsi commands with raw ISRCs, + * which needs GENERIC_WRITE. + */ + hDevice = CreateFile(filename, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, - NULL, OPEN_EXISTING, 0, NULL); + NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hDevice == INVALID_HANDLE_VALUE) { snprintf(disc->error_msg, MB_ERROR_MSG_LENGTH, "cannot open the CD audio device '%s'", device); @@ -199,10 +213,13 @@ static int mb_disc_winnt_read_toc(HANDLE device, mb_disc_private *disc, mb_disc_ int mb_disc_read_unportable(mb_disc_private *disc, const char *device, unsigned int features) { mb_disc_toc toc; + mb_scsi_handle handle; + mb_scsi_features scsi_features; char tmpDevice[MAX_DEV_LEN]; - HANDLE hDevice; int i, device_number; + memset(&handle, 0, sizeof handle); + device_number = (int) strtol(device, NULL, 10); if (device_number > 0) { @@ -214,32 +231,107 @@ int mb_disc_read_unportable(mb_disc_private *disc, const char *device, device = tmpDevice; } - hDevice = create_device_handle(disc, device); - if (hDevice == 0) + handle.hDevice = create_device_handle(disc, device); + if (handle.hDevice == 0) return 0; - if (!mb_disc_winnt_read_toc(hDevice, disc, &toc)) { - CloseHandle(hDevice); + if (!mb_disc_winnt_read_toc(handle.hDevice, disc, &toc)) { + CloseHandle(handle.hDevice); return 0; } if (!mb_disc_load_toc(disc, &toc)) { - CloseHandle(hDevice); + CloseHandle(handle.hDevice); return 0; } if (features & DISCID_FEATURE_MCN) { - read_disc_mcn(hDevice, disc); + read_disc_mcn(handle.hDevice, disc); } - for (i = disc->first_track_num; i <= disc->last_track_num; i++) { - if (features & DISCID_FEATURE_ISRC) { - read_disc_isrc(hDevice, disc, i); + if (features & DISCID_FEATURE_ISRC) { + /* test for scsi features */ + scsi_features = mb_scsi_get_features(handle); + if (!scsi_features.raw_isrc) { + fprintf(stderr, "Warning: raw ISRCs not available, using ISRCs given by subchannel read\n"); + } + if (!scsi_features.isrc) { + fprintf(stderr, "WARNING: can't read subchannel data!\n"); + } + + /* read ISRCs with the best method available */ + for (i = disc->first_track_num; i <= disc->last_track_num;i++) { + if (scsi_features.raw_isrc) { + mb_scsi_read_track_isrc_raw(handle, disc, i); + } else if (scsi_features.isrc) { + mb_scsi_read_track_isrc(handle, disc, i); + } else { + read_disc_isrc(handle.hDevice, disc, i); + } } } - CloseHandle(hDevice); + CloseHandle(handle.hDevice); return 1; } +mb_scsi_status mb_scsi_cmd_unportable(mb_scsi_handle handle, + unsigned char *cmd, int cmd_len, + unsigned char *data, int data_len) { + SCSI_PASS_THROUGH_DIRECT sptd; + DWORD bytes_returned = 0; + int return_value; + + memset(&sptd, 0, sizeof sptd); + sptd.Length = sizeof(SCSI_PASS_THROUGH_DIRECT); + sptd.DataIn = SCSI_IOCTL_DATA_IN; + sptd.TimeOutValue = DEFAULT_TIMEOUT; + sptd.DataBuffer = data; /* a pointer */ + sptd.DataTransferLength = data_len; + sptd.CdbLength = cmd_len; + + /* The command is a buffer, not a pointer. + * So we have to copy our buffer. + * The size of this buffer is not documented in MSDN, + * but in the include file defined as uchar[16]. + */ + assert(cmd_len <= sizeof sptd.Cdb); + memcpy(sptd.Cdb, cmd, cmd_len); + + /* the sptd struct is used for input and output -> listed twice + * We don't use bytes_returned, but this cannot be NULL in this case */ + return_value = DeviceIoControl(handle.hDevice, + IOCTL_SCSI_PASS_THROUGH_DIRECT, + &sptd, sizeof sptd, &sptd, sizeof sptd, + &bytes_returned, NULL); + + if (return_value == 0) { + /* failure */ + fprintf(stderr, "DeviceIoControl error: %ld\n", GetLastError()); + return IO_ERROR; + } else { + /* success of DeviceIoControl */ + + /* check for potentially informative success codes + * 1 seems to be what is mostly returned */ + if (return_value != 1) { + fprintf(stderr, "DeviceIoControl return value: %d\n", + return_value); + /* no actual error, but possibly informative */ + } + + /* check scsi status */ + if (sptd.ScsiStatus != GOOD) { + fprintf(stderr, "scsi status: %d\n", sptd.ScsiStatus); + return STATUS_ERROR; + } else if (data_len > 0 && bytes_returned == 0) { + /* not receiving data, when requested */ + fprintf(stderr, "data requested, but none returned\n"); + return NO_DATA_RETURNED; + } else { + return SUCCESS; + } + } +} + /* EOF */ diff --git a/src/ntddscsi.h b/src/ntddscsi.h new file mode 100644 index 0000000..e071324 --- /dev/null +++ b/src/ntddscsi.h @@ -0,0 +1,174 @@ +/* + * ntddscsi.h + * + * SCSI port IOCTL interface. + * + * This file is part of the w32api package. + * + * Contributors: + * Created by Casper S. Hornstrup + * + * THIS SOFTWARE IS NOT COPYRIGHTED + * + * This source code is offered for use in the public domain. You may + * use, modify or distribute it freely. + * + * This code is distributed in the hope that it will be useful but + * WITHOUT ANY WARRANTY. ALL WARRANTIES, EXPRESS OR IMPLIED ARE HEREBY + * DISCLAIMED. This includes but is not limited to warranties of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + */ + +#ifndef __NTDDSCSI_H +#define __NTDDSCSI_H + +#if __GNUC__ >=3 +#pragma GCC system_header +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#pragma pack(push,4) + +//#include "ntddk.h" + +#define DD_SCSI_DEVICE_NAME "\\Device\\ScsiPort" +#define DD_SCSI_DEVICE_NAME_U L"\\Device\\ScsiPort" + +#define IOCTL_SCSI_BASE FILE_DEVICE_CONTROLLER + +#define IOCTL_SCSI_GET_INQUIRY_DATA \ + CTL_CODE(IOCTL_SCSI_BASE, 0x0403, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_SCSI_GET_CAPABILITIES \ + CTL_CODE(IOCTL_SCSI_BASE, 0x0404, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_SCSI_GET_ADDRESS \ + CTL_CODE(IOCTL_SCSI_BASE, 0x0406, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_SCSI_MINIPORT \ + CTL_CODE(IOCTL_SCSI_BASE, 0x0402, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS) + +#define IOCTL_SCSI_PASS_THROUGH \ + CTL_CODE(IOCTL_SCSI_BASE, 0x0401, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS) + +#define IOCTL_SCSI_PASS_THROUGH_DIRECT \ + CTL_CODE(IOCTL_SCSI_BASE, 0x0405, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS) + +#define IOCTL_SCSI_RESCAN_BUS \ + CTL_CODE(IOCTL_SCSI_BASE, 0x0407, METHOD_BUFFERED, FILE_ANY_ACCESS) + + +DEFINE_GUID(ScsiRawInterfaceGuid, 0x53f56309L, 0xb6bf, 0x11d0, 0x94, 0xf2, 0x00, 0xa0, 0xc9, 0x1e, 0xfb, 0x8b); + +DEFINE_GUID(WmiScsiAddressGuid, 0x53f5630fL, 0xb6bf, 0x11d0, 0x94, 0xf2, 0x00, 0xa0, 0xc9, 0x1e, 0xfb, 0x8b); + +typedef struct _SCSI_PASS_THROUGH { + USHORT Length; + UCHAR ScsiStatus; + UCHAR PathId; + UCHAR TargetId; + UCHAR Lun; + UCHAR CdbLength; + UCHAR SenseInfoLength; + UCHAR DataIn; + ULONG DataTransferLength; + ULONG TimeOutValue; + ULONG_PTR DataBufferOffset; + ULONG SenseInfoOffset; + UCHAR Cdb[16]; +} SCSI_PASS_THROUGH, *PSCSI_PASS_THROUGH; + +typedef struct _SCSI_PASS_THROUGH_DIRECT { + USHORT Length; + UCHAR ScsiStatus; + UCHAR PathId; + UCHAR TargetId; + UCHAR Lun; + UCHAR CdbLength; + UCHAR SenseInfoLength; + UCHAR DataIn; + ULONG DataTransferLength; + ULONG TimeOutValue; + PVOID DataBuffer; + ULONG SenseInfoOffset; + UCHAR Cdb[16]; +} SCSI_PASS_THROUGH_DIRECT, *PSCSI_PASS_THROUGH_DIRECT; + +typedef struct _SRB_IO_CONTROL { + ULONG HeaderLength; + UCHAR Signature[8]; + ULONG Timeout; + ULONG ControlCode; + ULONG ReturnCode; + ULONG Length; +} SRB_IO_CONTROL, *PSRB_IO_CONTROL; + +typedef struct _SCSI_ADDRESS { + ULONG Length; + UCHAR PortNumber; + UCHAR PathId; + UCHAR TargetId; + UCHAR Lun; +} SCSI_ADDRESS, *PSCSI_ADDRESS; + +typedef struct _SCSI_BUS_DATA { + UCHAR NumberOfLogicalUnits; + UCHAR InitiatorBusId; + ULONG InquiryDataOffset; +}SCSI_BUS_DATA, *PSCSI_BUS_DATA; + +typedef struct _SCSI_ADAPTER_BUS_INFO { + UCHAR NumberOfBuses; + SCSI_BUS_DATA BusData[1]; +} SCSI_ADAPTER_BUS_INFO, *PSCSI_ADAPTER_BUS_INFO; + +typedef struct _IO_SCSI_CAPABILITIES { + ULONG Length; + ULONG MaximumTransferLength; + ULONG MaximumPhysicalPages; + ULONG SupportedAsynchronousEvents; + ULONG AlignmentMask; + BOOLEAN TaggedQueuing; + BOOLEAN AdapterScansDown; + BOOLEAN AdapterUsesPio; +} IO_SCSI_CAPABILITIES, *PIO_SCSI_CAPABILITIES; + +typedef struct _SCSI_INQUIRY_DATA { + UCHAR PathId; + UCHAR TargetId; + UCHAR Lun; + BOOLEAN DeviceClaimed; + ULONG InquiryDataLength; + ULONG NextInquiryDataOffset; + UCHAR InquiryData[1]; +} SCSI_INQUIRY_DATA, *PSCSI_INQUIRY_DATA; + +#define SCSI_IOCTL_DATA_OUT 0 +#define SCSI_IOCTL_DATA_IN 1 +#define SCSI_IOCTL_DATA_UNSPECIFIED 2 + +/* +typedef struct _DUMP_POINTERS { + PADAPTER_OBJECT AdapterObject; + PVOID MappedRegisterBase; + PVOID DumpData; + PVOID CommonBufferVa; + LARGE_INTEGER CommonBufferPa; + ULONG CommonBufferSize; + BOOLEAN AllocateCommonBuffers; + BOOLEAN UseDiskDump; + UCHAR Spare1[2]; + PVOID DeviceObject; +} DUMP_POINTERS, *PDUMP_POINTERS; +*/ +#pragma pack(pop) + +#ifdef __cplusplus +} +#endif + +#endif /* __NTDDSCSI_H */ diff --git a/src/scsi.c b/src/scsi.c new file mode 100644 index 0000000..424a669 --- /dev/null +++ b/src/scsi.c @@ -0,0 +1,455 @@ +/* -------------------------------------------------------------------------- + + MusicBrainz -- The Internet music metadatabase + + Copyright (C) 2013 Johannes Dewender + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + +--------------------------------------------------------------------------- */ + +/* + * These commands are standard SCSI commands defined by INCITS T10 + * SPC: primary commands + * MMC: multimedia commands + * + * The specs can be found on the net (t10.org, needs registration) + * The Seagate SCSI Commands Reference Manual is also an + * resource that is freely available, but only for primary commands. + */ + +#ifdef _MSC_VER +#define _CRT_SECURE_NO_WARNINGS +#endif +#include +#include +#include + +#include "discid/discid.h" +#include "discid/discid_private.h" +#include "scsi.h" + +#define SUBCHANNEL_BYTES 96 /* bytes with sub-channel info (per sector) */ +/* each sub-channel byte includes 1 bit for each of the subchannel types */ +#define BITS_SUBCHANNEL 96 +/* according to spec there should be at least one ISRC per 100 secotors */ +#define SECTORS_ISRC 100 + +#define FEATURE_BYTES 4000 /* bytes to allocate for feature list */ + +/* scsi profiles */ +#define CD_ROM_PROFILE 0x0008 +#define CD_R_PROFILE 0x0009 +#define CD_RW_PROFILE 0x000a + +/* scsi features */ +#define PROFILE_LIST 0x0000 +#define MULTI_READ 0x001d +#define CD_READ 0x001e +#define CD_AUDIO_EXTERNAL_PLAY 0x0103 + +enum isrc_search { + NOTHING_FOUND = 0, + VALID_ISRC = 1, + CRC_MISMATCH = 2, +}; + + +/* Send a scsi command and receive data. */ +static mb_scsi_status scsi_cmd(mb_scsi_handle handle, + unsigned char *cmd, int cmd_len, + unsigned char *data, int data_len) { + return mb_scsi_cmd_unportable(handle, cmd, cmd_len, data, data_len); +} + +/* This uses CRC-16 (CRC-CCITT?) as defined for audio CDs + * to check the parity for 10*8 = 80 data bits + * using 2*8 = 16 checksum/parity bits. + * We feed the complete Q-channel data, data+parity (96 bits), to the algorithm + * and check for a remainder of 0. + */ +static int check_crc(const unsigned char data[10], const unsigned char crc[2]) { + /* The generator polynomial to be used: + * x^16+x^12+x^5 = 10001000000100001 = 0x11021 + * so the degree is 16 and the generator has 17 bits */ + const long generator = 0x11021; + long remainder = 0; /* start value */ + int data_bit = 0; + int byte_num; + unsigned char data_byte, mask; + + do { + /* fill the remainder with data until the 17th bit is one */ + while (data_bit < BITS_SUBCHANNEL && remainder >> 16 == 0) { + byte_num = data_bit >> 3; + if (byte_num < 10) { + data_byte = data[byte_num]; + } else { + /* crc/parity stored inverted on disc */ + data_byte = ~crc[byte_num-10]; + } + remainder = (remainder << 1) ; + /* add the current bit from the current data_byte */ + mask = 0x80 >> (data_bit % 8); + remainder += (data_byte & mask) != 0x0; + data_bit++; + } + + /* We do a polynomial division by the generator modulo(2). + * So we get the (new) remainder with XOR (^) + * and don't care about the actual result of the division. + */ + if (remainder >> 16 == 1) + remainder = remainder ^ generator; + + } while (data_bit < BITS_SUBCHANNEL); /* while data left */ + + return remainder == 0; +} + +static int decode_isrc(unsigned char *q_channel, char *isrc) { + int isrc_pos; + int data_pos; + int bit_pos; + int bit; + unsigned char buffer; + unsigned char crc[2]; + + isrc_pos = 0; + /* upper 4 bits of q_channel[0] are CONTROL */ + /* lower 4 bits of q_channel[0] = ADR = 0x03 -> mode 3 -> ISRC data */ + data_pos = 1; + bit_pos = 7; + buffer = 0; + /* first 5 chars of ISRC are alphanumeric and 6-bit BCD encoded */ + for (bit = 0; bit < 5 * 6; bit++) { + buffer = buffer << 1; + if ((q_channel[data_pos] & (1 << bit_pos)) == (1 << bit_pos)) { + buffer++; + } + bit_pos--; + if ((bit + 1) % 8 == 0) { + bit_pos = 7; + data_pos++; + } + if ((bit + 1) % 6 == 0) { + /* 0x3f = only lowest 6 bits set */ + isrc[isrc_pos] = '0' + (buffer & 0x3f); + isrc_pos++; + buffer = 0; + } + } + /* buffer[4] includes 2 zero bits + * last 7 chars of ISRC are only numeric and 4-bit encoded + */ + isrc[5] = '0' + (q_channel[5] >> 4); + isrc[6] = '0' + (q_channel[5] & 0x0f); + isrc[7] = '0' + (q_channel[6] >> 4); + isrc[8] = '0' + (q_channel[6] & 0x0f); + isrc[9] = '0' + (q_channel[7] >> 4); + isrc[10] = '0' + (q_channel[7] & 0x0f); + isrc[11] = '0' + (q_channel[8] >> 4); + /* q_channel[8] & 0x0f are zero bits */ + /* q_channel[9] is AFRAME */ + crc[0] = q_channel[10]; + crc[1] = q_channel[11]; + + if (!check_crc(q_channel, crc)) { + return 0; + } else { + return 1; + } +} + +static enum isrc_search find_isrc_in_sector(unsigned char *data, char *isrc) { + unsigned char q_buffer; + unsigned char q_data[12]; /* sub-channel data in 1 sector */ + int char_num; + int i; + + char_num = 0; + memset(q_data, 0, sizeof q_data); + q_buffer = 0; + + /* Every one of these SUBCHANNEL_BYTES includes one bit + * for every sub-channel type. + * We fetch the bits for channel Q + * and check if there is ISRC information in them. + */ + for (i = 0; i < SUBCHANNEL_BYTES; i++) { + q_buffer = q_buffer << 1; + + /* the 6th bit is the Q-channel bit + * we want to collect these bits + */ + if ((data[i] & (1 << 6)) != 0x0) { + q_buffer++; + } + + if ((i + 1) % 8 == 0) { + /* we have gathered one complete byte */ + q_data[char_num] = q_buffer; + if (char_num == 0) { + /* test if we got the right type + * of q-channel (ADR = 0x03) + * upper 4 bit are CONTROL + * Go to next sector otherwise + */ + if ((q_buffer & 0x0F) != 0x03) { + break; + } + } + char_num++; + q_buffer = 0; + } + } + if ((q_data[0] & 0x0F) == 0x03) { + /* We found a Q-channel with ISRC data. + * Test if the CRC matches and stop searching if so. + */ + if(decode_isrc(q_data, isrc)) { + return VALID_ISRC; + } else { + return CRC_MISMATCH; + } + } else { + return NOTHING_FOUND; + } +} + +void mb_scsi_stop_disc(mb_scsi_handle handle) { + unsigned char cmd[6]; + + memset(cmd, 0, sizeof cmd); + + cmd[0] = 0x1b; /* START STOP UNIT (SPC)*/ + cmd[1] = 1; /* return immediately */ + /* cmd 2-3 reserved */ + cmd[4] = 0; /* stop */ + /* cmd 5 = control byte */ + + if (scsi_cmd(handle, cmd, sizeof cmd, NULL, 0) != SUCCESS) { + fprintf(stderr, "Warning: Cannot stop device"); + } +} + +void mb_scsi_read_track_isrc(mb_scsi_handle handle, mb_disc_private *disc, + int track_num) { + int i; + unsigned char cmd[10]; + unsigned char data[24]; + char buffer[ISRC_STR_LENGTH+1]; + + memset(cmd, 0, sizeof cmd); + memset(data, 0, sizeof data); + memset(buffer, 0, sizeof buffer); + + cmd[0] = 0x42; /* READ SUB-CHANNEL (MMC)*/ + /* cmd[1] reserved / MSF bit (unused) */ + cmd[2] = 1 << 6; /* 6th bit set (SUBQ) -> get sub-channel data */ + cmd[3] = 0x03; /* get ISRC (ADR 3, Q sub-channel Mode-3) */ + /* 4+5 reserved */ + cmd[6] = track_num; + /* cmd[7] = upper byte of the transfer length */ + cmd[8] = sizeof data; /* transfer length in bytes (4 header, 20 data)*/ + /* cmd[9] = control byte */ + + if (scsi_cmd(handle, cmd, sizeof cmd, data, sizeof data) != SUCCESS) { + fprintf(stderr, "Warning: Cannot get ISRC code for track %d\n", + track_num); + return; + } + + /* data[1:4] = sub-q channel data header (audio status, data length) */ + if (data[8] & (1 << 7)) { /* TCVAL is set -> ISRCs valid */ + for (i = 0; i < ISRC_STR_LENGTH; i++) { + buffer[i] = data[9 + i]; + } + buffer[ISRC_STR_LENGTH] = 0; + strncpy(disc->isrc[track_num], buffer, ISRC_STR_LENGTH); + } + /* data[21:23] = zero, AFRAME, reserved */ + +} + +mb_scsi_features mb_scsi_get_features(mb_scsi_handle handle) { + mb_scsi_features features; + unsigned char cmd[10]; + unsigned char data[FEATURE_BYTES]; + int data_length, current_profile, feature_code, profile_code; + int num_features = 0; + int offset = 0; + int profile_offset = 0; + + memset(&features, 0, sizeof features); + memset(cmd, 0, sizeof cmd); + memset(data, 0, sizeof data); + + cmd[0] = 0x46; /* GET CONFIGURATION (MMC) */ + cmd[1] = 1; /* all currently available features */ + /* 2-3 = starting feature number, just leave empty = 0 */ + /* 4-6 reserved */ + cmd[7] = sizeof data >> 8; + cmd[8] = (char) sizeof data; + /* cmd[9] is control */ + + if (scsi_cmd(handle, cmd, sizeof cmd, data, sizeof data) != SUCCESS) { + fprintf(stderr, "Warning: could not fetch features\n"); + return features; /* empty at this stage */ + } + + data_length = (data[0] << 24) + (data[1] << 16) + (data[2] << 8) + + data[3]; + if (data_length > sizeof data) { + fprintf(stderr, "Warning: not all features returned\n"); + } + + current_profile = (data[6] << 8) + data[7]; + if (current_profile == CD_ROM_PROFILE) { + features.raw_isrc = 1; + } else if (current_profile == CD_R_PROFILE) { + features.raw_isrc = 1; + } else if (current_profile == CD_RW_PROFILE) { + features.raw_isrc = 1; + } + + offset = 8; + while (offset < data_length) { + num_features++; + feature_code = (data[offset] << 8) + data[offset+1]; + + if (feature_code == PROFILE_LIST) { + profile_offset = offset + 4; + while ((profile_offset - offset) < data[offset+3]) { + profile_code = (data[profile_offset] << 8 ) + + data[profile_offset+1]; + if (profile_code == CD_ROM_PROFILE) { + features.raw_isrc = 1; + } else if (profile_code == CD_R_PROFILE) { + features.raw_isrc = 1; + } else if (profile_code == CD_RW_PROFILE) { + features.raw_isrc = 1; + } + profile_offset += 4; + } + } else if (feature_code == MULTI_READ) { + features.raw_isrc = 1; + } else if (feature_code == CD_READ) { + features.raw_isrc = 1; + if (data[offset+4] & 0x01) { + features.cd_text = 1; + } + } else if (feature_code == CD_AUDIO_EXTERNAL_PLAY) { + features.isrc = 1; + } + + offset += 4 + data[offset+3]; + } + + return features; +} + +/* We read sectors from a track until finding a valid ISRC. + * An empty ISRC is valid in that context -> leads to empty string. + * Up to 100 sectors have to be read for every ISRC candidate. + */ +void mb_scsi_read_track_isrc_raw(mb_scsi_handle handle, mb_disc_private *disc, + int track_num) { + int max_sectors; + int disc_offset; /* in sectors */ + int data_len; /* in bytes */ + unsigned char *data; /* overall data */ + unsigned char cmd[12]; + char isrc[ISRC_STR_LENGTH+1]; + int sector = 0; + enum isrc_search search_result; + int isrc_found = 0; + int valid_isrc = 0; + int warning_shown = 0; + int retval; + + data_len = SUBCHANNEL_BYTES; + data = (unsigned char *) calloc(data_len, 1); + + /* start reading sectors at start of track */ + disc_offset = disc->track_offsets[track_num]; + max_sectors = mb_disc_get_track_length(disc, track_num); + + /* search until a valid ISRC is found, + * the end of the track is reached + * or we are certain there are no ISRCs + * (otherwise there would be one in the first 100 sectors) + */ + while (!valid_isrc && sector <= max_sectors + && (isrc_found || sector <= SECTORS_ISRC + 10)) { + memset(cmd, 0, sizeof cmd); + memset(isrc, 0, sizeof isrc); + memset(data, 0, data_len); + + /* 0xbe = READ CD, implementation optional in contrast to 0x42 + * support given with GET CONFIGURATION (0x46) + * Support part of: + * Multi-Read Feature (0x001d) and + * CD Read Feature (0x001e), + */ + cmd[0] = 0xbe; /* READ CD (MMC) */ + cmd[2] = disc_offset >> 24; + cmd[3] = disc_offset >> 16; + cmd[4] = disc_offset >> 8; + cmd[5] = disc_offset; /* from where to start reading */ + /* cmd[6] and cmd[7] are unused upper bytes of sector count */ + cmd[8] = 1; /* sectors to read */ + cmd[9] = 0x00; /* read no raw (main channel) data */ + cmd[10] = 0x01; /* Sub-Channel Selection: raw P-W=001*/ + /* cmd[11] = control byte */ + + retval = scsi_cmd(handle, cmd, sizeof cmd, data, data_len); + if (retval != SUCCESS) { + fprintf(stderr, "Warning: raw ISRCs failed for track %d, trying normal read\n", track_num); + mb_scsi_read_track_isrc(handle, disc, track_num); + return; + } + + search_result = find_isrc_in_sector(data, isrc); + if (search_result == VALID_ISRC) { + isrc_found = 1; + valid_isrc = 1; + break; + } else if (search_result == CRC_MISMATCH) { + isrc_found = 1; /* invalid, but present, try more */ + fprintf(stderr, "Warning: CRC mismatch track %d: %s\n", + track_num, isrc); + warning_shown = 1; + } /* otherwise keep searching the sectors */ + + sector++; + disc_offset++; + } + + free(data); + + if (isrc_found) { + if (warning_shown) { + fprintf(stderr, + " valid ISRC for track %d: %s\n", + track_num, isrc); + } + strncpy(disc->isrc[track_num], isrc, ISRC_STR_LENGTH); + } +} + + +/* EOF */ diff --git a/src/scsi.h b/src/scsi.h new file mode 100644 index 0000000..4b564b4 --- /dev/null +++ b/src/scsi.h @@ -0,0 +1,85 @@ +/* -------------------------------------------------------------------------- + + MusicBrainz -- The Internet music metadatabase + + Copyright (C) 2013 Johannes Dewender + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +----------------------------------------------------------------------------*/ + +#include "discid/discid_private.h" + +#ifndef HANDLE +#define HANDLE void * +#endif + +typedef struct { + int fd; /* Linux */ + HANDLE hDevice; /* Windows */ +} mb_scsi_handle; + +typedef struct { + int raw_isrc; /* needed for read_track_isrc_raw */ + int isrc; /* needed for read_track_isrc */ + int cd_text; +} mb_scsi_features; + +typedef enum { + SUCCESS, + GNERIC_ERROR, + IO_ERROR, + STATUS_ERROR, + NO_DATA_RETURNED, +} mb_scsi_status; + +/* + * Send a scsi command to a device and receive data. + * + * THIS FUNCTION HAS TO BE IMPLEMENTED FOR THE PLATFORM + */ +LIBDISCID_INTERNAL mb_scsi_status mb_scsi_cmd_unportable(mb_scsi_handle handle, + unsigned char *cmd, int cmd_len, + unsigned char *data, int data_len); + + + +/* + * The following functions are implemented in scsi.c + * and can be used after scsi_cmd_unportable is implemented on the plaform. + */ + +/* + * gets a structure with currently available features + */ +LIBDISCID_INTERNAL mb_scsi_features mb_scsi_get_features(mb_scsi_handle handle); + +/* + * read an ISRC using the READ SUB-CHANNEL command (0x42) + */ +LIBDISCID_INTERNAL void mb_scsi_read_track_isrc(mb_scsi_handle handle, + mb_disc_private *disc, + int track_num); + +/* + * parsing the sub-channel and an ISRC using the READ command (0xbe) + */ +LIBDISCID_INTERNAL void mb_scsi_read_track_isrc_raw(mb_scsi_handle handle, + mb_disc_private *disc, int track_num); + +/* + * stop the CD drive -> stop disc spinning + */ +LIBDISCID_INTERNAL void mb_scsi_stop_disc(mb_scsi_handle handle); diff --git a/test/test.c b/test/test.c index 1d0a483..f7b9de3 100644 --- a/test/test.c +++ b/test/test.c @@ -19,6 +19,10 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA --------------------------------------------------------------------------- */ +#ifdef _MSC_VER +#define snprintf _snprintf +#define _CRT_SECURE_NO_WARNINGS +#endif #include #include diff --git a/test/test_core.c b/test/test_core.c index 39fa6aa..6556c96 100644 --- a/test/test_core.c +++ b/test/test_core.c @@ -88,7 +88,7 @@ int main(int argc, char *argv[]) { announce("discid_free"); discid_free(d); evaluate(1); /* only segfaults etc. would "show" */ - + return !test_result(); } diff --git a/test/test_put.c b/test/test_put.c index 66ecca9..9ba653c 100644 --- a/test/test_put.c +++ b/test/test_put.c @@ -107,7 +107,7 @@ int main(int argc, char *argv[]) { evaluate(strlen(discid_get_error_msg(d)) == 0); discid_free(d); - + return !test_result(); }