diff --git a/debian/control b/debian/control index 578fd30c..866047ad 100644 --- a/debian/control +++ b/debian/control @@ -23,6 +23,7 @@ Build-Depends: libglib2.0-dev, libudisks2-dev, libisoburn-dev, + libssl-dev, libmediainfo-dev, libsecret-1-dev, liblucene++-dev, diff --git a/debian/control.in b/debian/control.in index 78486393..6335b292 100644 --- a/debian/control.in +++ b/debian/control.in @@ -14,6 +14,7 @@ Build-Depends: libglib2.0-dev, libudisks2-dev, libisoburn-dev, + libssl-dev, libmediainfo-dev, libsecret-1-dev, liblucene++-dev, diff --git a/debian/libdfm-burn.install b/debian/libdfm-burn.install index 7a3cbf7c..c7b635c0 100644 --- a/debian/libdfm-burn.install +++ b/debian/libdfm-burn.install @@ -1 +1,2 @@ /usr/lib/*/libdfm-burn*.so* +usr/bin/dfm-burner diff --git a/debian/libdfm6-burn.install b/debian/libdfm6-burn.install index dca97207..762a9c34 100644 --- a/debian/libdfm6-burn.install +++ b/debian/libdfm6-burn.install @@ -1 +1,2 @@ /usr/lib/*/libdfm6-burn*.so* +usr/bin/dfm-burner diff --git a/include/dfm-burn/dfm-burn/dburn_global.h b/include/dfm-burn/dfm-burn/dburn_global.h index 674650fa..4a7fdec9 100644 --- a/include/dfm-burn/dfm-burn/dburn_global.h +++ b/include/dfm-burn/dfm-burn/dburn_global.h @@ -22,6 +22,7 @@ enum class BurnOption : unsigned int { kJolietSupport = 1 << 4, // add joliet extension kRockRidgeSupport = 1 << 5, // add rockridge extension kUDF102Supported = 1 << 6, + kChecksum = 1 << 7, // SM3 checksum verification kJolietAndRockRidge = kJolietSupport | kRockRidgeSupport // add both of them, not used yet }; Q_DECLARE_FLAGS(BurnOptions, BurnOption) diff --git a/include/dfm-burn/dfm-burn/dopticaldiscmanager.h b/include/dfm-burn/dfm-burn/dopticaldiscmanager.h index d092d0fd..78c74c4c 100644 --- a/include/dfm-burn/dfm-burn/dopticaldiscmanager.h +++ b/include/dfm-burn/dfm-burn/dopticaldiscmanager.h @@ -33,6 +33,8 @@ class DOpticalDiscManager : public QObject bool checkmedia(double *qgood, double *qslow, double *qbad); bool writeISO(const QString &isoPath, int speed = 0); bool dumpISO(const QString &isoPath); + bool generateChecksumManifest(const QString &savePath); + bool verifyChecksum(const QString &manifestPath); QString lastError() const; Q_SIGNALS: diff --git a/src/dfm-burn/dfm-burn-client/CMakeLists.txt b/src/dfm-burn/dfm-burn-client/CMakeLists.txt index b32dcfaf..67f027c7 100644 --- a/src/dfm-burn/dfm-burn-client/CMakeLists.txt +++ b/src/dfm-burn/dfm-burn-client/CMakeLists.txt @@ -1,21 +1,23 @@ cmake_minimum_required(VERSION 3.10) -project(dfm${DFM_VERSION_MAJOR}-burn-client) +project(dfm-burner) set(SRCS main.cpp + cli_options.cpp + cli_options.h + burn_utils.cpp + burn_utils.h ) find_package(Qt${QT_VERSION_MAJOR}Core REQUIRED) add_executable(${PROJECT_NAME} ${SRCS}) -include_directories( - ${PROJECT_SOURCE_DIR}/../dfm-burn-lib/inlcude -) - target_link_libraries( ${PROJECT_NAME} Qt${QT_VERSION_MAJOR}::Core dfm${DFM_VERSION_MAJOR}-burn ) + +install(TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/src/dfm-burn/dfm-burn-client/README.md b/src/dfm-burn/dfm-burn-client/README.md new file mode 100644 index 00000000..49c37ae1 --- /dev/null +++ b/src/dfm-burn/dfm-burn-client/README.md @@ -0,0 +1,169 @@ +# Optical Disc Burner Utilities + +A command-line tool for optical disc operations in Deepin File Manager, powered by libisoburn. + +## Features + +- **Disc information**: Query device, media type, capacity, write speeds +- **Burn files**: Burn files and directories to disc with various filesystem options +- **ISO operations**: Write ISO images to disc, or dump disc content to ISO +- **Disc erase**: Erase rewritable discs (CD-RW, DVD-RW, DVD+RW, BD-RE) +- **Quality check**: Verify disc media quality with detailed statistics +- **Packet writing**: UDF packet writing for incremental file operations + +## Supported Media Types + +CD-ROM, CD-R, CD-RW, DVD-ROM, DVD-R, DVD-RW, DVD+R, DVD+R DL, DVD-R DL, DVD-RAM, DVD+RW, BD-ROM, BD-R, BD-RE + +## Usage + +``` +dfm-burner [options] [arguments] +``` + +### Commands + +| Command | Description | +|---------|-------------| +| `info` | Show optical disc information | +| `burn` | Burn files to disc | +| `write-iso` | Write an ISO image to disc | +| `dump-iso` | Dump disc content to an ISO image | +| `erase` | Erase a rewritable disc | +| `check` | Check media quality | +| `pw` | Packet writing (UDF) operations | + +### info — Show disc information + +``` +dfm-burner info [--json] +``` + +```bash +# Show disc info +dfm-burner info /dev/sr0 + +# JSON output for scripting +dfm-burner info --json /dev/sr0 +``` + +### burn — Burn files to disc + +``` +dfm-burner burn [options] [...] +``` + +Options: +- `--volume-id=` — Set disc label shown in file manager (default: ISOIMAGE) +- `--speed=` — Write speed in CD/DVD multiplier, 0 = auto (default) + +Filesystem (pick one, default is iso9660): +- `--iso9660` — Basic format, works on virtually all systems +- `--joliet` — Long filenames and CJK characters, recommended for Windows +- `--rockridge` — Preserves Linux permissions and symlinks, Linux-only +- `--udf` — Modern format, files > 2GB, Unicode names. Best for mixed-OS + +Behavior: +- `--appendable` — Leave disc open to add more data later (multi-session) +- `--verify` — Read back data after burning to detect errors + +```bash +# Burn a directory +dfm-burner burn /dev/sr0 /home/user/data + +# Burn with custom volume label +dfm-burner burn --volume-id=BACKUP /dev/sr0 /home/user/files + +# Burn with UDF and appendable (multi-session) +dfm-burner burn --udf --appendable /dev/sr0 /home/user/archive + +# Burn with Joliet + Rock Ridge + verification +dfm-burner burn --joliet --rockridge --verify /dev/sr0 file1.txt file2.txt +``` + +### write-iso — Write ISO to disc + +``` +dfm-burner write-iso [--speed=] +``` + +```bash +dfm-burner write-iso /dev/sr0 /home/user/image.iso +dfm-burner write-iso --speed=8 /dev/sr0 /home/user/image.iso +``` + +### dump-iso — Dump disc to ISO + +``` +dfm-burner dump-iso +``` + +```bash +dfm-burner dump-iso /dev/sr0 /home/user/backup.iso +``` + +### erase — Erase disc + +``` +dfm-burner erase +``` + +```bash +dfm-burner erase /dev/sr0 +``` + +### check — Check media quality + +``` +dfm-burner check [--json] +``` + +```bash +dfm-burner check /dev/sr0 +dfm-burner check --json /dev/sr0 +``` + +### pw — Packet writing (UDF) + +``` +dfm-burner pw [additional args] +``` + +Actions: +- `open ` — Open a packet writing session +- `close ` — Close the session +- `put ` — Add a file to the disc +- `mv ` — Rename a file on the disc +- `rm ` — Remove a file from the disc + +```bash +# Open packet writing session +dfm-burner pw open /dev/sr0 /home/user/pw-work + +# Add files incrementally +dfm-burner pw put /dev/sr0 /home/user/pw-work /home/user/data.txt +dfm-burner pw put /dev/sr0 /home/user/pw-work /home/user/photo.jpg + +# Rename a file on disc +dfm-burner pw mv /dev/sr0 /home/user/pw-work old_name.txt new_name.txt + +# Remove a file from disc +dfm-burner pw rm /dev/sr0 /home/user/pw-work unwanted.txt + +# Close session +dfm-burner pw close /dev/sr0 /home/user/pw-work +``` + +## Implementation + +The tool wraps all public APIs of the dfm-burn library: + +- `DOpticalDiscInfo` — Disc properties and capabilities +- `DOpticalDiscManager` — Burn, erase, check, and ISO operations +- `DPacketWritingController` — UDF packet writing operations + +Async operations (burn, write-iso, dump-iso, erase, check) display real-time progress via Qt's event loop. Sync operations (info, pw-*) return immediately. + +## License + +GPL-3.0-or-later diff --git a/src/dfm-burn/dfm-burn-client/burn_utils.cpp b/src/dfm-burn/dfm-burn-client/burn_utils.cpp new file mode 100644 index 00000000..81366f88 --- /dev/null +++ b/src/dfm-burn/dfm-burn-client/burn_utils.cpp @@ -0,0 +1,125 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "burn_utils.h" + +DFM_BURN_BEGIN_NS + +QString formatSize(quint64 bytes) +{ + if (bytes == 0) + return "0 B"; + + const QStringList units = { "B", "KB", "MB", "GB", "TB" }; + int idx = 0; + double size = bytes; + + while (size >= 1024.0 && idx < units.size() - 1) { + size /= 1024.0; + ++idx; + } + + if (idx == 0) + return QString("%1 B").arg(bytes); + + return QString("%1 %2").arg(size, 0, 'f', 1).arg(units.at(idx)); +} + +QString mediaTypeName(MediaType type) +{ + switch (type) { + case MediaType::kNoMedia: + return "No Media"; + case MediaType::kCD_ROM: + return "CD-ROM"; + case MediaType::kCD_R: + return "CD-R"; + case MediaType::kCD_RW: + return "CD-RW"; + case MediaType::kDVD_ROM: + return "DVD-ROM"; + case MediaType::kDVD_R: + return "DVD-R"; + case MediaType::kDVD_RW: + return "DVD-RW"; + case MediaType::kDVD_PLUS_R: + return "DVD+R"; + case MediaType::kDVD_PLUS_R_DL: + return "DVD+R DL"; + case MediaType::kDVD_R_DL: + return "DVD-R DL"; + case MediaType::kDVD_RAM: + return "DVD-RAM"; + case MediaType::kDVD_PLUS_RW: + return "DVD+RW"; + case MediaType::kBD_ROM: + return "BD-ROM"; + case MediaType::kBD_R: + return "BD-R"; + case MediaType::kBD_RE: + return "BD-RE"; + } + return "Unknown"; +} + +QString jobStatusName(JobStatus status) +{ + switch (status) { + case JobStatus::kIdle: + return "Idle"; + case JobStatus::kRunning: + return "Running"; + case JobStatus::kStalled: + return "Stalled"; + case JobStatus::kFinished: + return "Finished"; + case JobStatus::kFailed: + return "Failed"; + } + return "Unknown"; +} + +QString burnOptionsSummary(BurnOptions options) +{ + QStringList parts; + + // Filesystem + if (options.testFlag(BurnOption::kUDF102Supported)) + parts << "UDF filesystem"; + else if (options.testFlag(BurnOption::kJolietSupport) && options.testFlag(BurnOption::kRockRidgeSupport)) + parts << "ISO9660 + Joliet + RockRidge"; + else if (options.testFlag(BurnOption::kJolietSupport)) + parts << "ISO9660 + Joliet"; + else if (options.testFlag(BurnOption::kRockRidgeSupport)) + parts << "ISO9660 + RockRidge"; + else + parts << "ISO9660 filesystem"; + + // Behavior + if (options.testFlag(BurnOption::kKeepAppendable)) + parts << "multi-session (disc stays open)"; + if (options.testFlag(BurnOption::kVerifyDatas)) + parts << "verify after burn"; + + return parts.join(", "); +} + +/** + * @brief Check if a media type is rewritable + */ +bool isRewritable(MediaType type) +{ + switch (type) { + case MediaType::kCD_RW: + case MediaType::kDVD_RW: + case MediaType::kDVD_PLUS_RW: + case MediaType::kDVD_RAM: + case MediaType::kBD_RE: + return true; + default: + return false; + } +} + +DFM_BURN_END_NS diff --git a/src/dfm-burn/dfm-burn-client/burn_utils.h b/src/dfm-burn/dfm-burn-client/burn_utils.h new file mode 100644 index 00000000..019ae000 --- /dev/null +++ b/src/dfm-burn/dfm-burn-client/burn_utils.h @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef BURN_UTILS_H +#define BURN_UTILS_H + +#include +#include + +DFM_BURN_BEGIN_NS + +/** + * @brief Format byte size to human-readable string + * @param bytes Size in bytes + * @return Formatted string (e.g., "702.5 MB") + */ +QString formatSize(quint64 bytes); + +/** + * @brief Convert MediaType enum to display name + */ +QString mediaTypeName(MediaType type); + +/** + * @brief Convert JobStatus enum to display name + */ +QString jobStatusName(JobStatus status); + +/** + * @brief Convert BurnOptions flags to human-readable description + * Returns one-line summary like "UDF filesystem, multi-session, verify after burn" + */ +QString burnOptionsSummary(BurnOptions options); + +/** + * @brief Check if a media type is rewritable (can be erased) + */ +bool isRewritable(MediaType type); + +DFM_BURN_END_NS + +#endif // BURN_UTILS_H diff --git a/src/dfm-burn/dfm-burn-client/cli_options.cpp b/src/dfm-burn/dfm-burn-client/cli_options.cpp new file mode 100644 index 00000000..b2a8edfb --- /dev/null +++ b/src/dfm-burn/dfm-burn-client/cli_options.cpp @@ -0,0 +1,693 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "cli_options.h" + +#include +#include + +DFM_BURN_USE_NS + +using namespace std; + +// ── Main parse entry ─────────────────────────────────────────── + +bool CliOptions::parse(int argc, char *argv[], BurnCliConfig &config) +{ + if (argc < 2) { + printHelp(); + return false; + } + + // Collect remaining arguments (skip program name) + QStringList args; + for (int i = 2; i < argc; ++i) + args.append(QString::fromLocal8Bit(argv[i])); + + QString cmd = QString::fromLocal8Bit(argv[1]); + + if (cmd == "--help" || cmd == "-h") { + printHelp(); + return false; + } + + // ── Route to subcommand parser ── + if (cmd == "info") + return parseInfoArgs(args, config); + if (cmd == "burn") + return parseBurnArgs(args, config); + if (cmd == "write-iso") + return parseWriteIsoArgs(args, config); + if (cmd == "dump-iso") + return parseDumpIsoArgs(args, config); + if (cmd == "erase") + return parseEraseArgs(args, config); + if (cmd == "check") + return parseCheckArgs(args, config); + if (cmd == "checksum") { + if (args.isEmpty()) { + cerr << "Error: 'checksum' requires a subcommand (gen, verify)" << endl; + cerr << "Run 'dfm-burner checksum --help' for details." << endl; + return false; + } + QString checksumAction = args.takeFirst(); + if (checksumAction == "gen") + return parseChecksumGenArgs(args, config); + if (checksumAction == "verify") + return parseChecksumVerifyArgs(args, config); + if (checksumAction == "--help" || checksumAction == "-h") { + printCommandHelp(BurnCommand::ChecksumGen); + return false; + } + cerr << "Error: Unknown checksum action '" << checksumAction.toStdString() << "'" << endl; + cerr << "Valid actions: gen, verify" << endl; + return false; + } + if (cmd == "pw") { + if (args.isEmpty()) { + cerr << "Error: 'pw' requires a subcommand (open, close, put, mv, rm)" << endl; + cerr << "Run 'dfm-burner pw --help' for details." << endl; + return false; + } + QString pwAction = args.takeFirst(); + return parsePwArgs(pwAction, args, config); + } + + cerr << "Error: Unknown command '" << cmd.toStdString() << "'" << endl; + printHelp(); + return false; +} + +// ── Help output ──────────────────────────────────────────────── + +void CliOptions::printHelp() const +{ + cout << "Usage: dfm-burner [options] [arguments]" << endl; + cout << endl; + cout << "Optical disc burning tool for Deepin File Manager." << endl; + cout << "Use 'dfm-burner info /dev/sr0' to check disc status before burning." << endl; + cout << endl; + cout << "Commands:" << endl; + cout << " info Show disc info (type, capacity, free space)" << endl; + cout << " burn Burn files or folders to disc" << endl; + cout << " write-iso Write an .iso image to disc" << endl; + cout << " dump-iso Read disc and save as .iso image" << endl; + cout << " erase Erase a rewritable disc (CD-RW, DVD-RW, BD-RE)" << endl; + cout << " check Scan disc for read errors" << endl; + cout << " checksum SM3 checksum verification (gen / verify)" << endl; + cout << " pw Incremental file operations (UDF packet writing)" << endl; + cout << endl; + cout << "Tips:" << endl; + cout << " - Find your device: ls /dev/sr*" << endl; + cout << " - Get started: dfm-burner info /dev/sr0" << endl; + cout << " - Quick burn: dfm-burner burn /dev/sr0 ./my-folder" << endl; + cout << endl; + cout << "Run 'dfm-burner --help' for details." << endl; +} + +void CliOptions::printCommandHelp(BurnCommand cmd) const +{ + switch (cmd) { + case BurnCommand::Info: + cout << "Usage: dfm-burner info [--json] " << endl; + cout << endl; + cout << "Show disc type, capacity, used/free space, and write speeds." << endl; + cout << "Run this first to check what disc is in the drive before burning." << endl; + cout << endl; + cout << "Arguments:" << endl; + cout << " device Device path (e.g., /dev/sr0). Use 'ls /dev/sr*' to find it." << endl; + cout << endl; + cout << "Options:" << endl; + cout << " --json, -j Output in JSON format (for scripting)" << endl; + cout << endl; + cout << "Examples:" << endl; + cout << " dfm-burner info /dev/sr0 # Check disc status" << endl; + cout << " dfm-burner info --json /dev/sr0 # Machine-readable output" << endl; + break; + + case BurnCommand::Burn: + cout << "Usage: dfm-burner burn [options] [...]" << endl; + cout << endl; + cout << "Burn files and directories to an optical disc." << endl; + cout << "Use 'dfm-burner info /dev/sr0' first to check disc type and free space." << endl; + cout << endl; + cout << "Arguments:" << endl; + cout << " device Device path (e.g., /dev/sr0). Use 'ls /dev/sr*' to find it." << endl; + cout << " file_or_dir One or more files or folders to burn." << endl; + cout << endl; + cout << "Options:" << endl; + cout << " --volume-id= Set disc label shown in file manager (default: ISOIMAGE)" << endl; + cout << " --speed= Write speed in CD/DVD multiplier. 0 = auto pick (default)" << endl; + cout << endl; + cout << "Filesystem (pick one, default is iso9660):" << endl; + cout << " --iso9660 Basic format, works on virtually all systems" << endl; + cout << " --joliet Like iso9660 but supports long filenames and CJK" << endl; + cout << " characters, recommended for Windows compatibility" << endl; + cout << " --rockridge Like iso9660 but preserves Linux permissions and" << endl; + cout << " symbolic links, recommended for Linux-only use" << endl; + cout << " --udf Modern format, supports files > 2GB and Unicode" << endl; + cout << " filenames. Best for large files or mixed-OS use." << endl; + cout << " Combine with --appendable for multi-session." << endl; + cout << endl; + cout << "Behavior:" << endl; + cout << " --appendable After burning, leave disc open so you can add more" << endl; + cout << " data in a later session (multi-session disc)" << endl; + cout << " --verify Read back all data after burning to detect errors." << endl; + cout << " Slower but ensures data integrity." << endl; + cout << endl; + cout << "Examples:" << endl; + cout << " # Quick burn with defaults" << endl; + cout << " dfm-burner burn /dev/sr0 ./my-folder" << endl; + cout << endl; + cout << " # Burn with a custom label" << endl; + cout << " dfm-burner burn --volume-id=Photos_2026 /dev/sr0 ./photos" << endl; + cout << endl; + cout << " # Burn a 4GB file (needs UDF)" << endl; + cout << " dfm-burner burn --udf /dev/sr0 ./large-video.mkv" << endl; + cout << endl; + cout << " # Multi-session: burn now, add more files later" << endl; + cout << " dfm-burner burn --udf --appendable /dev/sr0 ./batch1" << endl; + cout << endl; + cout << " # Cross-platform: works on Windows, Linux, macOS" << endl; + cout << " dfm-burner burn --joliet --rockridge --verify /dev/sr0 ./data" << endl; + break; + + case BurnCommand::WriteISO: + cout << "Usage: dfm-burner write-iso [--speed=] " << endl; + cout << endl; + cout << "Write an .iso image file to disc (1:1 copy, disc-at-once)." << endl; + cout << endl; + cout << "Arguments:" << endl; + cout << " device Device path (e.g., /dev/sr0). Use 'ls /dev/sr*' to find it." << endl; + cout << " iso_path Path to the .iso file to burn." << endl; + cout << endl; + cout << "Options:" << endl; + cout << " --speed= Write speed. 0 = auto pick (default)" << endl; + cout << endl; + cout << "Examples:" << endl; + cout << " dfm-burner write-iso /dev/sr0 ./ubuntu-22.04.iso" << endl; + cout << " dfm-burner write-iso --speed=8 /dev/sr0 ./debian-live.iso" << endl; + break; + + case BurnCommand::DumpISO: + cout << "Usage: dfm-burner dump-iso " << endl; + cout << endl; + cout << "Read disc content and save it as an .iso image file (for backup or redistribution)." << endl; + cout << endl; + cout << "Arguments:" << endl; + cout << " device Device path (e.g., /dev/sr0). Use 'ls /dev/sr*' to find it." << endl; + cout << " output_path Where to save the .iso file." << endl; + cout << endl; + cout << "Examples:" << endl; + cout << " dfm-burner dump-iso /dev/sr0 ./backup.iso" << endl; + break; + + case BurnCommand::Erase: + cout << "Usage: dfm-burner erase " << endl; + cout << endl; + cout << "Erase all data on a rewritable disc." << endl; + cout << "Only works on rewritable media: CD-RW, DVD-RW, DVD+RW, BD-RE." << endl; + cout << "CD-R and DVD+R are write-once and cannot be erased." << endl; + cout << endl; + cout << "Arguments:" << endl; + cout << " device Device path (e.g., /dev/sr0). Use 'ls /dev/sr*' to find it." << endl; + cout << endl; + cout << "Examples:" << endl; + cout << " dfm-burner erase /dev/sr0" << endl; + break; + + case BurnCommand::Check: + cout << "Usage: dfm-burner check [--json] " << endl; + cout << endl; + cout << "Read the entire disc to detect read errors and report quality statistics." << endl; + cout << "Useful for verifying old backups or checking disc health." << endl; + cout << endl; + cout << "Arguments:" << endl; + cout << " device Device path (e.g., /dev/sr0). Use 'ls /dev/sr*' to find it." << endl; + cout << endl; + cout << "Options:" << endl; + cout << " --json, -j Output in JSON format (for scripting)" << endl; + cout << endl; + cout << "Examples:" << endl; + cout << " dfm-burner check /dev/sr0" << endl; + cout << " dfm-burner check --json /dev/sr0" << endl; + break; + + case BurnCommand::ChecksumGen: + cout << "Usage: dfm-burner checksum gen --output= " << endl; + cout << endl; + cout << "Generate an SM3 checksum manifest for files before burning." << endl; + cout << "The manifest records the SM3 hash of every file, which can later be" << endl; + cout << "used to verify disc integrity with 'dfm-burner checksum verify'." << endl; + cout << endl; + cout << "Arguments:" << endl; + cout << " source_dir Directory containing files to be burned." << endl; + cout << endl; + cout << "Options:" << endl; + cout << " --output= Where to save the manifest JSON (required)." << endl; + cout << endl; + cout << "Examples:" << endl; + cout << " dfm-burner checksum gen --output=manifest.json ./my-data" << endl; + break; + + case BurnCommand::ChecksumVerify: + cout << "Usage: dfm-burner checksum verify " << endl; + cout << endl; + cout << "Verify burned disc integrity by comparing SM3 checksums." << endl; + cout << "Extracts all files from the disc and compares their SM3 hashes" << endl; + cout << "against the manifest generated by 'dfm-burner checksum gen'." << endl; + cout << endl; + cout << "Arguments:" << endl; + cout << " device Device path (e.g., /dev/sr0). Disc must still be in drive." << endl; + cout << " manifest.json Path to the manifest file created during 'gen'." << endl; + cout << endl; + cout << "Examples:" << endl; + cout << " dfm-burner checksum verify /dev/sr0 manifest.json" << endl; + break; + + case BurnCommand::PwOpen: + case BurnCommand::PwClose: + case BurnCommand::PwPut: + case BurnCommand::PwMv: + case BurnCommand::PwRm: + cout << "Usage: dfm-burner pw [extra_args...]" << endl; + cout << endl; + cout << "Packet writing (UDF) lets you use a rewritable disc like a USB drive:" << endl; + cout << "add, rename, or remove individual files without rewriting the whole disc." << endl; + cout << endl; + cout << "Arguments:" << endl; + cout << " device Device path (e.g., /dev/sr0). Use 'ls /dev/sr*' to find it." << endl; + cout << " working_path Local directory for temporary staging files." << endl; + cout << " Must be on the same filesystem as the files you put." << endl; + cout << endl; + cout << "Actions:" << endl; + cout << " open Open disc for packet writing (must be called first)" << endl; + cout << " close Finish packet writing and finalize disc" << endl; + cout << " put Copy a file from your computer onto the disc" << endl; + cout << " mv Rename a file already on the disc" << endl; + cout << " rm Delete a file from the disc" << endl; + cout << endl; + cout << "Workflow:" << endl; + cout << " 1. dfm-burner pw open /dev/sr0 /tmp/pw-work" << endl; + cout << " 2. dfm-burner pw put /dev/sr0 /tmp/pw-work /home/user/doc.txt" << endl; + cout << " 3. dfm-burner pw put /dev/sr0 /tmp/pw-work /home/user/photo.jpg" << endl; + cout << " 4. dfm-burner pw close /dev/sr0 /tmp/pw-work" << endl; + break; + + default: + printHelp(); + break; + } +} + +// ── Subcommand parsers ───────────────────────────────────────── + +bool CliOptions::parseInfoArgs(const QStringList &args, BurnCliConfig &config) const +{ + config.command = BurnCommand::Info; + + for (const auto &arg : args) { + if (arg == "--help" || arg == "-h") { + printCommandHelp(BurnCommand::Info); + return false; + } + if (arg == "--json" || arg == "-j") { + config.jsonOutput = true; + } else if (!arg.startsWith('-') && config.device.isEmpty()) { + config.device = arg; + } else { + cerr << "Error: Unknown argument '" << arg.toStdString() << "'" << endl; + return false; + } + } + + if (config.device.isEmpty()) { + cerr << "Error: Device path is required." << endl; + cerr << "Run 'dfm-burner info --help' for usage." << endl; + return false; + } + return true; +} + +bool CliOptions::parseBurnArgs(const QStringList &args, BurnCliConfig &config) const +{ + config.command = BurnCommand::Burn; + bool hasFsOption = false; + + for (const auto &arg : args) { + if (arg == "--help" || arg == "-h") { + printCommandHelp(BurnCommand::Burn); + return false; + } + if (arg.startsWith("--volume-id=")) { + config.volumeId = arg.mid(12); + if (config.volumeId.isEmpty()) { + cerr << "Error: --volume-id requires a non-empty value." << endl; + return false; + } + } else if (arg.startsWith("--speed=")) { + bool ok = false; + config.speed = arg.mid(8).toInt(&ok); + if (!ok || config.speed < 0) { + cerr << "Error: Invalid speed value '" << arg.mid(8).toStdString() << "'" << endl; + return false; + } + } else if (arg == "--iso9660") { + config.burnOptions |= BurnOption::kISO9660Only; + hasFsOption = true; + } else if (arg == "--joliet") { + config.burnOptions |= BurnOption::kJolietSupport; + hasFsOption = true; + } else if (arg == "--rockridge") { + config.burnOptions |= BurnOption::kRockRidgeSupport; + hasFsOption = true; + } else if (arg == "--udf") { + config.burnOptions |= BurnOption::kUDF102Supported; + hasFsOption = true; + } else if (arg == "--appendable") { + config.burnOptions |= BurnOption::kKeepAppendable; + } else if (arg == "--verify") { + config.burnOptions |= BurnOption::kVerifyDatas; + } else if (!arg.startsWith('-')) { + if (config.device.isEmpty()) + config.device = arg; + else + config.stageFiles << arg; + } else { + cerr << "Error: Unknown option '" << arg.toStdString() << "'" << endl; + return false; + } + } + + // Default filesystem is ISO9660 + if (!hasFsOption) + config.burnOptions |= BurnOption::kISO9660Only; + + if (config.device.isEmpty()) { + cerr << "Error: Device path is required." << endl; + cerr << "Run 'dfm-burner burn --help' for usage." << endl; + return false; + } + if (config.stageFiles.isEmpty()) { + cerr << "Error: At least one file or directory is required." << endl; + cerr << "Run 'dfm-burner burn --help' for usage." << endl; + return false; + } + + // Validate stage files exist + for (const auto &f : config.stageFiles) { + if (!QFileInfo::exists(f)) { + cerr << "Error: File not found: " << f.toStdString() << endl; + return false; + } + } + + return true; +} + +bool CliOptions::parseWriteIsoArgs(const QStringList &args, BurnCliConfig &config) const +{ + config.command = BurnCommand::WriteISO; + + for (const auto &arg : args) { + if (arg == "--help" || arg == "-h") { + printCommandHelp(BurnCommand::WriteISO); + return false; + } + if (arg.startsWith("--speed=")) { + bool ok = false; + config.speed = arg.mid(8).toInt(&ok); + if (!ok || config.speed < 0) { + cerr << "Error: Invalid speed value '" << arg.mid(8).toStdString() << "'" << endl; + return false; + } + } else if (!arg.startsWith('-')) { + if (config.device.isEmpty()) + config.device = arg; + else if (config.isoPath.isEmpty()) + config.isoPath = arg; + else { + cerr << "Error: Unexpected argument '" << arg.toStdString() << "'" << endl; + return false; + } + } else { + cerr << "Error: Unknown option '" << arg.toStdString() << "'" << endl; + return false; + } + } + + if (config.device.isEmpty() || config.isoPath.isEmpty()) { + cerr << "Error: Device and ISO path are required." << endl; + cerr << "Run 'dfm-burner write-iso --help' for usage." << endl; + return false; + } + if (!QFileInfo::exists(config.isoPath)) { + cerr << "Error: ISO file not found: " << config.isoPath.toStdString() << endl; + return false; + } + return true; +} + +bool CliOptions::parseDumpIsoArgs(const QStringList &args, BurnCliConfig &config) const +{ + config.command = BurnCommand::DumpISO; + + for (const auto &arg : args) { + if (arg == "--help" || arg == "-h") { + printCommandHelp(BurnCommand::DumpISO); + return false; + } + if (!arg.startsWith('-')) { + if (config.device.isEmpty()) + config.device = arg; + else if (config.outputPath.isEmpty()) + config.outputPath = arg; + else { + cerr << "Error: Unexpected argument '" << arg.toStdString() << "'" << endl; + return false; + } + } else { + cerr << "Error: Unknown option '" << arg.toStdString() << "'" << endl; + return false; + } + } + + if (config.device.isEmpty() || config.outputPath.isEmpty()) { + cerr << "Error: Device and output path are required." << endl; + cerr << "Run 'dfm-burner dump-iso --help' for usage." << endl; + return false; + } + return true; +} + +bool CliOptions::parseEraseArgs(const QStringList &args, BurnCliConfig &config) const +{ + config.command = BurnCommand::Erase; + + for (const auto &arg : args) { + if (arg == "--help" || arg == "-h") { + printCommandHelp(BurnCommand::Erase); + return false; + } + if (!arg.startsWith('-') && config.device.isEmpty()) { + config.device = arg; + } else { + cerr << "Error: Unexpected argument '" << arg.toStdString() << "'" << endl; + return false; + } + } + + if (config.device.isEmpty()) { + cerr << "Error: Device path is required." << endl; + cerr << "Run 'dfm-burner erase --help' for usage." << endl; + return false; + } + return true; +} + +bool CliOptions::parseCheckArgs(const QStringList &args, BurnCliConfig &config) const +{ + config.command = BurnCommand::Check; + + for (const auto &arg : args) { + if (arg == "--help" || arg == "-h") { + printCommandHelp(BurnCommand::Check); + return false; + } + if (arg == "--json" || arg == "-j") { + config.jsonOutput = true; + } else if (!arg.startsWith('-') && config.device.isEmpty()) { + config.device = arg; + } else { + cerr << "Error: Unknown argument '" << arg.toStdString() << "'" << endl; + return false; + } + } + + if (config.device.isEmpty()) { + cerr << "Error: Device path is required." << endl; + cerr << "Run 'dfm-burner check --help' for usage." << endl; + return false; + } + return true; +} + +bool CliOptions::parseChecksumGenArgs(const QStringList &args, BurnCliConfig &config) const +{ + config.command = BurnCommand::ChecksumGen; + + for (const auto &arg : args) { + if (arg == "--help" || arg == "-h") { + printCommandHelp(BurnCommand::ChecksumGen); + return false; + } + if (arg.startsWith("--output=")) { + config.manifestPath = arg.mid(9); + if (config.manifestPath.isEmpty()) { + cerr << "Error: --output requires a non-empty value." << endl; + return false; + } + } else if (!arg.startsWith('-')) { + if (config.stageFiles.isEmpty()) + config.stageFiles << arg; + else { + cerr << "Error: Unexpected argument '" << arg.toStdString() << "'" << endl; + return false; + } + } else { + cerr << "Error: Unknown option '" << arg.toStdString() << "'" << endl; + return false; + } + } + + if (config.manifestPath.isEmpty()) { + cerr << "Error: --output= is required." << endl; + cerr << "Run 'dfm-burner checksum gen --help' for usage." << endl; + return false; + } + if (config.stageFiles.isEmpty()) { + cerr << "Error: Source directory is required." << endl; + cerr << "Run 'dfm-burner checksum gen --help' for usage." << endl; + return false; + } + if (!QFileInfo::exists(config.stageFiles.first())) { + cerr << "Error: Source not found: " << config.stageFiles.first().toStdString() << endl; + return false; + } + + return true; +} + +bool CliOptions::parseChecksumVerifyArgs(const QStringList &args, BurnCliConfig &config) const +{ + config.command = BurnCommand::ChecksumVerify; + + for (const auto &arg : args) { + if (arg == "--help" || arg == "-h") { + printCommandHelp(BurnCommand::ChecksumVerify); + return false; + } + if (!arg.startsWith('-')) { + if (config.device.isEmpty()) + config.device = arg; + else if (config.manifestPath.isEmpty()) + config.manifestPath = arg; + else { + cerr << "Error: Unexpected argument '" << arg.toStdString() << "'" << endl; + return false; + } + } else { + cerr << "Error: Unknown option '" << arg.toStdString() << "'" << endl; + return false; + } + } + + if (config.device.isEmpty()) { + cerr << "Error: Device path is required." << endl; + cerr << "Run 'dfm-burner checksum verify --help' for usage." << endl; + return false; + } + if (config.manifestPath.isEmpty()) { + cerr << "Error: Manifest path is required." << endl; + cerr << "Run 'dfm-burner checksum verify --help' for usage." << endl; + return false; + } + if (!QFileInfo::exists(config.manifestPath)) { + cerr << "Error: Manifest not found: " << config.manifestPath.toStdString() << endl; + return false; + } + + return true; +} + +bool CliOptions::parsePwArgs(const QString &pwAction, const QStringList &args, BurnCliConfig &config) const +{ + // Map action to command + if (pwAction == "--help" || pwAction == "-h") { + printCommandHelp(BurnCommand::PwOpen); + return false; + } + + if (pwAction == "open") { + config.command = BurnCommand::PwOpen; + } else if (pwAction == "close") { + config.command = BurnCommand::PwClose; + } else if (pwAction == "put") { + config.command = BurnCommand::PwPut; + } else if (pwAction == "mv") { + config.command = BurnCommand::PwMv; + } else if (pwAction == "rm") { + config.command = BurnCommand::PwRm; + } else { + cerr << "Error: Unknown pw action '" << pwAction.toStdString() << "'" << endl; + cerr << "Valid actions: open, close, put, mv, rm" << endl; + return false; + } + + // Parse positional arguments + for (const auto &arg : args) { + if (arg.startsWith('-')) { + cerr << "Error: Unknown option '" << arg.toStdString() << "'" << endl; + return false; + } + if (config.device.isEmpty()) + config.device = arg; + else if (config.workingPath.isEmpty()) + config.workingPath = arg; + else if (config.command == BurnCommand::PwPut && config.pwFileName.isEmpty()) + config.pwFileName = arg; + else if (config.command == BurnCommand::PwRm && config.pwFileName.isEmpty()) + config.pwFileName = arg; + else if (config.command == BurnCommand::PwMv && config.pwSrcName.isEmpty()) + config.pwSrcName = arg; + else if (config.command == BurnCommand::PwMv && config.pwDestName.isEmpty()) + config.pwDestName = arg; + else { + cerr << "Error: Unexpected argument '" << arg.toStdString() << "'" << endl; + return false; + } + } + + // Validate required arguments + if (config.device.isEmpty() || config.workingPath.isEmpty()) { + cerr << "Error: Device and working path are required." << endl; + cerr << "Run 'dfm-burner pw --help' for usage." << endl; + return false; + } + + if (config.command == BurnCommand::PwPut && config.pwFileName.isEmpty()) { + cerr << "Error: File name is required for 'put' action." << endl; + return false; + } + if (config.command == BurnCommand::PwRm && config.pwFileName.isEmpty()) { + cerr << "Error: File name is required for 'rm' action." << endl; + return false; + } + if (config.command == BurnCommand::PwMv && (config.pwSrcName.isEmpty() || config.pwDestName.isEmpty())) { + cerr << "Error: Source and destination names are required for 'mv' action." << endl; + return false; + } + + return true; +} diff --git a/src/dfm-burn/dfm-burn-client/cli_options.h b/src/dfm-burn/dfm-burn-client/cli_options.h new file mode 100644 index 00000000..8d34b7d4 --- /dev/null +++ b/src/dfm-burn/dfm-burn-client/cli_options.h @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef CLI_OPTIONS_H +#define CLI_OPTIONS_H + +#include +#include + +DFM_BURN_BEGIN_NS + +/** + * @brief Supported burn subcommands + */ +enum class BurnCommand { + None, + Info, + Burn, + WriteISO, + DumpISO, + Erase, + Check, + ChecksumGen, + ChecksumVerify, + PwOpen, + PwClose, + PwPut, + PwMv, + PwRm +}; + +/** + * @brief Parsed CLI configuration + * + * Fields are populated based on the active subcommand; + * unrelated fields are left at their defaults. + */ +struct BurnCliConfig +{ + BurnCommand command = BurnCommand::None; + QString device; + bool jsonOutput = false; + + // burn options + QStringList stageFiles; + QString volumeId = "ISOIMAGE"; + int speed = 0; + BurnOptions burnOptions; + + // write-iso / dump-iso paths + QString isoPath; + QString outputPath; + + // packet writing + QString workingPath; + QString pwSrcName; + QString pwDestName; + QString pwFileName; + + // checksum + QString manifestPath; +}; + +/** + * @brief Command-line option parser with subcommand routing + * + * Supports git-style subcommands: info, burn, write-iso, dump-iso, + * erase, check, pw . + */ +class CliOptions +{ +public: + CliOptions() = default; + ~CliOptions() = default; + + /** + * @brief Parse command-line arguments into config + * @return true on success, false on error (message already printed) + */ + bool parse(int argc, char *argv[], BurnCliConfig &config); + + void printHelp() const; + +private: + void printCommandHelp(BurnCommand cmd) const; + + // ── Per-subcommand parsers ── + bool parseInfoArgs(const QStringList &args, BurnCliConfig &config) const; + bool parseBurnArgs(const QStringList &args, BurnCliConfig &config) const; + bool parseWriteIsoArgs(const QStringList &args, BurnCliConfig &config) const; + bool parseDumpIsoArgs(const QStringList &args, BurnCliConfig &config) const; + bool parseEraseArgs(const QStringList &args, BurnCliConfig &config) const; + bool parseCheckArgs(const QStringList &args, BurnCliConfig &config) const; + bool parseChecksumGenArgs(const QStringList &args, BurnCliConfig &config) const; + bool parseChecksumVerifyArgs(const QStringList &args, BurnCliConfig &config) const; + bool parsePwArgs(const QString &pwAction, const QStringList &args, BurnCliConfig &config) const; +}; + +DFM_BURN_END_NS + +#endif // CLI_OPTIONS_H diff --git a/src/dfm-burn/dfm-burn-client/main.cpp b/src/dfm-burn/dfm-burn-client/main.cpp index 6d7e6941..bdf0187d 100644 --- a/src/dfm-burn/dfm-burn-client/main.cpp +++ b/src/dfm-burn/dfm-burn-client/main.cpp @@ -3,109 +3,444 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include -#include +#include +#include +#include +#include +#include +#include +#include #include #include #include +#include "cli_options.h" +#include "burn_utils.h" + DFM_BURN_USE_NS -// TODO(zhangs): follow code is test code - -//static void erase(const QString &dev) -//{ -// DOpticalDiscManager manager(dev); -// QObject::connect(&manager, &DOpticalDiscManager::jobStatusChanged, [](JobStatus status, int progress, QString speed, QStringList message) { -// qDebug() << int(status) << progress << speed << message; -// }); -// manager.erase(); -//} - -//static void showInfo(const QString &dev) -//{ -// QScopedPointer info { DOpticalDiscManager::createOpticalInfo(dev) }; -// qDebug() << info->device(); -// qDebug() << int(info->mediaType()); -// qDebug() << info->writeSpeed(); -// qDebug() << info->volumeName(); -//} - -//static void commit() -//{ -// DOpticalDiscManager manager("/dev/sr0"); -// QObject::connect(&manager, &DOpticalDiscManager::jobStatusChanged, [](JobStatus status, int progress, QString speed, QStringList message) { -// qDebug() << int(status) << progress << speed << message; -// }); -// manager.setStageFile("/home/zhangs/.cache/deepin/discburn/_dev_sr0"); -// BurnOptions opts; -// opts |= BurnOption::kJolietAndRockRidge; -// opts |= BurnOption::kKeepAppendable; -// manager.commit(opts, 0, "123"); -//} - -//static void commitUDF() -//{ -// DOpticalDiscManager manager("/dev/sr0"); -// QObject::connect(&manager, &DOpticalDiscManager::jobStatusChanged, [](JobStatus status, int progress, QString speed, QStringList message) { -// qDebug() << int(status) << progress << speed << message; -// }); -// manager.setStageFile("/home/zhangs/Downloads/254111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111.gz"); -// BurnOptions opts; -// opts |= BurnOption::kUDF102Supported; -// opts |= BurnOption::kKeepAppendable; -// manager.commit(opts, 0, "abc123"); -//} - -//static void writeISO() -//{ -// DOpticalDiscManager manager("/dev/sr0"); -// QObject::connect(&manager, &DOpticalDiscManager::jobStatusChanged, [](JobStatus status, int progress, QString speed, QStringList message) { -// qDebug() << int(status) << progress << speed << message; -// }); -// manager.writeISO("/home/zhangs/Downloads/deb/20200413_214350.iso"); -//} - -//static void check() -//{ -// DOpticalDiscManager manager("/dev/sr0"); -// QObject::connect(&manager, &DOpticalDiscManager::jobStatusChanged, [](JobStatus status, int progress, QString speed, QStringList message) { -// qInfo() << int(status) << progress << speed << message; -// }); -// double gud, slo, bad; -// manager.checkmedia(&gud, &slo, &bad); -// bool check { true }; -// bool checkRet { !(check && (bad > (2 + 1e-6))) }; -// qDebug() << "check ret" << checkRet; -//} - -//static void dumpISO() -//{ -// DOpticalDiscManager manager("/dev/sr0"); -// QObject::connect(&manager, &DOpticalDiscManager::jobStatusChanged, [](JobStatus status, int progress, QString speed, QStringList message) { -// qInfo() << int(status) << progress << speed << message; -// }); -// manager.dumpISO("/home/zhangs/tmp/aabb.iso"); -//} - -//static void pw() -//{ -// DPacketWritingController controller("/dev/sr0", "/home/zhangs"); -// controller.open(); -// controller.close(); -// qDebug() << "quit!!!"; -//} +using namespace std; + +// ── Helpers ──────────────────────────────────────────────────── + +/** + * @brief Run an async operation that reports progress via jobStatusChanged. + * + * @param config Parsed CLI config (must have .device set) + * @param app QCoreApplication reference (for event loop) + * @param startOp Lambda that starts the operation; returns false on immediate failure + * @param opName Human-readable operation name for progress display + * @return Exit code (0 = success, 1 = failure) + */ +static int runAsyncJob(const BurnCliConfig &config, QCoreApplication &app, + const function &startOp, + const QString &opName) +{ + DOpticalDiscManager manager(config.device); + int exitCode = 1; + + QObject::connect(&manager, &DOpticalDiscManager::jobStatusChanged, + [&exitCode, &app, &opName](JobStatus status, int progress, + const QString &speed, const QStringList &message) { + if (status == JobStatus::kRunning) { + cout << "\r" << opName.toStdString() << "... " << progress << "%"; + if (!speed.isEmpty()) + cout << " (" << speed.toStdString() << ")"; + cout << flush; + } else if (status == JobStatus::kFinished) { + cout << "\r" << opName.toStdString() << "... Done. " << endl; + exitCode = 0; + app.quit(); + } else if (status == JobStatus::kFailed) { + cerr << endl + << "Operation failed: " << message.join(" ").toStdString() << endl; + exitCode = 1; + app.quit(); + } else if (status == JobStatus::kStalled) { + cout << "\r" << opName.toStdString() << "... Stalled (waiting for drive)." << flush; + } + }); + + if (!startOp(manager)) + return 1; + + return app.exec(); +} + +// ── Command: info (sync) ────────────────────────────────────── + +static int handleInfo(const BurnCliConfig &config) +{ + QScopedPointer info(DOpticalDiscManager::createOpticalInfo(config.device)); + if (!info) { + cerr << "Error: Failed to get disc info for " << config.device.toStdString() << endl; + return 1; + } + + if (info->mediaType() == MediaType::kNoMedia) { + cerr << "Error: No disc detected in " << config.device.toStdString() << endl; + cerr << " Insert a disc and try again." << endl; + return 1; + } + + if (config.jsonOutput) { + QJsonObject obj; + obj["device"] = info->device(); + obj["mediaType"] = mediaTypeName(info->mediaType()); + obj["blank"] = info->blank(); + obj["volumeName"] = info->volumeName(); + obj["usedSize"] = static_cast(info->usedSize()); + obj["availableSize"] = static_cast(info->availableSize()); + obj["totalSize"] = static_cast(info->totalSize()); + obj["dataBlocks"] = static_cast(info->dataBlocks()); + QJsonArray speeds; + for (const auto &s : info->writeSpeed()) + speeds.append(s); + obj["writeSpeeds"] = speeds; + cout << QJsonDocument(obj).toJson(QJsonDocument::Indented).toStdString(); + } else { + cout << "Device: " << info->device().toStdString() << endl; + cout << "Media Type: " << mediaTypeName(info->mediaType()).toStdString() + << (isRewritable(info->mediaType()) ? " (rewritable)" : " (write-once)") << endl; + cout << "Blank: " << (info->blank() ? "Yes" : "No") << endl; + cout << "Volume Name: " << (info->volumeName().isEmpty() ? "(empty)" : info->volumeName().toStdString()) << endl; + cout << "Used: " << formatSize(info->usedSize()).toStdString() << endl; + cout << "Available: " << formatSize(info->availableSize()).toStdString() << endl; + cout << "Total: " << formatSize(info->totalSize()).toStdString() << endl; + cout << "Data Blocks: " << info->dataBlocks() << endl; + + QStringList speeds = info->writeSpeed(); + cout << "Write Speeds: " << (speeds.isEmpty() ? "(none)" : speeds.join(", ").toStdString()) << endl; + + // Actionable hints + cout << endl; + if (info->blank()) { + cout << ">> Disc is blank and ready to burn." << endl; + cout << " Quick start: dfm-burner burn " << info->device().toStdString() << " ./your-folder" << endl; + } else if (info->availableSize() > 0) { + cout << ">> Disc has free space (" << formatSize(info->availableSize()).toStdString() << ")." << endl; + cout << " To add data: dfm-burner burn --appendable " << info->device().toStdString() << " ./your-folder" << endl; + cout << " To start fresh, erase first: dfm-burner erase " << info->device().toStdString() << endl; + } else { + cout << ">> Disc is full. To reuse, erase first: dfm-burner erase " << info->device().toStdString() << endl; + } + } + + return 0; +} + +// ── Command: burn (async) ────────────────────────────────────── + +static int handleBurn(const BurnCliConfig &config, QCoreApplication &app) +{ + auto startOp = [&config](DOpticalDiscManager &manager) -> bool { + for (const auto &file : config.stageFiles) { + if (!manager.setStageFile(file)) { + cerr << "Error: Could not add '" << file.toStdString() << "' to burn queue." << endl; + cerr << " " << manager.lastError().toStdString() << endl; + return false; + } + } + + cout << "Device: " << config.device.toStdString() << endl; + cout << "Volume: " << config.volumeId.toStdString() << endl; + cout << "Mode: " << burnOptionsSummary(config.burnOptions).toStdString() << endl; + cout << "Files: " << config.stageFiles.join(", ").toStdString() << endl; + cout << endl; + + if (!manager.commit(config.burnOptions, config.speed, config.volumeId)) { + cerr << "Error: Failed to start burn operation." << endl; + cerr << " " << manager.lastError().toStdString() << endl; + return false; + } + return true; + }; + + return runAsyncJob(config, app, startOp, "Burning"); +} + +// ── Command: write-iso (async) ───────────────────────────────── + +static int handleWriteISO(const BurnCliConfig &config, QCoreApplication &app) +{ + QFileInfo isoInfo(config.isoPath); + cout << "Device: " << config.device.toStdString() << endl; + cout << "ISO: " << config.isoPath.toStdString() << endl; + cout << "Size: " << formatSize(isoInfo.size()).toStdString() << endl; + cout << endl; + + auto startOp = [&config](DOpticalDiscManager &manager) -> bool { + if (!manager.writeISO(config.isoPath, config.speed)) { + cerr << "Error: " << manager.lastError().toStdString() << endl; + return false; + } + return true; + }; + + return runAsyncJob(config, app, startOp, "Writing ISO"); +} + +// ── Command: dump-iso (async) ────────────────────────────────── + +static int handleDumpISO(const BurnCliConfig &config, QCoreApplication &app) +{ + cout << "Device: " << config.device.toStdString() << endl; + cout << "Output: " << config.outputPath.toStdString() << endl; + cout << endl; + + auto startOp = [&config](DOpticalDiscManager &manager) -> bool { + if (!manager.dumpISO(config.outputPath)) { + cerr << "Error: " << manager.lastError().toStdString() << endl; + return false; + } + return true; + }; + + return runAsyncJob(config, app, startOp, "Dumping"); +} + +// ── Command: erase (async) ───────────────────────────────────── + +static int handleErase(const BurnCliConfig &config, QCoreApplication &app) +{ + cout << "Device: " << config.device.toStdString() << endl; + cout << endl; + + auto startOp = [](DOpticalDiscManager &manager) -> bool { + if (!manager.erase()) { + cerr << "Error: " << manager.lastError().toStdString() << endl; + return false; + } + return true; + }; + + return runAsyncJob(config, app, startOp, "Erasing"); +} + +// ── Command: check (async, custom handler) ───────────────────── + +static int handleCheck(const BurnCliConfig &config, QCoreApplication &app) +{ + DOpticalDiscManager manager(config.device); + int exitCode = 1; + double good = 0, slow = 0, bad = 0; + + QObject::connect(&manager, &DOpticalDiscManager::jobStatusChanged, + [&exitCode, &app, &config, &good, &slow, &bad]( + JobStatus status, int progress, + const QString &speed, const QStringList &message) { + if (status == JobStatus::kRunning) { + cout << "\rChecking... " << progress << "%" << flush; + } else if (status == JobStatus::kFinished) { + cout << endl; + bool passed = !(bad > (2 + 1e-6)); + + if (config.jsonOutput) { + QJsonObject obj; + obj["good"] = good; + obj["slow"] = slow; + obj["bad"] = bad; + obj["passed"] = passed; + cout << QJsonDocument(obj).toJson(QJsonDocument::Indented).toStdString(); + } else { + cout << fixed << setprecision(1); + cout << "Quality:" << endl; + cout << " Good: " << good << "%" << endl; + cout << " Slow: " << slow << "%" << endl; + cout << " Bad: " << bad << "%" << endl; + cout << endl; + cout << "Result: " << (passed ? "PASS" : "FAIL") << endl; + } + + exitCode = passed ? 0 : 1; + app.quit(); + } else if (status == JobStatus::kFailed) { + cerr << endl + << "Check failed: " << message.join(" ").toStdString() << endl; + exitCode = 1; + app.quit(); + } + }); + + if (!manager.checkmedia(&good, &slow, &bad)) { + cerr << "Error: " << manager.lastError().toStdString() << endl; + return 1; + } + + return app.exec(); +} + +// ── Command: checksum gen (sync) ──────────────────────────────── + +static int handleChecksumGen(const BurnCliConfig &config) +{ + DOpticalDiscManager manager(config.device); + + if (!manager.setStageFile(config.stageFiles.first())) { + cerr << "Error: " << manager.lastError().toStdString() << endl; + return 1; + } + + if (!manager.generateChecksumManifest(config.manifestPath)) { + cerr << "Error: " << manager.lastError().toStdString() << endl; + return 1; + } + + cout << "Manifest generated: " << config.manifestPath.toStdString() << endl; + return 0; +} + +// ── Command: checksum verify (async) ───────────────────────────── + +static int handleChecksumVerify(const BurnCliConfig &config, QCoreApplication &app) +{ + DOpticalDiscManager manager(config.device); + int exitCode = 1; + + QObject::connect(&manager, &DOpticalDiscManager::jobStatusChanged, + [&exitCode, &app](JobStatus status, int progress, + const QString &speed, const QStringList &message) { + Q_UNUSED(speed); + if (status == JobStatus::kRunning) { + cout << "\rVerifying... " << progress << "%"; + if (!message.isEmpty()) + cout << " (" << message.first().toStdString() << ")"; + cout << flush; + } else if (status == JobStatus::kStalled) { + cout << "\rVerifying... Stalled (waiting for drive)." << flush; + } else if (status == JobStatus::kFinished) { + cout << "\rVerifying... Done. " << endl; + cout << "All checksums matched." << endl; + exitCode = 0; + app.quit(); + } else if (status == JobStatus::kFailed) { + cerr << endl + << "Verification failed: " << message.join(" ").toStdString() << endl; + exitCode = 1; + app.quit(); + } + }); + + if (!manager.verifyChecksum(config.manifestPath)) { + cerr << "Error: " << manager.lastError().toStdString() << endl; + return 1; + } + + return app.exec(); +} + +// ── Packet Writing Commands (sync) ───────────────────────────── + +static int handlePwOpen(const BurnCliConfig &config) +{ + DPacketWritingController controller(config.device, config.workingPath); + + if (!controller.open()) { + cerr << "Error: Failed to open packet writing session." << endl; + cerr << " " << controller.lastError().toStdString() << endl; + return 1; + } + + cout << "Packet writing session opened." << endl; + cout << " Device: " << config.device.toStdString() << endl; + cout << " Working: " << config.workingPath.toStdString() << endl; + return 0; +} + +static int handlePwClose(const BurnCliConfig &config) +{ + DPacketWritingController controller(config.device, config.workingPath); + controller.close(); + + cout << "Packet writing session closed." << endl; + return 0; +} + +static int handlePwPut(const BurnCliConfig &config) +{ + DPacketWritingController controller(config.device, config.workingPath); + + if (!controller.put(config.pwFileName)) { + cerr << "Error: Failed to add file." << endl; + cerr << " " << controller.lastError().toStdString() << endl; + return 1; + } + + cout << "Added: " << config.pwFileName.toStdString() << endl; + return 0; +} + +static int handlePwMv(const BurnCliConfig &config) +{ + DPacketWritingController controller(config.device, config.workingPath); + + if (!controller.mv(config.pwSrcName, config.pwDestName)) { + cerr << "Error: Failed to rename." << endl; + cerr << " " << controller.lastError().toStdString() << endl; + return 1; + } + + cout << "Renamed: " << config.pwSrcName.toStdString() + << " -> " << config.pwDestName.toStdString() << endl; + return 0; +} + +static int handlePwRm(const BurnCliConfig &config) +{ + DPacketWritingController controller(config.device, config.workingPath); + + if (!controller.rm(config.pwFileName)) { + cerr << "Error: Failed to remove." << endl; + cerr << " " << controller.lastError().toStdString() << endl; + return 1; + } + + cout << "Removed: " << config.pwFileName.toStdString() << endl; + return 0; +} + +// ── Entry point ──────────────────────────────────────────────── int main(int argc, char *argv[]) { - QCoreApplication a(argc, argv); - // showInfo("/dev/sr0"); - // erase("/dev/sr0"); - // writeISO(); - // commit(); - // commitUDF(); - // check(); - // dumpISO(); - // pw(); - return a.exec(); + QCoreApplication app(argc, argv); + app.setApplicationName("dfm-burner"); + app.setApplicationVersion("1.0.0"); + + CliOptions cli; + BurnCliConfig config; + if (!cli.parse(argc, argv, config)) + return 1; + + switch (config.command) { + case BurnCommand::Info: + return handleInfo(config); + case BurnCommand::Burn: + return handleBurn(config, app); + case BurnCommand::WriteISO: + return handleWriteISO(config, app); + case BurnCommand::DumpISO: + return handleDumpISO(config, app); + case BurnCommand::Erase: + return handleErase(config, app); + case BurnCommand::Check: + return handleCheck(config, app); + case BurnCommand::ChecksumGen: + return handleChecksumGen(config); + case BurnCommand::ChecksumVerify: + return handleChecksumVerify(config, app); + case BurnCommand::PwOpen: + return handlePwOpen(config); + case BurnCommand::PwClose: + return handlePwClose(config); + case BurnCommand::PwPut: + return handlePwPut(config); + case BurnCommand::PwMv: + return handlePwMv(config); + case BurnCommand::PwRm: + return handlePwRm(config); + default: + return 1; + } } diff --git a/src/dfm-burn/dfm-burn-lib/dfm-burn.cmake b/src/dfm-burn/dfm-burn-lib/dfm-burn.cmake index 58a69d9b..c6d5d576 100644 --- a/src/dfm-burn/dfm-burn-lib/dfm-burn.cmake +++ b/src/dfm-burn/dfm-burn-lib/dfm-burn.cmake @@ -1,6 +1,7 @@ # Setup the environment find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core REQUIRED) find_package(PkgConfig REQUIRED) +find_package(OpenSSL REQUIRED COMPONENTS Crypto) pkg_check_modules(isoburn REQUIRED libisoburn-1 IMPORTED_TARGET) # Build @@ -13,6 +14,7 @@ add_library(${BIN_NAME} SHARED target_link_libraries(${BIN_NAME} Qt${QT_VERSION_MAJOR}::Core PkgConfig::isoburn + OpenSSL::Crypto ) target_include_directories( diff --git a/src/dfm-burn/dfm-burn-lib/dopticaldiscmanager.cpp b/src/dfm-burn/dfm-burn-lib/dopticaldiscmanager.cpp index ffef12b8..270f06de 100644 --- a/src/dfm-burn/dfm-burn-lib/dopticaldiscmanager.cpp +++ b/src/dfm-burn/dfm-burn-lib/dopticaldiscmanager.cpp @@ -8,13 +8,51 @@ #include "private/dopticaldiscmanager_p.h" #include "private/dxorrisoengine.h" #include "private/dudfburnengine.h" +#include "private/dsm3hash.h" #include +#include +#include +#include +#include +#include +#include #include #include DFM_BURN_USE_NS +static const QString kExtractCacheSubDir = "discburn"; + +static QString extractCacheDir(const QString &dev) +{ + QString safeDev = dev; + safeDev.replace("/", "_"); + // Remove leading underscore caused by the leading "/" in device path + if (safeDev.startsWith("_")) + safeDev.remove(0, 1); + return QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + + "/deepin/" + kExtractCacheSubDir + "/extract_" + safeDev; +} + +// xorriso extracts files with read-only permissions (dr-xr-xr-x), +// so we must chmod recursively before removal. +static void cleanupExtractCache(QDir dir) +{ + if (!dir.exists()) + return; + QDirIterator it(dir.absolutePath(), QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot, + QDirIterator::Subdirectories); + while (it.hasNext()) { + QString path = it.next(); + QFile::setPermissions(path, + QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner + | QFile::ReadGroup | QFile::ExeGroup | QFile::ReadOther + | QFile::ExeOther); + } + dir.removeRecursively(); +} + DOpticalDiscManager::DOpticalDiscManager(const QString &dev, QObject *parent) : QObject(parent), dptr(new DOpticalDiscManagerPrivate) { @@ -58,7 +96,8 @@ bool DOpticalDiscManager::commit(const BurnOptions &opts, int speed, const QStri if (opts.testFlag(BurnOption::kUDF102Supported)) { QScopedPointer udfEngine { new DUDFBurnEngine }; - connect(udfEngine.data(), &DUDFBurnEngine::jobStatusChanged, this, + connect( + udfEngine.data(), &DUDFBurnEngine::jobStatusChanged, this, [this, ptr = QPointer(udfEngine.data())](JobStatus status, int progress) { if (ptr) { if (status == JobStatus::kFailed) @@ -71,7 +110,8 @@ bool DOpticalDiscManager::commit(const BurnOptions &opts, int speed, const QStri ret = udfEngine->doBurn(dptr->curDev, dptr->files, volId, opts); } else { QScopedPointer xorrisoEngine { new DXorrisoEngine }; - connect(xorrisoEngine.data(), &DXorrisoEngine::jobStatusChanged, this, + connect( + xorrisoEngine.data(), &DXorrisoEngine::jobStatusChanged, this, [this, ptr = QPointer(xorrisoEngine.data())](JobStatus status, int progress, QString speed) { if (ptr) Q_EMIT jobStatusChanged(status, progress, speed, ptr->takeInfoMessages()); @@ -105,7 +145,8 @@ bool DOpticalDiscManager::erase() { bool ret { false }; QScopedPointer engine { new DXorrisoEngine }; - connect(engine.data(), &DXorrisoEngine::jobStatusChanged, this, + connect( + engine.data(), &DXorrisoEngine::jobStatusChanged, this, [this, ptr = QPointer(engine.data())](JobStatus status, int progress, QString speed) { if (ptr) Q_EMIT jobStatusChanged(status, progress, speed, ptr->takeInfoMessages()); @@ -134,7 +175,8 @@ bool DOpticalDiscManager::checkmedia(double *qgood, double *qslow, double *qbad) } QScopedPointer engine { new DXorrisoEngine }; - connect(engine.data(), &DXorrisoEngine::jobStatusChanged, this, + connect( + engine.data(), &DXorrisoEngine::jobStatusChanged, this, [this, ptr = QPointer(engine.data())](JobStatus status, int progress, QString speed) { if (ptr) Q_EMIT jobStatusChanged(status, progress, speed, ptr->takeInfoMessages()); @@ -155,7 +197,8 @@ bool DOpticalDiscManager::writeISO(const QString &isoPath, int speed) { bool ret { false }; QScopedPointer engine { new DXorrisoEngine }; - connect(engine.data(), &DXorrisoEngine::jobStatusChanged, this, + connect( + engine.data(), &DXorrisoEngine::jobStatusChanged, this, [this, ptr = QPointer(engine.data())](JobStatus status, int progress, QString speed) { if (ptr) Q_EMIT jobStatusChanged(status, progress, speed, ptr->takeInfoMessages()); @@ -190,7 +233,8 @@ bool DOpticalDiscManager::dumpISO(const QString &isoPath) } QScopedPointer engine { new DXorrisoEngine }; - connect(engine.data(), &DXorrisoEngine::jobStatusChanged, this, + connect( + engine.data(), &DXorrisoEngine::jobStatusChanged, this, [this, ptr = QPointer(engine.data())](JobStatus status, int progress, QString speed) { if (ptr) emit jobStatusChanged(status, progress, speed, ptr->takeInfoMessages()); @@ -212,6 +256,185 @@ bool DOpticalDiscManager::dumpISO(const QString &isoPath) return ret; } +bool DOpticalDiscManager::generateChecksumManifest(const QString &savePath) +{ + QString srcPath = dptr->files.first; + if (srcPath.isEmpty()) { + dptr->errorMsg = "No staged file for checksum generation"; + return false; + } + + QFileInfo srcInfo(srcPath); + if (!srcInfo.exists()) { + dptr->errorMsg = QString("Source path does not exist: %1").arg(srcPath); + return false; + } + + QString isoBase = dptr->files.second; + if (isoBase.isEmpty()) + isoBase = "/"; + + // Normalize isoBase: ensure it ends with / + if (!isoBase.endsWith("/")) + isoBase += "/"; + + QJsonObject filesObj; + + if (srcInfo.isDir()) { + QDirIterator it(srcPath, QDir::Files | QDir::Hidden, QDirIterator::Subdirectories); + while (it.hasNext()) { + QString filePath = it.next(); + QString relPath = QDir(srcPath).relativeFilePath(filePath); + QString isoRelPath = isoBase + relPath; + + QString hash = DSM3Hash::sm3File(filePath); + if (hash.isEmpty()) { + dptr->errorMsg = QString("Failed to compute SM3 for: %1").arg(filePath); + return false; + } + filesObj[isoRelPath] = hash; + } + } else { + QString hash = DSM3Hash::sm3File(srcPath); + if (hash.isEmpty()) { + dptr->errorMsg = QString("Failed to compute SM3 for: %1").arg(srcPath); + return false; + } + filesObj[isoBase + srcInfo.fileName()] = hash; + } + + if (filesObj.isEmpty()) { + dptr->errorMsg = "No files found to checksum"; + return false; + } + + QJsonObject root; + root["algorithm"] = "sm3"; + root["device"] = dptr->curDev; + root["files"] = filesObj; + + QFile outFile(savePath); + if (!outFile.open(QIODevice::WriteOnly)) { + dptr->errorMsg = QString("Cannot write manifest to: %1").arg(savePath); + return false; + } + + outFile.write(QJsonDocument(root).toJson(QJsonDocument::Indented)); + return true; +} + +bool DOpticalDiscManager::verifyChecksum(const QString &manifestPath) +{ + QFile mf(manifestPath); + if (!mf.open(QIODevice::ReadOnly)) { + dptr->errorMsg = QString("Cannot read manifest: %1").arg(manifestPath); + return false; + } + + QJsonParseError parseErr; + QJsonObject root = QJsonDocument::fromJson(mf.readAll(), &parseErr).object(); + if (root.isEmpty()) { + dptr->errorMsg = QString("Invalid manifest JSON: %1").arg(parseErr.errorString()); + return false; + } + + QJsonObject filesObj = root["files"].toObject(); + if (filesObj.isEmpty()) { + dptr->errorMsg = "Manifest contains no file entries"; + return false; + } + + // Prepare temp directory for per-file extraction + QString cacheBase = extractCacheDir(dptr->curDev); + QDir cacheDir(cacheBase); + if (cacheDir.exists()) + cleanupExtractCache(cacheDir); + if (!cacheDir.mkpath(".")) { + dptr->errorMsg = QString("Cannot create cache directory: %1").arg(cacheBase); + return false; + } + + // Acquire device and enable osirrox once for all per-file extractions + QScopedPointer engine { new DXorrisoEngine }; + connect( + engine.data(), &DXorrisoEngine::jobStatusChanged, this, + [this, ptr = QPointer(engine.data())](JobStatus status, int progress, QString speed) { + if (ptr) + Q_EMIT jobStatusChanged(status, progress, speed, ptr->takeInfoMessages()); + }, + Qt::DirectConnection); + + if (!engine->acquireDevice(dptr->curDev)) { + dptr->errorMsg = "Cannot acquire device for verification"; + cleanupExtractCache(cacheDir); + return false; + } + + if (!engine->doOsirroxOn()) { + dptr->errorMsg = QString("Failed to enable osirrox: %1").arg(engine->takeInfoMessages().join(" ")); + engine->releaseDevice(); + cleanupExtractCache(cacheDir); + return false; + } + + // Extract and verify each file individually: + // extract → compute SM3 → delete → next file + // This avoids extracting the entire disc (multi-session friendly) + // and minimizes disk usage (only one file at a time). + QStringList isoPaths = filesObj.keys(); + int total = isoPaths.size(); + int checked = 0; + + for (const auto &isoPath : isoPaths) { + QString localTmp = cacheBase + isoPath; + + // Ensure parent directory exists + QDir().mkpath(QFileInfo(localTmp).absolutePath()); + + if (!engine->doExtract(localTmp, isoPath)) { + dptr->errorMsg = QString("Failed to extract '%1' from disc: %2") + .arg(isoPath, engine->takeInfoMessages().join(" ")); + engine->releaseDevice(); + cleanupExtractCache(cacheDir); + return false; + } + + QString expectedHash = filesObj[isoPath].toString(); + QString actualHash = DSM3Hash::sm3File(localTmp); + + // Delete immediately to free disk space and avoid permission issues on cleanup + QFile::remove(localTmp); + + ++checked; + int pct = (total > 0) ? (100 * checked / total) : 0; + Q_EMIT jobStatusChanged(JobStatus::kRunning, pct, {}, { isoPath }); + + if (actualHash.isEmpty()) { + dptr->errorMsg = QString("Checksum mismatch: file not found on disc: %1").arg(isoPath); + engine->releaseDevice(); + cleanupExtractCache(cacheDir); + return false; + } + + if (actualHash != expectedHash) { + dptr->errorMsg = QString("Checksum mismatch: %1 (expected %2, got %3)") + .arg(isoPath, expectedHash, actualHash); + engine->releaseDevice(); + cleanupExtractCache(cacheDir); + return false; + } + } + + engine->releaseDevice(); + + Q_EMIT jobStatusChanged(JobStatus::kFinished, 100, {}, {}); + + // Clean up cache directory structure + cleanupExtractCache(cacheDir); + + return true; +} + QString DOpticalDiscManager::lastError() const { return dptr->errorMsg; diff --git a/src/dfm-burn/dfm-burn-lib/private/dsm3hash.cpp b/src/dfm-burn/dfm-burn-lib/private/dsm3hash.cpp new file mode 100644 index 00000000..15eba643 --- /dev/null +++ b/src/dfm-burn/dfm-burn-lib/private/dsm3hash.cpp @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "dsm3hash.h" + +#include + +#include + +DFM_BURN_USE_NS + +static constexpr int kSM3HashLen = 32; // SM3 produces 256-bit (32-byte) hash +static constexpr int kBufferSize = 8192; + +static QString toHex(const unsigned char *data, int len) +{ + QString result; + result.reserve(len * 2); + static const char kHex[] = "0123456789abcdef"; + for (int i = 0; i < len; ++i) { + result.append(kHex[(data[i] >> 4) & 0x0F]); + result.append(kHex[data[i] & 0x0F]); + } + return result; +} + +QString DSM3Hash::sm3File(const QString &filePath) +{ + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) + return {}; + + EVP_MD_CTX *ctx = EVP_MD_CTX_new(); + if (!ctx) + return {}; + + const EVP_MD *md = EVP_sm3(); + if (!md || EVP_DigestInit_ex(ctx, md, nullptr) != 1) { + EVP_MD_CTX_free(ctx); + return {}; + } + + char buf[kBufferSize]; + while (!file.atEnd()) { + qint64 n = file.read(buf, kBufferSize); + if (n <= 0) + break; + if (EVP_DigestUpdate(ctx, buf, static_cast(n)) != 1) { + EVP_MD_CTX_free(ctx); + return {}; + } + } + + unsigned char hash[kSM3HashLen]; + unsigned int hashLen = 0; + if (EVP_DigestFinal_ex(ctx, hash, &hashLen) != 1) { + EVP_MD_CTX_free(ctx); + return {}; + } + + EVP_MD_CTX_free(ctx); + return toHex(hash, hashLen); +} diff --git a/src/dfm-burn/dfm-burn-lib/private/dsm3hash.h b/src/dfm-burn/dfm-burn-lib/private/dsm3hash.h new file mode 100644 index 00000000..8c57b996 --- /dev/null +++ b/src/dfm-burn/dfm-burn-lib/private/dsm3hash.h @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef DSM3HASH_H +#define DSM3HASH_H + +#include + +#include + +DFM_BURN_BEGIN_NS + +class DSM3Hash +{ +public: + /*! + * \brief Compute SM3 hash of a file. + * \param filePath Absolute path to the file. + * \return Hex-encoded SM3 hash string (64 chars), or empty string on failure. + */ + static QString sm3File(const QString &filePath); +}; + +DFM_BURN_END_NS + +#endif // DSM3HASH_H diff --git a/src/dfm-burn/dfm-burn-lib/private/dxorrisoengine.cpp b/src/dfm-burn/dfm-burn-lib/private/dxorrisoengine.cpp index ae2bd9c2..e91d4c1f 100644 --- a/src/dfm-burn/dfm-burn-lib/private/dxorrisoengine.cpp +++ b/src/dfm-burn/dfm-burn-lib/private/dxorrisoengine.cpp @@ -515,6 +515,32 @@ bool DXorrisoEngine::doBurn(const QPair files, int speed, QStr return true; } +bool DXorrisoEngine::doOsirroxOn() +{ + int r = XORRISO_OPT(xorriso, [this]() { + return Xorriso_option_osirrox(xorriso, PCHAR("on"), 0); + }); + return !JOBFAILED_IF(this, r, xorriso); +} + +bool DXorrisoEngine::doExtract(const QString &diskPath, const QString &isoPath) +{ + xorrisomsg.clear(); + + // NOTE: Despite the API parameter naming (disk_path, iso_path), xorriso actually + // treats the first argument as the ISO source path and the second as disk destination. + // This matches the CLI behavior: -extract + int r = XORRISO_OPT(xorriso, [this, diskPath, isoPath]() { + return Xorriso_option_extract(xorriso, + PCHAR(isoPath.toUtf8().data()), + PCHAR(diskPath.toUtf8().data()), 0); + }); + if (JOBFAILED_IF(this, r, xorriso)) + return false; + + return true; +} + void DXorrisoEngine::messageReceived(int type, char *text) { Q_UNUSED(type); diff --git a/src/dfm-burn/dfm-burn-lib/private/dxorrisoengine.h b/src/dfm-burn/dfm-burn-lib/private/dxorrisoengine.h index 570b7e69..beca39c8 100644 --- a/src/dfm-burn/dfm-burn-lib/private/dxorrisoengine.h +++ b/src/dfm-burn/dfm-burn-lib/private/dxorrisoengine.h @@ -53,6 +53,8 @@ class DXorrisoEngine : public QObject bool doCheckmedia(quint64 dataBlocks, double *qgood, double *qslow, double *qbad); bool doBurn(const QPair files, int speed, QString volId, JolietSupport joliet, RockRageSupport rockRage, KeepAppendable appendable); + bool doExtract(const QString &diskPath, const QString &isoPath); + bool doOsirroxOn(); public Q_SLOTS: void messageReceived(int type, char *text);