| 1 | /* |
| 2 | SPDX-FileCopyrightText: 2010 Michael Zanetti <mzanetti@kde.org> |
| 3 | SPDX-FileCopyrightText: 2010-2012 Lukáš Tinkl <ltinkl@redhat.com> |
| 4 | |
| 5 | SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL |
| 6 | */ |
| 7 | |
| 8 | #include "udisksopticaldisc.h" |
| 9 | #include <fcntl.h> |
| 10 | #include <sys/stat.h> |
| 11 | #include <sys/types.h> |
| 12 | #include <unistd.h> |
| 13 | |
| 14 | #include <QMap> |
| 15 | #include <QSharedMemory> |
| 16 | #include <QSystemSemaphore> |
| 17 | #include <QThreadStorage> |
| 18 | |
| 19 | #include "soliddefs_p.h" |
| 20 | #include "udisks2.h" |
| 21 | #include "udisks_debug.h" |
| 22 | |
| 23 | // inspired by http://cgit.freedesktop.org/hal/tree/hald/linux/probing/probe-volume.c |
| 24 | static Solid::OpticalDisc::ContentType advancedDiscDetect(const QByteArray &device_file) |
| 25 | { |
| 26 | /* the discs block size */ |
| 27 | unsigned short bs; |
| 28 | /* the path table size */ |
| 29 | unsigned short ts; |
| 30 | /* the path table location (in blocks) */ |
| 31 | unsigned int tl; |
| 32 | /* length of the directory name in current path table entry */ |
| 33 | unsigned char len_di = 0; |
| 34 | /* the number of the parent directory's path table entry */ |
| 35 | unsigned int parent = 0; |
| 36 | /* filename for the current path table entry */ |
| 37 | char dirname[256]; |
| 38 | /* our position into the path table */ |
| 39 | int pos = 0; |
| 40 | /* the path table record we're on */ |
| 41 | int curr_record = 1; |
| 42 | /* import debug category */ |
| 43 | using Solid::Backends::UDisks2::UDISKS2; |
| 44 | |
| 45 | Solid::OpticalDisc::ContentType result = Solid::OpticalDisc::NoContent; |
| 46 | |
| 47 | int fd = open(file: device_file.constData(), O_RDONLY); |
| 48 | |
| 49 | /* read the block size */ |
| 50 | lseek(fd: fd, offset: 0x8080, SEEK_CUR); |
| 51 | if (read(fd: fd, buf: &bs, nbytes: 2) != 2) { |
| 52 | qCDebug(UDISKS2, "Advanced probing on %s failed while reading block size" , device_file.constData()); |
| 53 | goto out; |
| 54 | } |
| 55 | |
| 56 | /* read in size of path table */ |
| 57 | lseek(fd: fd, offset: 2, SEEK_CUR); |
| 58 | if (read(fd: fd, buf: &ts, nbytes: 2) != 2) { |
| 59 | qCDebug(UDISKS2, "Advanced probing on %s failed while reading path table size" , device_file.constData()); |
| 60 | goto out; |
| 61 | } |
| 62 | |
| 63 | /* read in which block path table is in */ |
| 64 | lseek(fd: fd, offset: 6, SEEK_CUR); |
| 65 | if (read(fd: fd, buf: &tl, nbytes: 4) != 4) { |
| 66 | qCDebug(UDISKS2, "Advanced probing on %s failed while reading path table block" , device_file.constData()); |
| 67 | goto out; |
| 68 | } |
| 69 | |
| 70 | /* seek to the path table */ |
| 71 | lseek(fd: fd, offset: bs * tl, SEEK_SET); |
| 72 | |
| 73 | /* loop through the path table entries */ |
| 74 | while (pos < ts) { |
| 75 | /* get the length of the filename of the current entry */ |
| 76 | if (read(fd: fd, buf: &len_di, nbytes: 1) != 1) { |
| 77 | qCDebug(UDISKS2, "Advanced probing on %s failed, cannot read more entries" , device_file.constData()); |
| 78 | break; |
| 79 | } |
| 80 | |
| 81 | /* get the record number of this entry's parent |
| 82 | i'm pretty sure that the 1st entry is always the top directory */ |
| 83 | lseek(fd: fd, offset: 5, SEEK_CUR); |
| 84 | if (read(fd: fd, buf: &parent, nbytes: 2) != 2) { |
| 85 | qCDebug(UDISKS2, "Advanced probing on %s failed, couldn't read parent entry" , device_file.constData()); |
| 86 | break; |
| 87 | } |
| 88 | |
| 89 | /* read the name */ |
| 90 | if (read(fd: fd, buf: dirname, nbytes: len_di) != len_di) { |
| 91 | qCDebug(UDISKS2, "Advanced probing on %s failed, couldn't read the entry name" , device_file.constData()); |
| 92 | break; |
| 93 | } |
| 94 | dirname[len_di] = 0; |
| 95 | |
| 96 | /* if we found a folder that has the root as a parent, and the directory name matches |
| 97 | one of the special directories then set the properties accordingly */ |
| 98 | if (parent == 1) { |
| 99 | if (!strcasecmp(s1: dirname, s2: "VIDEO_TS" )) { |
| 100 | qCDebug(UDISKS2, "Disc in %s is a Video DVD" , device_file.constData()); |
| 101 | result = Solid::OpticalDisc::VideoDvd; |
| 102 | break; |
| 103 | } else if (!strcasecmp(s1: dirname, s2: "BDMV" )) { |
| 104 | qCDebug(UDISKS2, "Disc in %s is a Blu-ray video disc" , device_file.constData()); |
| 105 | result = Solid::OpticalDisc::VideoBluRay; |
| 106 | break; |
| 107 | } else if (!strcasecmp(s1: dirname, s2: "VCD" )) { |
| 108 | qCDebug(UDISKS2, "Disc in %s is a Video CD" , device_file.constData()); |
| 109 | result = Solid::OpticalDisc::VideoCd; |
| 110 | break; |
| 111 | } else if (!strcasecmp(s1: dirname, s2: "SVCD" )) { |
| 112 | qCDebug(UDISKS2, "Disc in %s is a Super Video CD" , device_file.constData()); |
| 113 | result = Solid::OpticalDisc::SuperVideoCd; |
| 114 | break; |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | /* all path table entries are padded to be even, |
| 119 | so if this is an odd-length table, seek a byte to fix it */ |
| 120 | if (len_di % 2 == 1) { |
| 121 | lseek(fd: fd, offset: 1, SEEK_CUR); |
| 122 | pos++; |
| 123 | } |
| 124 | |
| 125 | /* update our position */ |
| 126 | pos += 8 + len_di; |
| 127 | curr_record++; |
| 128 | } |
| 129 | |
| 130 | close(fd: fd); |
| 131 | return result; |
| 132 | |
| 133 | out: |
| 134 | /* go back to the start of the file */ |
| 135 | lseek(fd: fd, offset: 0, SEEK_SET); |
| 136 | close(fd: fd); |
| 137 | return result; |
| 138 | } |
| 139 | |
| 140 | using namespace Solid::Backends::UDisks2; |
| 141 | |
| 142 | class ContentTypesCache |
| 143 | { |
| 144 | public: |
| 145 | ContentTypesCache() |
| 146 | : m_n(0) |
| 147 | { |
| 148 | } |
| 149 | |
| 150 | void add(const OpticalDisc::Identity &key, Solid::OpticalDisc::ContentTypes content) |
| 151 | { |
| 152 | if (!find(key)) { |
| 153 | m_n = qMin(a: m_n + 1, b: sizeof(m_info) / sizeof(*m_info)); |
| 154 | moveToFront(i: m_n - 1); |
| 155 | front().first = key; |
| 156 | } |
| 157 | front().second = content; |
| 158 | } |
| 159 | |
| 160 | bool find(const OpticalDisc::Identity &key) |
| 161 | { |
| 162 | for (size_t i = 0; i < m_n; i++) { |
| 163 | if (m_info[i].first == key) { |
| 164 | moveToFront(i); |
| 165 | return true; |
| 166 | } |
| 167 | } |
| 168 | return false; |
| 169 | } |
| 170 | |
| 171 | QPair<OpticalDisc::Identity, Solid::OpticalDisc::ContentTypes> &front() |
| 172 | { |
| 173 | return *m_info; |
| 174 | } |
| 175 | |
| 176 | private: |
| 177 | void moveToFront(size_t i) |
| 178 | { |
| 179 | while (i) { |
| 180 | qSwap(value1&: m_info[i - 1], value2&: m_info[i]); |
| 181 | --i; |
| 182 | } |
| 183 | } |
| 184 | |
| 185 | size_t m_n; |
| 186 | QPair<OpticalDisc::Identity, Solid::OpticalDisc::ContentTypes> m_info[100]; |
| 187 | }; |
| 188 | |
| 189 | class SharedContentTypesCache |
| 190 | { |
| 191 | private: |
| 192 | ContentTypesCache *m_pointer; |
| 193 | QSystemSemaphore m_semaphore; |
| 194 | QSharedMemory m_shmem; |
| 195 | |
| 196 | struct Unlocker { |
| 197 | public: |
| 198 | Unlocker(QSharedMemory *mem) |
| 199 | : m_mem(mem) |
| 200 | { |
| 201 | } |
| 202 | ~Unlocker() |
| 203 | { |
| 204 | m_mem->unlock(); |
| 205 | } |
| 206 | Unlocker(const Unlocker &) = delete; |
| 207 | Unlocker &operator=(const Unlocker &) = delete; |
| 208 | |
| 209 | private: |
| 210 | QSharedMemory *m_mem; |
| 211 | }; |
| 212 | |
| 213 | struct Releaser { |
| 214 | public: |
| 215 | Releaser(QSystemSemaphore *sem) |
| 216 | : m_sem(sem) |
| 217 | { |
| 218 | } |
| 219 | ~Releaser() |
| 220 | { |
| 221 | m_sem->release(); |
| 222 | } |
| 223 | Releaser(const Releaser &) = delete; |
| 224 | Releaser &operator=(const Releaser &) = delete; |
| 225 | |
| 226 | private: |
| 227 | QSystemSemaphore *m_sem; |
| 228 | }; |
| 229 | |
| 230 | static QString getKey() |
| 231 | { |
| 232 | static const QString keyTemplate(QStringLiteral("solid-disk-info-1-%1-%2" )); |
| 233 | static const QString tableSize(QString::number(sizeof(ContentTypesCache))); |
| 234 | |
| 235 | return keyTemplate.arg(args: tableSize, args: QString::number(geteuid())); |
| 236 | } |
| 237 | |
| 238 | public: |
| 239 | SharedContentTypesCache() |
| 240 | : m_pointer(nullptr) |
| 241 | , m_semaphore(getKey() + QStringLiteral("sem" ), 1) |
| 242 | , m_shmem(getKey() + QStringLiteral("mem" )) |
| 243 | { |
| 244 | if (!m_semaphore.acquire()) { |
| 245 | return; |
| 246 | } |
| 247 | Releaser releaser(&m_semaphore); |
| 248 | |
| 249 | if (m_shmem.attach()) { |
| 250 | m_pointer = reinterpret_cast<ContentTypesCache *>(m_shmem.data()); |
| 251 | return; |
| 252 | } |
| 253 | |
| 254 | if (!m_shmem.create(size: sizeof(ContentTypesCache))) { |
| 255 | return; |
| 256 | } |
| 257 | |
| 258 | if (!m_shmem.lock()) { |
| 259 | m_shmem.detach(); |
| 260 | return; |
| 261 | } |
| 262 | Unlocker unlocker(&m_shmem); |
| 263 | |
| 264 | m_pointer = new (m_shmem.data()) ContentTypesCache; |
| 265 | } |
| 266 | |
| 267 | Solid::OpticalDisc::ContentTypes getContent(const OpticalDisc::Identity &info, const QByteArray &file) |
| 268 | { |
| 269 | if (!m_pointer) { |
| 270 | return advancedDiscDetect(device_file: file); |
| 271 | } |
| 272 | |
| 273 | if (!m_semaphore.acquire()) { |
| 274 | return advancedDiscDetect(device_file: file); |
| 275 | } |
| 276 | Releaser releaser(&m_semaphore); |
| 277 | |
| 278 | if (!m_shmem.lock()) { |
| 279 | return advancedDiscDetect(device_file: file); |
| 280 | } |
| 281 | Unlocker unlocker(&m_shmem); |
| 282 | |
| 283 | if (!m_pointer->find(key: info)) { |
| 284 | m_pointer->add(key: info, content: advancedDiscDetect(device_file: file)); |
| 285 | } |
| 286 | |
| 287 | Solid::OpticalDisc::ContentTypes content = m_pointer->front().second; |
| 288 | return content; |
| 289 | } |
| 290 | |
| 291 | ~SharedContentTypesCache() |
| 292 | { |
| 293 | m_semaphore.acquire(); |
| 294 | Releaser releaser(&m_semaphore); |
| 295 | m_shmem.detach(); |
| 296 | } |
| 297 | }; |
| 298 | |
| 299 | Q_GLOBAL_STATIC(QThreadStorage<SharedContentTypesCache>, sharedContentTypesCache) |
| 300 | |
| 301 | OpticalDisc::Identity::Identity() |
| 302 | : m_detectTime(0) |
| 303 | , m_size(0) |
| 304 | , m_labelHash(0) |
| 305 | { |
| 306 | } |
| 307 | |
| 308 | OpticalDisc::Identity::Identity(const Device &device, const Device &drive) |
| 309 | : m_detectTime(drive.prop(QStringLiteral("TimeMediaDetected" )).toLongLong()) |
| 310 | , m_size(device.prop(QStringLiteral("Size" )).toLongLong()) |
| 311 | , m_labelHash(qHash(key: device.prop(QStringLiteral("IdLabel" )).toString())) |
| 312 | { |
| 313 | } |
| 314 | |
| 315 | bool OpticalDisc::Identity::operator==(const OpticalDisc::Identity &b) const |
| 316 | { |
| 317 | /* clang-format off */ |
| 318 | return m_detectTime == b.m_detectTime |
| 319 | && m_size == b.m_size |
| 320 | && m_labelHash == b.m_labelHash; |
| 321 | /* clang-format on */ |
| 322 | } |
| 323 | |
| 324 | OpticalDisc::OpticalDisc(Device *dev) |
| 325 | : StorageVolume(dev) |
| 326 | { |
| 327 | #if UDEV_FOUND |
| 328 | UdevQt::Client client(this); |
| 329 | m_udevDevice = client.deviceByDeviceFile(deviceFile: device()); |
| 330 | // qDebug() << "udev device:" << m_udevDevice.name() << "valid:" << m_udevDevice.isValid(); |
| 331 | /*qDebug() << "\tProperties:" << */ m_udevDevice.deviceProperties(); // initialize the properties DB so that it doesn't crash further down, #298416 |
| 332 | #endif |
| 333 | |
| 334 | m_drive = new Device(m_device->drivePath()); |
| 335 | } |
| 336 | |
| 337 | OpticalDisc::~OpticalDisc() |
| 338 | { |
| 339 | delete m_drive; |
| 340 | } |
| 341 | |
| 342 | qulonglong OpticalDisc::capacity() const |
| 343 | { |
| 344 | return m_device->prop(QStringLiteral("Size" )).toULongLong(); |
| 345 | } |
| 346 | |
| 347 | bool OpticalDisc::isRewritable() const |
| 348 | { |
| 349 | // the hard way, udisks has no notion of a disc "rewritability" |
| 350 | const QString mediaType = media(); |
| 351 | /* clang-format off */ |
| 352 | return mediaType == QLatin1String("optical_cd_rw" ) |
| 353 | || mediaType == QLatin1String("optical_dvd_rw" ) |
| 354 | || mediaType == QLatin1String("optical_dvd_ram" ) |
| 355 | || mediaType == QLatin1String("optical_dvd_plus_rw" ) |
| 356 | || mediaType == QLatin1String("optical_dvd_plus_rw_dl" ) |
| 357 | || mediaType == QLatin1String("optical_bd_re" ) |
| 358 | || mediaType == QLatin1String("optical_hddvd_rw" ); |
| 359 | /* clang-format on */ |
| 360 | } |
| 361 | |
| 362 | bool OpticalDisc::isBlank() const |
| 363 | { |
| 364 | return m_drive->prop(QStringLiteral("OpticalBlank" )).toBool(); |
| 365 | } |
| 366 | |
| 367 | bool OpticalDisc::isAppendable() const |
| 368 | { |
| 369 | // qDebug() << "appendable prop" << m_udevDevice.deviceProperty("ID_CDROM_MEDIA_STATE"); |
| 370 | #if UDEV_FOUND |
| 371 | return m_udevDevice.deviceProperty(QStringLiteral("ID_CDROM_MEDIA_STATE" )).toString() == QLatin1String("appendable" ); |
| 372 | #elif defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) |
| 373 | return m_device->prop(QStringLiteral("bsdisks_IsAppendable" )).toBool(); |
| 374 | #else |
| 375 | #error Implement this or stub this out for your platform |
| 376 | #endif |
| 377 | } |
| 378 | |
| 379 | Solid::OpticalDisc::DiscType OpticalDisc::discType() const |
| 380 | { |
| 381 | const QMap<Solid::OpticalDisc::DiscType, QString> map{ |
| 382 | {Solid::OpticalDisc::CdRom, QStringLiteral("optical_cd" )}, |
| 383 | {Solid::OpticalDisc::CdRecordable, QStringLiteral("optical_cd_r" )}, |
| 384 | {Solid::OpticalDisc::CdRewritable, QStringLiteral("optical_cd_rw" )}, |
| 385 | {Solid::OpticalDisc::DvdRom, QStringLiteral("optical_dvd" )}, |
| 386 | {Solid::OpticalDisc::DvdRecordable, QStringLiteral("optical_dvd_r" )}, |
| 387 | {Solid::OpticalDisc::DvdRewritable, QStringLiteral("optical_dvd_rw" )}, |
| 388 | {Solid::OpticalDisc::DvdRam, QStringLiteral("optical_dvd_ram" )}, |
| 389 | {Solid::OpticalDisc::DvdPlusRecordable, QStringLiteral("optical_dvd_plus_r" )}, |
| 390 | {Solid::OpticalDisc::DvdPlusRewritable, QStringLiteral("optical_dvd_plus_rw" )}, |
| 391 | {Solid::OpticalDisc::DvdPlusRecordableDuallayer, QStringLiteral("optical_dvd_plus_r_dl" )}, |
| 392 | {Solid::OpticalDisc::DvdPlusRewritableDuallayer, QStringLiteral("optical_dvd_plus_rw_dl" )}, |
| 393 | {Solid::OpticalDisc::BluRayRom, QStringLiteral("optical_bd" )}, |
| 394 | {Solid::OpticalDisc::BluRayRecordable, QStringLiteral("optical_bd_r" )}, |
| 395 | {Solid::OpticalDisc::BluRayRewritable, QStringLiteral("optical_bd_re" )}, |
| 396 | {Solid::OpticalDisc::HdDvdRom, QStringLiteral("optical_hddvd" )}, |
| 397 | {Solid::OpticalDisc::HdDvdRecordable, QStringLiteral("optical_hddvd_r" )}, |
| 398 | {Solid::OpticalDisc::HdDvdRewritable, QStringLiteral("optical_hddvd_rw" )}, |
| 399 | }; |
| 400 | // TODO add these to Solid |
| 401 | // map[Solid::OpticalDisc::MagnetoOptical] ="optical_mo"; |
| 402 | // map[Solid::OpticalDisc::MountRainer] ="optical_mrw"; |
| 403 | // map[Solid::OpticalDisc::MountRainerWritable] ="optical_mrw_w"; |
| 404 | |
| 405 | return map.key(value: media(), defaultKey: Solid::OpticalDisc::UnknownDiscType); // FIXME optimize, lookup by value, not key |
| 406 | } |
| 407 | |
| 408 | Solid::OpticalDisc::ContentTypes OpticalDisc::availableContent() const |
| 409 | { |
| 410 | if (isBlank()) { |
| 411 | return Solid::OpticalDisc::NoContent; |
| 412 | } |
| 413 | |
| 414 | Solid::OpticalDisc::ContentTypes content = Solid::OpticalDisc::NoContent; |
| 415 | const bool hasData = m_drive->prop(QStringLiteral("OpticalNumDataTracks" )).toUInt() > 0; |
| 416 | const bool hasAudio = m_drive->prop(QStringLiteral("OpticalNumAudioTracks" )).toUInt() > 0; |
| 417 | |
| 418 | if (hasData) { |
| 419 | content |= Solid::OpticalDisc::Data; |
| 420 | |
| 421 | Identity newIdentity(*m_device, *m_drive); |
| 422 | if (!(m_identity == newIdentity)) { |
| 423 | QByteArray deviceFile(m_device->prop(QStringLiteral("Device" )).toByteArray()); |
| 424 | m_cachedContent = sharedContentTypesCache->localData().getContent(info: newIdentity, file: deviceFile); |
| 425 | m_identity = newIdentity; |
| 426 | } |
| 427 | |
| 428 | content |= m_cachedContent; |
| 429 | } |
| 430 | if (hasAudio) { |
| 431 | content |= Solid::OpticalDisc::Audio; |
| 432 | } |
| 433 | |
| 434 | return content; |
| 435 | } |
| 436 | |
| 437 | QString OpticalDisc::media() const |
| 438 | { |
| 439 | return m_drive->prop(QStringLiteral("Media" )).toString(); |
| 440 | } |
| 441 | |
| 442 | #include "moc_udisksopticaldisc.cpp" |
| 443 | |