Systemd/src/udev/cdrom_id/cdrom_id.c

1031 lines
35 KiB
C

/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* cdrom_id - optical drive and media information prober
*/
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <limits.h>
#include <linux/cdrom.h>
#include <scsi/sg.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include "fd-util.h"
#include "log.h"
#include "memory-util.h"
#include "random-util.h"
#include "sort-util.h"
#include "string-table.h"
#include "string-util.h"
#include "udev-util.h"
#include "unaligned.h"
static bool arg_eject = false;
static bool arg_lock = false;
static bool arg_unlock = false;
static const char *arg_node = NULL;
typedef enum Feature {
FEATURE_RW_NONREMOVABLE = 0x01,
FEATURE_RW_REMOVABLE = 0x02,
FEATURE_MO_SE = 0x03, /* sector erase */
FEATURE_MO_WO = 0x04, /* write once */
FEATURE_MO_AS = 0x05, /* advance storage */
FEATURE_CD_ROM = 0x08,
FEATURE_CD_R = 0x09,
FEATURE_CD_RW = 0x0a,
FEATURE_DVD_ROM = 0x10,
FEATURE_DVD_R = 0x11,
FEATURE_DVD_RAM = 0x12,
FEATURE_DVD_RW_RO = 0x13, /* restricted overwrite mode */
FEATURE_DVD_RW_SEQ = 0x14, /* sequential mode */
FEATURE_DVD_R_DL_SEQ = 0x15, /* sequential recording */
FEATURE_DVD_R_DL_JR = 0x16, /* jump recording */
FEATURE_DVD_RW_DL = 0x17,
FEATURE_DVD_R_DDR = 0x18, /* download disc recording - dvd for css managed recording */
FEATURE_DVD_PLUS_RW = 0x1a,
FEATURE_DVD_PLUS_R = 0x1b,
FEATURE_DDCD_ROM = 0x20,
FEATURE_DDCD_R = 0x21,
FEATURE_DDCD_RW = 0x22,
FEATURE_DVD_PLUS_RW_DL = 0x2a,
FEATURE_DVD_PLUS_R_DL = 0x2b,
FEATURE_BD = 0x40,
FEATURE_BD_R_SRM = 0x41, /* sequential recording mode */
FEATURE_BD_R_RRM = 0x42, /* random recording mode */
FEATURE_BD_RE = 0x43,
FEATURE_HDDVD = 0x50,
FEATURE_HDDVD_R = 0x51,
FEATURE_HDDVD_RAM = 0x52,
FEATURE_HDDVD_RW = 0x53,
FEATURE_HDDVD_R_DL = 0x58,
FEATURE_HDDVD_RW_DL = 0x5a,
FEATURE_MRW,
FEATURE_MRW_W,
_FEATURE_MAX,
_FEATURE_INVALID = -1,
} Feature;
typedef enum MediaState {
MEDIA_STATE_BLANK = 0,
MEDIA_STATE_APPENDABLE = 1,
MEDIA_STATE_COMPLETE = 2,
MEDIA_STATE_OTHER = 3,
_MEDIA_STATE_MAX,
_MEDIA_STATE_INVALID = -1,
} MediaState;
typedef struct Context {
int fd;
Feature *drive_features;
size_t n_drive_feature;
size_t n_allocated;
Feature media_feature;
bool has_media;
MediaState media_state;
unsigned media_session_next;
unsigned media_session_count;
unsigned media_track_count;
unsigned media_track_count_data;
unsigned media_track_count_audio;
uint64_t media_session_last_offset;
} Context;
static void context_clear(Context *c) {
if (!c)
return;
safe_close(c->fd);
free(c->drive_features);
}
static void context_init(Context *c) {
assert(c);
*c = (Context) {
.fd = -1,
.media_feature = _FEATURE_INVALID,
.media_state = _MEDIA_STATE_INVALID,
};
}
static bool drive_has_feature(const Context *c, Feature f) {
assert(c);
for (size_t i = 0; i < c->n_drive_feature; i++)
if (c->drive_features[i] == f)
return true;
return false;
}
static int set_drive_feature(Context *c, Feature f) {
assert(c);
if (drive_has_feature(c, f))
return 0;
if (!GREEDY_REALLOC(c->drive_features, c->n_allocated, c->n_drive_feature + 1))
return -ENOMEM;
c->drive_features[c->n_drive_feature++] = f;
return 1;
}
#define ERRCODE(s) ((((s)[2] & 0x0F) << 16) | ((s)[12] << 8) | ((s)[13]))
#define SK(errcode) (((errcode) >> 16) & 0xF)
#define ASC(errcode) (((errcode) >> 8) & 0xFF)
#define ASCQ(errcode) ((errcode) & 0xFF)
#define CHECK_CONDITION 0x01
static int log_scsi_debug_errno(int error, const char *msg) {
assert(error != 0);
/* error < 0 means errno-style error, error > 0 means SCSI error */
if (error < 0)
return log_debug_errno(error, "Failed to %s: %m", msg);
return log_debug_errno(SYNTHETIC_ERRNO(EIO),
"Failed to %s with SK=%X/ASC=%02X/ACQ=%02X",
msg, SK(error), ASC(error), ASCQ(error));
}
struct scsi_cmd {
struct cdrom_generic_command cgc;
union {
struct request_sense s;
unsigned char u[18];
} _sense;
struct sg_io_hdr sg_io;
};
static void scsi_cmd_init(struct scsi_cmd *cmd) {
memzero(cmd, sizeof(struct scsi_cmd));
cmd->cgc.quiet = 1;
cmd->cgc.sense = &cmd->_sense.s;
cmd->sg_io.interface_id = 'S';
cmd->sg_io.mx_sb_len = sizeof(cmd->_sense);
cmd->sg_io.cmdp = cmd->cgc.cmd;
cmd->sg_io.sbp = cmd->_sense.u;
cmd->sg_io.flags = SG_FLAG_LUN_INHIBIT | SG_FLAG_DIRECT_IO;
}
static void scsi_cmd_set(struct scsi_cmd *cmd, size_t i, unsigned char arg) {
cmd->sg_io.cmd_len = i + 1;
cmd->cgc.cmd[i] = arg;
}
static int scsi_cmd_run(struct scsi_cmd *cmd, int fd, unsigned char *buf, size_t bufsize) {
int r;
assert(cmd);
assert(fd >= 0);
assert(buf || bufsize == 0);
/* Return 0 on success. On failure, return negative errno or positive error code. */
if (bufsize > 0) {
cmd->sg_io.dxferp = buf;
cmd->sg_io.dxfer_len = bufsize;
cmd->sg_io.dxfer_direction = SG_DXFER_FROM_DEV;
} else
cmd->sg_io.dxfer_direction = SG_DXFER_NONE;
if (ioctl(fd, SG_IO, &cmd->sg_io) < 0)
return -errno;
if ((cmd->sg_io.info & SG_INFO_OK_MASK) != SG_INFO_OK) {
if (cmd->sg_io.masked_status & CHECK_CONDITION) {
r = ERRCODE(cmd->_sense.u);
if (r != 0)
return r;
}
return -EIO;
}
return 0;
}
static int scsi_cmd_run_and_log(struct scsi_cmd *cmd, int fd, unsigned char *buf, size_t bufsize, const char *msg) {
int r;
assert(msg);
r = scsi_cmd_run(cmd, fd, buf, bufsize);
if (r != 0)
return log_scsi_debug_errno(r, msg);
return 0;
}
static int media_lock(int fd, bool lock) {
/* disable the kernel's lock logic */
if (ioctl(fd, CDROM_CLEAR_OPTIONS, CDO_LOCK) < 0)
log_debug_errno(errno, "Failed to issue ioctl(CDROM_CLEAR_OPTIONS, CDO_LOCK), ignoring: %m");
if (ioctl(fd, CDROM_LOCKDOOR, lock ? 1 : 0) < 0)
return log_debug_errno(errno, "Failed to issue ioctl(CDROM_LOCKDOOR): %m");
return 0;
}
static int media_eject(int fd) {
struct scsi_cmd sc;
scsi_cmd_init(&sc);
scsi_cmd_set(&sc, 0, GPCMD_START_STOP_UNIT);
scsi_cmd_set(&sc, 4, 0x02);
scsi_cmd_set(&sc, 5, 0);
return scsi_cmd_run_and_log(&sc, fd, NULL, 0, "start/stop unit");
}
static int cd_capability_compat(Context *c) {
int capability, r;
assert(c);
capability = ioctl(c->fd, CDROM_GET_CAPABILITY, NULL);
if (capability < 0)
return log_debug_errno(errno, "CDROM_GET_CAPABILITY failed");
if (capability & CDC_CD_R) {
r = set_drive_feature(c, FEATURE_CD_R);
if (r < 0)
return log_oom_debug();
}
if (capability & CDC_CD_RW) {
r = set_drive_feature(c, FEATURE_CD_RW);
if (r < 0)
return log_oom_debug();
}
if (capability & CDC_DVD) {
r = set_drive_feature(c, FEATURE_DVD_ROM);
if (r < 0)
return log_oom_debug();
}
if (capability & CDC_DVD_R) {
r = set_drive_feature(c, FEATURE_DVD_R);
if (r < 0)
return log_oom_debug();
}
if (capability & CDC_DVD_RAM) {
r = set_drive_feature(c, FEATURE_DVD_RAM);
if (r < 0)
return log_oom_debug();
}
if (capability & CDC_MRW) {
r = set_drive_feature(c, FEATURE_MRW);
if (r < 0)
return log_oom_debug();
}
if (capability & CDC_MRW_W) {
r = set_drive_feature(c, FEATURE_MRW_W);
if (r < 0)
return log_oom_debug();
}
return 0;
}
static int cd_media_compat(Context *c) {
assert(c);
if (ioctl(c->fd, CDROM_DRIVE_STATUS, CDSL_CURRENT) != CDS_DISC_OK)
return log_debug_errno(errno, "CDROM_DRIVE_STATUS != CDS_DISC_OK");
c->has_media = true;
return 0;
}
static int cd_inquiry(Context *c) {
struct scsi_cmd sc;
unsigned char inq[36];
int r;
assert(c);
scsi_cmd_init(&sc);
scsi_cmd_set(&sc, 0, GPCMD_INQUIRY);
scsi_cmd_set(&sc, 4, sizeof(inq));
scsi_cmd_set(&sc, 5, 0);
r = scsi_cmd_run_and_log(&sc, c->fd, inq, sizeof(inq), "inquire");
if (r < 0)
return r;
if ((inq[0] & 0x1F) != 5)
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Not an MMC unit");
log_debug("INQUIRY: [%.8s][%.16s][%.4s]", inq + 8, inq + 16, inq + 32);
return 0;
}
static int feature_profiles(Context *c, const unsigned char *profiles, size_t size) {
int r;
assert(c);
for (size_t i = 0; i + 4 <= size; i += 4) {
r = set_drive_feature(c, (Feature) unaligned_read_be16(&profiles[i]));
if (r < 0)
return log_oom_debug();
}
return 1;
}
static int cd_profiles_old_mmc(Context *c) {
disc_information discinfo;
struct scsi_cmd sc;
size_t len;
int r;
assert(c);
scsi_cmd_init(&sc);
scsi_cmd_set(&sc, 0, GPCMD_READ_DISC_INFO);
scsi_cmd_set(&sc, 8, sizeof(discinfo.disc_information_length));
scsi_cmd_set(&sc, 9, 0);
r = scsi_cmd_run_and_log(&sc, c->fd, (unsigned char *)&discinfo.disc_information_length, sizeof(discinfo.disc_information_length), "read disc information");
if (r >= 0) {
/* Not all drives have the same disc_info length, so requeue
* packet with the length the drive tells us it can supply */
len = be16toh(discinfo.disc_information_length) + sizeof(discinfo.disc_information_length);
if (len > sizeof(discinfo))
len = sizeof(discinfo);
scsi_cmd_init(&sc);
scsi_cmd_set(&sc, 0, GPCMD_READ_DISC_INFO);
scsi_cmd_set(&sc, 8, len);
scsi_cmd_set(&sc, 9, 0);
r = scsi_cmd_run_and_log(&sc, c->fd, (unsigned char *)&discinfo, len, "read disc information");
}
if (r < 0) {
if (c->has_media) {
log_debug("No current profile, but disc is present; assuming CD-ROM.");
c->media_feature = FEATURE_CD_ROM;
c->media_track_count = 1;
c->media_track_count_data = 1;
return 1;
} else
return log_debug_errno(SYNTHETIC_ERRNO(ENOMEDIUM),
"no current profile, assuming no media.");
};
c->has_media = true;
if (discinfo.erasable)
c->media_feature = FEATURE_CD_RW;
else if (discinfo.disc_status < 2 && drive_has_feature(c, FEATURE_CD_R))
c->media_feature = FEATURE_CD_R;
else
c->media_feature = FEATURE_CD_ROM;
return 0;
}
static int cd_profiles(Context *c) {
struct scsi_cmd sc;
unsigned char features[65530];
unsigned cur_profile;
size_t len;
int r;
assert(c);
/* First query the current profile */
scsi_cmd_init(&sc);
scsi_cmd_set(&sc, 0, GPCMD_GET_CONFIGURATION);
scsi_cmd_set(&sc, 8, 8);
scsi_cmd_set(&sc, 9, 0);
r = scsi_cmd_run(&sc, c->fd, features, 8);
if (r != 0) {
/* handle pre-MMC2 drives which do not support GET CONFIGURATION */
if (r > 0 && SK(r) == 0x5 && IN_SET(ASC(r), 0x20, 0x24)) {
log_debug("Drive is pre-MMC2 and does not support 46h get configuration command; "
"trying to work around the problem.");
return cd_profiles_old_mmc(c);
}
return log_scsi_debug_errno(r, "get configuration");
}
cur_profile = unaligned_read_be16(&features[6]);
if (cur_profile > 0) {
log_debug("current profile 0x%02x", cur_profile);
c->media_feature = (Feature) cur_profile;
c->has_media = true;
} else {
log_debug("no current profile, assuming no media");
c->has_media = false;
}
len = unaligned_read_be32(features);
log_debug("GET CONFIGURATION: size of features buffer %zu", len);
if (len > sizeof(features)) {
log_debug("cannot get features in a single query, truncating");
len = sizeof(features);
} else if (len <= 8)
len = sizeof(features);
/* Now get the full feature buffer */
scsi_cmd_init(&sc);
scsi_cmd_set(&sc, 0, GPCMD_GET_CONFIGURATION);
scsi_cmd_set(&sc, 7, (len >> 8) & 0xff);
scsi_cmd_set(&sc, 8, len & 0xff);
scsi_cmd_set(&sc, 9, 0);
r = scsi_cmd_run_and_log(&sc, c->fd, features, len, "get configuration");
if (r < 0)
return r;
/* parse the length once more, in case the drive decided to have other features suddenly :) */
len = unaligned_read_be32(features);
log_debug("GET CONFIGURATION: size of features buffer %zu", len);
if (len > sizeof(features)) {
log_debug("cannot get features in a single query, truncating");
len = sizeof(features);
}
/* device features */
for (size_t i = 8; i + 4 < len; i += 4 + features[i + 3]) {
unsigned feature;
feature = unaligned_read_be16(&features[i]);
switch (feature) {
case 0x00:
log_debug("GET CONFIGURATION: feature 'profiles', with %u entries", features[i + 3] / 4);
feature_profiles(c, features + i + 4, MIN(features[i + 3], len - i - 4));
break;
default:
log_debug("GET CONFIGURATION: feature 0x%04x <ignored>, with 0x%02x bytes", feature, features[i + 3]);
break;
}
}
return c->has_media;
}
static const char * const media_state_table[_MEDIA_STATE_MAX] = {
[MEDIA_STATE_BLANK] = "blank",
[MEDIA_STATE_APPENDABLE] = "appendable",
[MEDIA_STATE_COMPLETE] = "complete",
[MEDIA_STATE_OTHER] = "other",
};
DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(media_state, MediaState);
static int dvd_ram_media_update_state(Context *c) {
struct scsi_cmd sc;
unsigned char dvdstruct[8];
unsigned char format[12];
unsigned char len;
int r;
assert(c);
/* Return 1 if media state is determined. */
if (c->media_feature != FEATURE_DVD_RAM)
return 0;
/* a write protected dvd-ram may report "complete" status */
scsi_cmd_init(&sc);
scsi_cmd_set(&sc, 0, GPCMD_READ_DVD_STRUCTURE);
scsi_cmd_set(&sc, 7, 0xC0);
scsi_cmd_set(&sc, 9, sizeof(dvdstruct));
scsi_cmd_set(&sc, 11, 0);
r = scsi_cmd_run_and_log(&sc, c->fd, dvdstruct, sizeof(dvdstruct), "read DVD structure");
if (r < 0)
return r;
if (dvdstruct[4] & 0x02) {
c->media_state = MEDIA_STATE_COMPLETE;
log_debug("write-protected DVD-RAM media inserted");
return 1;
}
/* let's make sure we don't try to read unformatted media */
scsi_cmd_init(&sc);
scsi_cmd_set(&sc, 0, GPCMD_READ_FORMAT_CAPACITIES);
scsi_cmd_set(&sc, 8, sizeof(format));
scsi_cmd_set(&sc, 9, 0);
r = scsi_cmd_run_and_log(&sc, c->fd, format, sizeof(format), "read DVD format capacities");
if (r < 0)
return r;
len = format[3];
if (len & 7 || len < 16)
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
"invalid format capacities length");
switch(format[8] & 3) {
case 1:
/* This means that last format was interrupted or failed, blank dvd-ram discs are
* factory formatted. Take no action here as it takes quite a while to reformat a
* dvd-ram and it's not automatically started. */
log_debug("unformatted DVD-RAM media inserted");
return 1;
case 2:
log_debug("formatted DVD-RAM media inserted");
return 0;
case 3:
c->has_media = false;
return log_debug_errno(SYNTHETIC_ERRNO(ENOMEDIUM),
"format capacities returned no media");
}
return 0;
}
static int dvd_media_update_state(Context *c) {
struct scsi_cmd sc;
unsigned char buffer[32 * 2048];
int r;
r = dvd_ram_media_update_state(c);
if (r != 0)
return r;
/* Take a closer look at formatted media (unformatted DVD+RW
* has "blank" status", DVD-RAM was examined earlier) and check
* for ISO and UDF PVDs or a fs superblock presence and do it
* in one ioctl (we need just sectors 0 and 16) */
scsi_cmd_init(&sc);
scsi_cmd_set(&sc, 0, GPCMD_READ_10);
scsi_cmd_set(&sc, 5, 0);
scsi_cmd_set(&sc, 8, sizeof(buffer)/2048);
scsi_cmd_set(&sc, 9, 0);
r = scsi_cmd_run_and_log(&sc, c->fd, buffer, sizeof(buffer), "read first 32 blocks");
if (r < 0) {
c->has_media = false;
return r;
}
/* if any non-zero data is found in sector 16 (iso and udf) or
* eventually 0 (fat32 boot sector, ext2 superblock, etc), disc
* is assumed non-blank */
for (size_t offset = 32768; offset < 32768 + 2048; offset++)
if (buffer[offset] != 0) {
log_debug("data in block 16, assuming complete");
return 0;
}
for (size_t offset = 0; offset < 2048; offset++)
if (buffer[offset] != 0) {
log_debug("data in block 0, assuming complete");
return 0;
}
log_debug("no data in blocks 0 or 16, assuming blank");
c->media_state = MEDIA_STATE_BLANK;
return 0;
}
static int cd_media_info(Context *c) {
struct scsi_cmd sc;
unsigned char header[32];
MediaState state;
int r;
assert(c);
scsi_cmd_init(&sc);
scsi_cmd_set(&sc, 0, GPCMD_READ_DISC_INFO);
scsi_cmd_set(&sc, 8, sizeof(header));
scsi_cmd_set(&sc, 9, 0);
r = scsi_cmd_run_and_log(&sc, c->fd, header, sizeof(header), "read disc information");
if (r < 0)
return r;
c->has_media = true;
log_debug("disk type %02x", header[8]);
state = (MediaState) (header[2] & 0x03);
log_debug("hardware reported media status: %s", strna(media_state_to_string(state)));
/* exclude plain CDROM, some fake cdroms return 0 for "blank" media here */
if (c->media_feature != FEATURE_CD_ROM)
c->media_state = state;
/* fresh DVD-RW in restricted overwrite mode reports itself as
* "appendable"; change it to "blank" to make it consistent with what
* gets reported after blanking, and what userspace expects. */
if (c->media_feature == FEATURE_DVD_RW_RO && state == MEDIA_STATE_APPENDABLE)
c->media_state = MEDIA_STATE_BLANK;
/* DVD+RW discs (and DVD-RW in restricted mode) once formatted are
* always "complete", DVD-RAM are "other" or "complete" if the disc is
* write protected; we need to check the contents if it is blank */
if (IN_SET(c->media_feature, FEATURE_DVD_RW_RO, FEATURE_DVD_PLUS_RW, FEATURE_DVD_PLUS_RW_DL, FEATURE_DVD_RAM) &&
IN_SET(state, MEDIA_STATE_COMPLETE, MEDIA_STATE_OTHER)) {
r = dvd_media_update_state(c);
if (r < 0)
return r;
}
/* "other" is e. g. DVD-RAM, can't append sessions there; DVDs in
* restricted overwrite mode can never append, only in sequential mode */
if (c->media_feature != FEATURE_DVD_RW_RO && IN_SET(state, MEDIA_STATE_BLANK, MEDIA_STATE_APPENDABLE))
c->media_session_next = header[10] << 8 | header[5];
c->media_session_count = header[9] << 8 | header[4];
c->media_track_count = header[11] << 8 | header[6];
return 0;
}
static int cd_media_toc(Context *c) {
struct scsi_cmd sc;
unsigned char header[12];
unsigned char toc[65536];
unsigned num_tracks;
size_t len;
int r;
assert(c);
scsi_cmd_init(&sc);
scsi_cmd_set(&sc, 0, GPCMD_READ_TOC_PMA_ATIP);
scsi_cmd_set(&sc, 6, 1);
scsi_cmd_set(&sc, 8, sizeof(header));
scsi_cmd_set(&sc, 9, 0);
r = scsi_cmd_run_and_log(&sc, c->fd, header, sizeof(header), "read TOC");
if (r < 0)
return r;
len = unaligned_read_be16(header) + 2;
log_debug("READ TOC: len: %zu, start track: %u, end track: %u", len, header[2], header[3]);
if (len > sizeof(toc))
return -1;
/* empty media has no tracks */
if (len < 8)
return 0;
/* 2: first track, 3: last track */
num_tracks = header[3] - header[2] + 1;
scsi_cmd_init(&sc);
scsi_cmd_set(&sc, 0, GPCMD_READ_TOC_PMA_ATIP);
scsi_cmd_set(&sc, 6, header[2]); /* First Track/Session Number */
scsi_cmd_set(&sc, 7, (len >> 8) & 0xff);
scsi_cmd_set(&sc, 8, len & 0xff);
scsi_cmd_set(&sc, 9, 0);
r = scsi_cmd_run_and_log(&sc, c->fd, toc, len, "read TOC (tracks)");
if (r < 0)
return r;
/* Take care to not iterate beyond the last valid track as specified in
* the TOC, but also avoid going beyond the TOC length, just in case
* the last track number is invalidly large */
for (size_t i = 4; i + 8 < len && num_tracks > 0; i += 8, --num_tracks) {
bool is_data_track;
uint32_t block;
is_data_track = (toc[i + 1] & 0x04) != 0;
block = unaligned_read_be32(&toc[i + 4]);
log_debug("track=%u info=0x%x(%s) start_block=%"PRIu32,
toc[i + 2], toc[i + 1] & 0x0f, is_data_track ? "data":"audio", block);
if (is_data_track)
c->media_track_count_data++;
else
c->media_track_count_audio++;
}
scsi_cmd_init(&sc);
scsi_cmd_set(&sc, 0, GPCMD_READ_TOC_PMA_ATIP);
scsi_cmd_set(&sc, 2, 1); /* Session Info */
scsi_cmd_set(&sc, 8, sizeof(header));
scsi_cmd_set(&sc, 9, 0);
r = scsi_cmd_run_and_log(&sc, c->fd, header, sizeof(header), "read TOC (multi session)");
if (r < 0)
return r;
len = unaligned_read_be32(&header[8]);
log_debug("last track %u starts at block %zu", header[4+2], len);
c->media_session_last_offset = (uint64_t) len * 2048;
return 0;
}
static int open_drive(Context *c) {
_cleanup_close_ int fd = -1;
assert(c);
assert(c->fd < 0);
for (int cnt = 0; cnt < 20; cnt++) {
if (cnt != 0)
(void) usleep(100 * USEC_PER_MSEC + random_u64() % (100 * USEC_PER_MSEC));
fd = open(arg_node, O_RDONLY|O_NONBLOCK|O_CLOEXEC);
if (fd >= 0 || errno != EBUSY)
break;
}
if (fd < 0)
return log_debug_errno(errno, "Unable to open '%s'", arg_node);
log_debug("probing: '%s'", arg_node);
c->fd = TAKE_FD(fd);
return 0;
}
typedef struct FeatureToString {
Feature feature;
const char *str;
} FeatureToString;
static const FeatureToString feature_to_string[] = {
{ .feature = FEATURE_RW_NONREMOVABLE, .str = "RW_NONREMOVABLE", },
{ .feature = FEATURE_RW_REMOVABLE, .str = "RW_REMOVABLE", },
{ .feature = FEATURE_MO_SE, .str = "MO_SE", },
{ .feature = FEATURE_MO_WO, .str = "MO_WO", },
{ .feature = FEATURE_MO_AS, .str = "MO_AS", },
{ .feature = FEATURE_CD_ROM, .str = "CD", },
{ .feature = FEATURE_CD_R, .str = "CD_R", },
{ .feature = FEATURE_CD_RW, .str = "CD_RW", },
{ .feature = FEATURE_DVD_ROM, .str = "DVD", },
{ .feature = FEATURE_DVD_R, .str = "DVD_R", },
{ .feature = FEATURE_DVD_RAM, .str = "DVD_RAM", },
{ .feature = FEATURE_DVD_RW_RO, .str = "DVD_RW_RO", },
{ .feature = FEATURE_DVD_RW_SEQ, .str = "DVD_RW_SEQ", },
{ .feature = FEATURE_DVD_R_DL_SEQ, .str = "DVD_R_DL_SEQ", },
{ .feature = FEATURE_DVD_R_DL_JR, .str = "DVD_R_DL_JR", },
{ .feature = FEATURE_DVD_RW_DL, .str = "DVD_RW_DL", },
{ .feature = FEATURE_DVD_R_DDR, .str = "DVD_R_DDR", },
{ .feature = FEATURE_DVD_PLUS_RW, .str = "DVD_PLUS_RW", },
{ .feature = FEATURE_DVD_PLUS_R, .str = "DVD_PLUS_R", },
{ .feature = FEATURE_DDCD_ROM, .str = "DDCD", },
{ .feature = FEATURE_DDCD_R, .str = "DDCD_R", },
{ .feature = FEATURE_DDCD_RW, .str = "DDCD_RW", },
{ .feature = FEATURE_DVD_PLUS_RW_DL, .str = "DVD_PLUS_RW_DL", },
{ .feature = FEATURE_DVD_PLUS_R_DL, .str = "DVD_PLUS_R_DL", },
{ .feature = FEATURE_BD, .str = "BD", },
{ .feature = FEATURE_BD_R_SRM, .str = "BD_R_SRM", },
{ .feature = FEATURE_BD_R_RRM, .str = "BD_R_RRM", },
{ .feature = FEATURE_BD_RE, .str = "BD_RE", },
{ .feature = FEATURE_HDDVD, .str = "HDDVD", },
{ .feature = FEATURE_HDDVD_R, .str = "HDDVD_R", },
{ .feature = FEATURE_HDDVD_RAM, .str = "HDDVD_RAM", },
{ .feature = FEATURE_HDDVD_RW, .str = "HDDVD_RW", },
{ .feature = FEATURE_HDDVD_R_DL, .str = "HDDVD_R_DL", },
{ .feature = FEATURE_HDDVD_RW_DL, .str = "HDDVD_RW_DL", },
{ .feature = FEATURE_MRW, .str = "MRW", },
{ .feature = FEATURE_MRW_W, .str = "MRW_W", },
};
static int feature_to_string_compare_func(const FeatureToString *a, const FeatureToString *b) {
assert(a);
assert(b);
return CMP(a->feature, b->feature);
}
static void print_feature(Feature feature, const char *prefix) {
FeatureToString *found, in = {
.feature = feature,
};
assert(prefix);
found = typesafe_bsearch(&in, feature_to_string, ELEMENTSOF(feature_to_string), feature_to_string_compare_func);
if (!found)
return (void) log_debug("Unknown feature 0x%02x, ignoring", (unsigned) feature);
printf("%s_%s=1\n", prefix, found->str);
}
static void print_properties(const Context *c) {
const char *state;
assert(c);
printf("ID_CDROM=1\n");
for (size_t i = 0; i < c->n_drive_feature; i++)
print_feature(c->drive_features[i], "ID_CDROM");
if (drive_has_feature(c, FEATURE_MO_SE) ||
drive_has_feature(c, FEATURE_MO_WO) ||
drive_has_feature(c, FEATURE_MO_AS))
printf("ID_CDROM_MO=1\n");
if (drive_has_feature(c, FEATURE_DVD_RW_RO) ||
drive_has_feature(c, FEATURE_DVD_RW_SEQ))
printf("ID_CDROM_DVD_RW=1\n");
if (drive_has_feature(c, FEATURE_DVD_R_DL_SEQ) ||
drive_has_feature(c, FEATURE_DVD_R_DL_JR))
printf("ID_CDROM_DVD_R_DL=1\n");
if (drive_has_feature(c, FEATURE_DVD_R_DDR))
printf("ID_CDROM_DVD_R=1\n");
if (drive_has_feature(c, FEATURE_BD_R_SRM) ||
drive_has_feature(c, FEATURE_BD_R_RRM))
printf("ID_CDROM_BD_R=1\n");
if (c->has_media) {
printf("ID_CDROM_MEDIA=1\n");
print_feature(c->media_feature, "ID_CDROM_MEDIA");
if (IN_SET(c->media_feature, FEATURE_MO_SE, FEATURE_MO_WO, FEATURE_MO_AS))
printf("ID_CDROM_MEDIA_MO=1\n");
if (IN_SET(c->media_feature, FEATURE_DVD_RW_RO, FEATURE_DVD_RW_SEQ))
printf("ID_CDROM_MEDIA_DVD_RW=1\n");
if (IN_SET(c->media_feature, FEATURE_DVD_R_DL_SEQ, FEATURE_DVD_R_DL_JR))
printf("ID_CDROM_MEDIA_DVD_R_DL=1\n");
if (c->media_feature == FEATURE_DVD_R_DDR)
printf("ID_CDROM_MEDIA_DVD_R=1\n");
if (IN_SET(c->media_feature, FEATURE_BD_R_SRM, FEATURE_BD_R_RRM))
printf("ID_CDROM_MEDIA_BD_R=1\n");
}
state = media_state_to_string(c->media_state);
if (state)
printf("ID_CDROM_MEDIA_STATE=%s\n", state);
if (c->media_session_next > 0)
printf("ID_CDROM_MEDIA_SESSION_NEXT=%u\n", c->media_session_next);
if (c->media_session_count > 0)
printf("ID_CDROM_MEDIA_SESSION_COUNT=%u\n", c->media_session_count);
if (c->media_session_count > 1 && c->media_session_last_offset > 0)
printf("ID_CDROM_MEDIA_SESSION_LAST_OFFSET=%" PRIu64 "\n", c->media_session_last_offset);
if (c->media_track_count > 0)
printf("ID_CDROM_MEDIA_TRACK_COUNT=%u\n", c->media_track_count);
if (c->media_track_count_audio > 0)
printf("ID_CDROM_MEDIA_TRACK_COUNT_AUDIO=%u\n", c->media_track_count_audio);
if (c->media_track_count_data > 0)
printf("ID_CDROM_MEDIA_TRACK_COUNT_DATA=%u\n", c->media_track_count_data);
}
static int help(void) {
printf("Usage: %s [options] <device>\n"
" -l --lock-media lock the media (to enable eject request events)\n"
" -u --unlock-media unlock the media\n"
" -e --eject-media eject the media\n"
" -d --debug print debug messages to stderr\n"
" -h --help print this help text\n"
"\n",
program_invocation_short_name);
return 0;
}
static int parse_argv(int argc, char *argv[]) {
static const struct option options[] = {
{ "lock-media", no_argument, NULL, 'l' },
{ "unlock-media", no_argument, NULL, 'u' },
{ "eject-media", no_argument, NULL, 'e' },
{ "debug", no_argument, NULL, 'd' },
{ "help", no_argument, NULL, 'h' },
{}
};
int c;
while ((c = getopt_long(argc, argv, "deluh", options, NULL)) >= 0)
switch (c) {
case 'l':
arg_lock = true;
break;
case 'u':
arg_unlock = true;
break;
case 'e':
arg_eject = true;
break;
case 'd':
log_set_target(LOG_TARGET_CONSOLE);
log_set_max_level(LOG_DEBUG);
log_open();
break;
case 'h':
return help();
default:
assert_not_reached("Unknown option");
}
arg_node = argv[optind];
if (!arg_node)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No device is specified.");
return 1;
}
int main(int argc, char *argv[]) {
_cleanup_(context_clear) Context c;
int r, rc = 0;
log_set_target(LOG_TARGET_AUTO);
udev_parse_config();
log_parse_environment();
log_open();
context_init(&c);
r = parse_argv(argc, argv);
if (r <= 0) {
rc = r < 0;
goto exit;
}
if (open_drive(&c) < 0) {
rc = 1;
goto exit;
}
/* same data as original cdrom_id */
if (cd_capability_compat(&c) < 0) {
rc = 1;
goto exit;
}
/* check for media - don't bail if there's no media as we still need to
* to read profiles */
(void) cd_media_compat(&c);
/* check if drive talks MMC */
if (cd_inquiry(&c) < 0)
goto work;
/* read drive and possibly current profile */
r = cd_profiles(&c);
if (r > 0) {
/* at this point we are guaranteed to have media in the drive - find out more about it */
/* get session/track info */
(void) cd_media_toc(&c);
/* get writable media state */
(void) cd_media_info(&c);
}
work:
/* lock the media, so we enable eject button events */
if (arg_lock && c.has_media) {
log_debug("PREVENT_ALLOW_MEDIUM_REMOVAL (lock)");
(void) media_lock(c.fd, true);
}
if (arg_unlock && c.has_media) {
log_debug("PREVENT_ALLOW_MEDIUM_REMOVAL (unlock)");
(void) media_lock(c.fd, false);
}
if (arg_eject) {
log_debug("PREVENT_ALLOW_MEDIUM_REMOVAL (unlock)");
(void) media_lock(c.fd, false);
log_debug("START_STOP_UNIT (eject)");
(void) media_eject(c.fd);
}
print_properties(&c);
exit:
log_close();
return rc;
}