From 158e1152422b81d0685992924682a8738f883aea Mon Sep 17 00:00:00 2001 From: Martin Wohlert Date: Tue, 3 Nov 2026 20:19:21 +0100 Subject: [PATCH] first draft --- .gitignore | 5 + Cargo.lock | 150 ++ Cargo.toml | 23 + LICENSE-GPL-2.0 | 343 +++++ LICENSE-LGPL-2.1 | 508 +++++++ Makefile | 57 + README.md | 141 ++ crates/capi-types/Cargo.toml | 13 + crates/capi-types/src/lib.rs | 165 +++ crates/cdda-interface/Cargo.toml | 16 + crates/cdda-interface/build.rs | 3 + crates/cdda-interface/src/lib.rs | 571 ++++++++ crates/cdda-paranoia/Cargo.toml | 19 + crates/cdda-paranoia/build.rs | 3 + crates/cdda-paranoia/src/lib.rs | 314 ++++ crates/cli/Cargo.toml | 25 + crates/cli/src/analyze.rs | 295 ++++ crates/cli/src/args.rs | 420 ++++++ crates/cli/src/bin_cdparanoia.rs | 4 + crates/cli/src/lib.rs | 806 +++++++++++ crates/cli/src/log.rs | 121 ++ crates/cli/src/main.rs | 4 + crates/cli/src/output.rs | 226 +++ crates/cli/src/progress.rs | 351 +++++ crates/cli/src/span.rs | 271 ++++ crates/cli/tests/e2e.rs | 50 + crates/core/Cargo.toml | 11 + crates/core/src/callback.rs | 16 + crates/core/src/device/discover.rs | 138 ++ crates/core/src/device/image.rs | 323 +++++ crates/core/src/device/linux.rs | 1314 +++++++++++++++++ crates/core/src/device/mod.rs | 422 ++++++ crates/core/src/lib.rs | 27 + crates/core/src/paranoia/block.rs | 195 +++ crates/core/src/paranoia/mod.rs | 2043 +++++++++++++++++++++++++++ crates/core/src/paranoia/sort.rs | 87 ++ crates/core/tests/paranoia_image.rs | 130 ++ include/cdda_interface.h | 207 +++ include/cdda_paranoia.h | 52 + include/utils.h | 106 ++ install.sh | 5 + man/cdparanoia.1 | 385 +++++ man/cdrustanoia.1 | 385 +++++ tests/capi_compile.c | 35 + 44 files changed, 10785 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE-GPL-2.0 create mode 100644 LICENSE-LGPL-2.1 create mode 100644 Makefile create mode 100644 README.md create mode 100644 crates/capi-types/Cargo.toml create mode 100644 crates/capi-types/src/lib.rs create mode 100644 crates/cdda-interface/Cargo.toml create mode 100644 crates/cdda-interface/build.rs create mode 100644 crates/cdda-interface/src/lib.rs create mode 100644 crates/cdda-paranoia/Cargo.toml create mode 100644 crates/cdda-paranoia/build.rs create mode 100644 crates/cdda-paranoia/src/lib.rs create mode 100644 crates/cli/Cargo.toml create mode 100644 crates/cli/src/analyze.rs create mode 100644 crates/cli/src/args.rs create mode 100644 crates/cli/src/bin_cdparanoia.rs create mode 100644 crates/cli/src/lib.rs create mode 100644 crates/cli/src/log.rs create mode 100644 crates/cli/src/main.rs create mode 100644 crates/cli/src/output.rs create mode 100644 crates/cli/src/progress.rs create mode 100644 crates/cli/src/span.rs create mode 100644 crates/cli/tests/e2e.rs create mode 100644 crates/core/Cargo.toml create mode 100644 crates/core/src/callback.rs create mode 100644 crates/core/src/device/discover.rs create mode 100644 crates/core/src/device/image.rs create mode 100644 crates/core/src/device/linux.rs create mode 100644 crates/core/src/device/mod.rs create mode 100644 crates/core/src/lib.rs create mode 100644 crates/core/src/paranoia/block.rs create mode 100644 crates/core/src/paranoia/mod.rs create mode 100644 crates/core/src/paranoia/sort.rs create mode 100644 crates/core/tests/paranoia_image.rs create mode 100644 include/cdda_interface.h create mode 100644 include/cdda_paranoia.h create mode 100644 include/utils.h create mode 100755 install.sh create mode 100644 man/cdparanoia.1 create mode 100644 man/cdrustanoia.1 create mode 100644 tests/capi_compile.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e7b1645 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +target/ +*.log +cdda.* +track*.cdda.* +cdparanoia-III-10.2 diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..e5860a1 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,150 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "cdda_interface" +version = "0.1.0" +dependencies = [ + "cdrustanoia_capi_types", + "cdrustanoia_core", + "libc", +] + +[[package]] +name = "cdda_paranoia" +version = "0.1.0" +dependencies = [ + "cdda_interface", + "cdrustanoia_capi_types", + "cdrustanoia_core", + "libc", +] + +[[package]] +name = "cdrustanoia_capi_types" +version = "0.1.0" +dependencies = [ + "libc", +] + +[[package]] +name = "cdrustanoia_cli" +version = "0.1.0" +dependencies = [ + "cdrustanoia_core", + "libc", + "tempfile", +] + +[[package]] +name = "cdrustanoia_core" +version = "0.1.0" +dependencies = [ + "libc", + "tempfile", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rustix" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..714a601 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[workspace] +resolver = "2" +members = [ + "crates/core", + "crates/cli", + "crates/capi-types", + "crates/cdda-interface", + "crates/cdda-paranoia", +] + +[workspace.package] +version = "0.1.0" +edition = "2021" +license = "LGPL-2.1-or-later" + +[workspace.dependencies] +libc = "0.2" +cdrustanoia_core = { path = "crates/core" } +cdrustanoia_capi_types = { path = "crates/capi-types" } + +[profile.release] +lto = true +codegen-units = 1 diff --git a/LICENSE-GPL-2.0 b/LICENSE-GPL-2.0 new file mode 100644 index 0000000..e17fde6 --- /dev/null +++ b/LICENSE-GPL-2.0 @@ -0,0 +1,343 @@ +The cdparanoia command line tool versions 10.1 and later are +distributed under the GNU General Public Licence v2.0 or, at your +option, any later version of the GPL. + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/LICENSE-LGPL-2.1 b/LICENSE-LGPL-2.1 new file mode 100644 index 0000000..e796aff --- /dev/null +++ b/LICENSE-LGPL-2.1 @@ -0,0 +1,508 @@ +The 10.1 and later release versions of the cdda-interface and +cdda-paranoia libraries are distributed the GNU Lesser General Public +License v2.1 or, at your option, any later version of the GNU LGPL. + + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..28dfb19 --- /dev/null +++ b/Makefile @@ -0,0 +1,57 @@ +PREFIX ?= /usr/local +DESTDIR ?= +CARGO ?= cargo +CC ?= cc +INSTALL ?= install + +BINDIR = $(DESTDIR)$(PREFIX)/bin +LIBDIR = $(DESTDIR)$(PREFIX)/lib +INCLUDEDIR = $(DESTDIR)$(PREFIX)/include +MANDIR = $(DESTDIR)$(PREFIX)/share/man/man1 + +.PHONY: all build test check install uninstall clean + +all: build + +build: + $(CARGO) build --release --workspace + +test: + $(CARGO) test --workspace + +check: + $(CARGO) fmt --all -- --check + $(CARGO) clippy --workspace --all-targets -- -D warnings + $(CARGO) test --workspace + $(CC) -std=gnu11 -Wall -Wextra -Werror -Wno-unused-variable -Wno-unused-function -Iinclude -c tests/capi_compile.c -o target/capi_compile.o + +install: build + $(INSTALL) -d -m 0755 $(BINDIR) + $(INSTALL) -m 0755 target/release/cdrustanoia $(BINDIR)/cdrustanoia + ln -sf cdrustanoia $(BINDIR)/cdparanoia + $(INSTALL) -d -m 0755 $(MANDIR) + $(INSTALL) -m 0644 man/cdparanoia.1 $(MANDIR)/cdparanoia.1 + $(INSTALL) -m 0644 man/cdrustanoia.1 $(MANDIR)/cdrustanoia.1 + $(INSTALL) -d -m 0755 $(INCLUDEDIR) + $(INSTALL) -m 0644 include/cdda_interface.h $(INCLUDEDIR)/cdda_interface.h + $(INSTALL) -m 0644 include/cdda_paranoia.h $(INCLUDEDIR)/cdda_paranoia.h + $(INSTALL) -m 0644 include/utils.h $(INCLUDEDIR)/utils.h + $(INSTALL) -d -m 0755 $(LIBDIR) + $(INSTALL) -m 0755 target/release/libcdda_interface.so $(LIBDIR)/libcdda_interface.so.0.10.2 + $(INSTALL) -m 0755 target/release/libcdda_paranoia.so $(LIBDIR)/libcdda_paranoia.so.0.10.2 + ln -sf libcdda_interface.so.0.10.2 $(LIBDIR)/libcdda_interface.so.0 + ln -sf libcdda_interface.so.0 $(LIBDIR)/libcdda_interface.so + ln -sf libcdda_paranoia.so.0.10.2 $(LIBDIR)/libcdda_paranoia.so.0 + ln -sf libcdda_paranoia.so.0 $(LIBDIR)/libcdda_paranoia.so + $(INSTALL) -m 0644 target/release/libcdda_interface.a $(LIBDIR)/libcdda_interface.a + $(INSTALL) -m 0644 target/release/libcdda_paranoia.a $(LIBDIR)/libcdda_paranoia.a + +uninstall: + rm -f $(BINDIR)/cdrustanoia $(BINDIR)/cdparanoia + rm -f $(MANDIR)/cdparanoia.1 $(MANDIR)/cdrustanoia.1 + rm -f $(INCLUDEDIR)/cdda_interface.h $(INCLUDEDIR)/cdda_paranoia.h $(INCLUDEDIR)/utils.h + rm -f $(LIBDIR)/libcdda_interface.so* $(LIBDIR)/libcdda_paranoia.so* + rm -f $(LIBDIR)/libcdda_interface.a $(LIBDIR)/libcdda_paranoia.a + +clean: + $(CARGO) clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..329f064 --- /dev/null +++ b/README.md @@ -0,0 +1,141 @@ +# cdrustanoia + +`cdrustanoia` is a Rust port of cdparanoia III 10.2 for Linux. It is intended +to be a drop-in replacement for the `cdparanoia` command line tool and for the +`libcdda_interface` / `libcdda_paranoia` C libraries. + +The port includes: + +- the cdparanoia command line interface and span grammar, +- the two-stage verification, overlap, drift and rift-reconstruction engine, +- the Linux SG_IO and cooked-ioctl CD-ROM backends, +- TOC query and drive/endianness probing, +- WAV, AIFF, AIFF-C and raw output, +- batch mode, stdout output, sample/TOC offsets, logging and progress display, +- source- and ABI-compatible C libraries and headers, +- a synthetic image backend and fault-injection tests that do not need a CD + drive. + +## Building + +```sh +cargo build --release --workspace +``` + +The binaries are written to `target/release/cdrustanoia` and +`target/release/cdparanoia`. Both run the same code; `cdparanoia` exists so +existing scripts and packages can invoke the expected name. + +## Installing + +```sh +sudo make install PREFIX=/usr/local +# or +sudo ./install.sh +``` + +This installs: + +- `bin/cdrustanoia` and the `bin/cdparanoia` symlink, +- `man/man1/cdparanoia.1` and `cdrustanoia.1`, +- `include/cdda_interface.h`, `cdda_paranoia.h` and `utils.h`, +- `lib/libcdda_interface.{so,a}` and `lib/libcdda_paranoia.{so,a}`, +- the original `.so.0` SONAME symlinks. + +`DESTDIR` is supported for package builds. + +## Usage + +The command line is compatible with cdparanoia 10.2: + +```sh +cdrustanoia -Q +cdrustanoia -B +cdrustanoia "1[:30.12]-1[1:10]" track.wav +cdrustanoia -r -- "-3" out.raw +``` + +Use `cdrustanoia --help` for the full option and span documentation. The man +page is the original cdparanoia man page. + +## C API + +The two C libraries export the traditional symbols and use the original +public structure layout. The headers are installed under `include/`; link +programs the same way as with cdparanoia: + +```sh +cc app.c -lcdda_paranoia -lcdda_interface +``` + +The public `cdrom_drive` layout, `TOC` layout, function-pointer fields, error +codes and `paranoia_*` entry points match cdparanoia 10.2 on 64-bit Linux. + +## Testing without a CD drive + +Set `CDRUSTANOIA_TEST_IMAGE` to a raw CDDA image (2352-byte sectors). A +sidecar file named `..toc` may contain one line per track: + +```text +start_sector end_sector flags_hex +``` + +For example: + +```text +0 99 0 +100 199 0 +``` + +Then use the normal command line: + +```sh +CDRUSTANOIA_TEST_IMAGE=/tmp/disc.raw ./target/debug/cdparanoia -Q +CDRUSTANOIA_TEST_IMAGE=/tmp/disc.raw ./target/debug/cdparanoia -B +``` + +The backend is intentionally not advertised in `--help`; it exists for tests +and development. + +Run the automated suite with: + +```sh +cargo test --workspace +``` + +The suite covers span parsing, output headers, command-line parsing, the +paranoia engine (including jitter and fragmented reads), the C ABI layout, and +the C libraries. + +## Hardware checklist + +The SG_IO and cooked ioctl paths cannot be exercised in a machine without a +CD-ROM drive. When hardware is available, verify: + +1. `cdrustanoia -Q` lists the TOC. +2. `cdrustanoia -v -Q` shows SG_IO or cooked autosense. +3. `cdrustanoia -B` produces `trackNN.cdda.wav` and the audio matches the disc. +4. A deliberately scratched disc produces the expected `:-(`/`;-(` progress + and either reconstructs audio or removes the aborted file with `-X`. +5. `-A` completes and writes `cdparanoia.log`. +6. `-e` produces the `##:` callback lines for wrapper scripts. +7. A small C program using `cdda_identify`, `cdda_open`, `paranoia_init`, + `paranoia_read` and `cdda_close` works. + +## Compatibility notes + +- Linux is the only supported platform, matching cdparanoia 10.2. +- Modern SG_IO and legacy cooked ioctl access are implemented. The obsolete + `/dev/sg` SG2/SG3 packet transport is not; `-g` uses SG_IO on the named + generic device where the kernel supports it. +- `-i/--output-info` is accepted but is a no-op, matching the 10.2 source. +- 64-bit Linux ABI is the primary target. +- The `-A` cache analysis is a compact functional port; its timing + recommendations should be treated as advisory. + +## Licence + +The port is derived from cdparanoia III 10.2. The command line program is +GPL-2.0-or-later; the libraries are LGPL-2.1-or-later, following the original +distribution. See `LICENSE-GPL-2.0`, `LICENSE-LGPL-2.1` and the copyright +notices in the original headers. diff --git a/crates/capi-types/Cargo.toml b/crates/capi-types/Cargo.toml new file mode 100644 index 0000000..9d88117 --- /dev/null +++ b/crates/capi-types/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "cdrustanoia_capi_types" +version.workspace = true +edition.workspace = true +license.workspace = true +publish = false + +[lib] +name = "cdrustanoia_capi_types" +path = "src/lib.rs" + +[dependencies] +libc.workspace = true diff --git a/crates/capi-types/src/lib.rs b/crates/capi-types/src/lib.rs new file mode 100644 index 0000000..259b8b0 --- /dev/null +++ b/crates/capi-types/src/lib.rs @@ -0,0 +1,165 @@ +//! C ABI-compatible public structures shared by the two cdda libraries. + +use libc::{c_char, c_int, c_long, c_void, sigset_t}; + +pub const MAXTRK: usize = 100; + +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct Toc { + pub b_flags: u8, + pub b_track: u8, + pub dw_start_sector: i32, +} + +pub type EnableCddaFn = Option c_int>; +pub type ReadTocFn = Option c_int>; +pub type ReadAudioFn = + Option c_long>; +pub type SetSpeedFn = Option c_int>; + +#[repr(C)] +pub struct CddaPrivateData { + pub holder: *mut c_void, + pub last_milliseconds: c_int, + pub clock: c_int, +} + +#[repr(C)] +pub struct CdromDrive { + pub opened: c_int, + pub cdda_device_name: *mut c_char, + pub ioctl_device_name: *mut c_char, + pub cdda_fd: c_int, + pub ioctl_fd: c_int, + pub drive_model: *mut c_char, + pub drive_type: c_int, + pub interface: c_int, + pub bigendianp: c_int, + pub nsectors: c_int, + pub cd_extra: c_int, + pub tracks: c_int, + pub disc_toc: [Toc; MAXTRK], + pub audio_first_sector: c_long, + pub audio_last_sector: c_long, + pub errordest: c_int, + pub messagedest: c_int, + pub errorbuf: *mut c_char, + pub messagebuf: *mut c_char, + pub enable_cdda: EnableCddaFn, + pub read_toc: ReadTocFn, + pub read_audio: ReadAudioFn, + pub set_speed: SetSpeedFn, + pub error_retry: c_int, + pub report_all: c_int, + pub is_atapi: c_int, + pub is_mmc: c_int, + pub private: *mut CddaPrivateData, + pub reserved: *mut c_void, + pub inqbytes: [u8; 4], + pub density: u8, + pub orgdens: u8, + pub orgsize: u32, + pub bigbuff: c_long, + pub adjust_ssize: c_int, + pub fua: c_int, + pub lun: c_int, + pub sigset: sigset_t, +} + +impl CdromDrive { + pub fn empty() -> Self { + Self { + opened: 0, + cdda_device_name: std::ptr::null_mut(), + ioctl_device_name: std::ptr::null_mut(), + cdda_fd: -1, + ioctl_fd: -1, + drive_model: std::ptr::null_mut(), + drive_type: 0, + interface: 0, + bigendianp: -1, + nsectors: -1, + cd_extra: 0, + tracks: 0, + disc_toc: [Toc::default(); MAXTRK], + audio_first_sector: 0, + audio_last_sector: 0, + errordest: 0, + messagedest: 0, + errorbuf: std::ptr::null_mut(), + messagebuf: std::ptr::null_mut(), + enable_cdda: None, + read_toc: None, + read_audio: None, + set_speed: None, + error_retry: 0, + report_all: 0, + is_atapi: 0, + is_mmc: 0, + private: std::ptr::null_mut(), + reserved: std::ptr::null_mut(), + inqbytes: [0; 4], + density: 0, + orgdens: 0, + orgsize: 0, + bigbuff: 0, + adjust_ssize: 1, + fua: 0, + lun: 0, + sigset: unsafe { std::mem::zeroed() }, + } + } +} + +pub const INTERFACE_GENERIC_SCSI: c_int = 0; +pub const INTERFACE_COOKED_IOCTL: c_int = 1; +pub const INTERFACE_TEST: c_int = 2; +pub const INTERFACE_SGIO_SCSI: c_int = 3; +pub const INTERFACE_SGIO_SCSI_BUGGY1: c_int = 4; + +pub const CDDA_MESSAGE_FORGETIT: c_int = 0; +pub const CDDA_MESSAGE_PRINTIT: c_int = 1; +pub const CDDA_MESSAGE_LOGIT: c_int = 2; + +#[cfg(test)] +mod tests { + use super::*; + use std::mem::{offset_of, size_of}; + + #[test] + fn abi_layout_matches_cdparanoia() { + assert_eq!(size_of::(), 8); + assert_eq!(offset_of!(Toc, b_flags), 0); + assert_eq!(offset_of!(Toc, b_track), 1); + assert_eq!(offset_of!(Toc, dw_start_sector), 4); + assert_eq!(size_of::(), 1136); + assert_eq!(offset_of!(CdromDrive, opened), 0); + assert_eq!(offset_of!(CdromDrive, cdda_device_name), 8); + assert_eq!(offset_of!(CdromDrive, cdda_fd), 24); + assert_eq!(offset_of!(CdromDrive, drive_model), 32); + assert_eq!(offset_of!(CdromDrive, drive_type), 40); + assert_eq!(offset_of!(CdromDrive, interface), 44); + assert_eq!(offset_of!(CdromDrive, bigendianp), 48); + assert_eq!(offset_of!(CdromDrive, nsectors), 52); + assert_eq!(offset_of!(CdromDrive, cd_extra), 56); + assert_eq!(offset_of!(CdromDrive, tracks), 60); + assert_eq!(offset_of!(CdromDrive, disc_toc), 64); + assert_eq!(offset_of!(CdromDrive, audio_first_sector), 864); + assert_eq!(offset_of!(CdromDrive, errordest), 880); + assert_eq!(offset_of!(CdromDrive, errorbuf), 888); + assert_eq!(offset_of!(CdromDrive, error_retry), 936); + assert_eq!(offset_of!(CdromDrive, report_all), 940); + assert_eq!(offset_of!(CdromDrive, is_atapi), 944); + assert_eq!(offset_of!(CdromDrive, is_mmc), 948); + assert_eq!(offset_of!(CdromDrive, private), 952); + assert_eq!(offset_of!(CdromDrive, reserved), 960); + assert_eq!(offset_of!(CdromDrive, inqbytes), 968); + assert_eq!(offset_of!(CdromDrive, density), 972); + assert_eq!(offset_of!(CdromDrive, bigbuff), 984); + assert_eq!(offset_of!(CdromDrive, adjust_ssize), 992); + assert_eq!(offset_of!(CdromDrive, fua), 996); + assert_eq!(offset_of!(CdromDrive, lun), 1000); + assert_eq!(offset_of!(CdromDrive, sigset), 1008); + } +} diff --git a/crates/cdda-interface/Cargo.toml b/crates/cdda-interface/Cargo.toml new file mode 100644 index 0000000..1e063c7 --- /dev/null +++ b/crates/cdda-interface/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "cdda_interface" +version.workspace = true +edition.workspace = true +license.workspace = true +publish = false + +[lib] +name = "cdda_interface" +crate-type = ["cdylib", "staticlib", "rlib"] +path = "src/lib.rs" + +[dependencies] +cdrustanoia_core.workspace = true +cdrustanoia_capi_types.workspace = true +libc.workspace = true diff --git a/crates/cdda-interface/build.rs b/crates/cdda-interface/build.rs new file mode 100644 index 0000000..3ac1e8d --- /dev/null +++ b/crates/cdda-interface/build.rs @@ -0,0 +1,3 @@ +fn main() { + println!("cargo:rustc-cdylib-link-arg=-Wl,-soname,libcdda_interface.so.0"); +} diff --git a/crates/cdda-interface/src/lib.rs b/crates/cdda-interface/src/lib.rs new file mode 100644 index 0000000..a0a11c4 --- /dev/null +++ b/crates/cdda-interface/src/lib.rs @@ -0,0 +1,571 @@ +#![allow( + clippy::missing_safety_doc, + clippy::unnecessary_cast, + clippy::field_reassign_with_default +)] + +//! C ABI-compatible libcdda_interface implementation. + +use cdrustanoia_capi_types::*; +use cdrustanoia_core::device::discover; +use cdrustanoia_core::device::{DiscToc, Drive, InterfaceKind, ReadError}; +use libc::{c_char, c_int, c_long, c_void}; +use std::ffi::CString; +use std::ptr; + +pub struct BackendHolder { + pub drive: Box, +} + +unsafe fn cstr(s: &str) -> *mut c_char { + let clean: String = s.chars().filter(|c| *c != '\0').collect(); + match CString::new(clean) { + Ok(c) => libc::strdup(c.as_ptr()), + Err(_) => ptr::null_mut(), + } +} + +unsafe fn set_error(d: *mut CdromDrive, message: &str) { + if d.is_null() { + return; + } + if !(*d).errorbuf.is_null() { + libc::free((*d).errorbuf as *mut c_void); + } + (*d).errorbuf = cstr(message); +} + +pub unsafe fn holder<'a>(d: *mut CdromDrive) -> Option<&'a mut BackendHolder> { + if d.is_null() { + return None; + } + let private = (*d).private; + if private.is_null() { + return None; + } + let h = (*private).holder as *mut BackendHolder; + if h.is_null() { + None + } else { + Some(&mut *h) + } +} + +pub unsafe fn make_drive( + device: &str, + ioctl_device: Option<&str>, + backend: Box, +) -> *mut CdromDrive { + let mut d = Box::new(CdromDrive::empty()); + d.drive_type = 0; + d.interface = match backend.interface() { + InterfaceKind::SgIo => INTERFACE_SGIO_SCSI, + InterfaceKind::Cooked => INTERFACE_COOKED_IOCTL, + InterfaceKind::Image => INTERFACE_TEST, + }; + d.bigendianp = backend.bigendianp(); + d.nsectors = backend.nsectors() as c_int; + d.cdda_device_name = cstr(device); + d.ioctl_device_name = cstr(ioctl_device.unwrap_or(device)); + d.drive_model = cstr(backend.drive_model()); + d.enable_cdda = Some(cb_enable_cdda); + d.read_toc = Some(cb_read_toc); + d.read_audio = Some(cb_read_audio); + d.set_speed = Some(cb_set_speed); + d.error_retry = 1; + d.report_all = 0; + d.adjust_ssize = 1; + d.lun = 0; + let private = Box::new(CddaPrivateData { + holder: Box::into_raw(Box::new(BackendHolder { drive: backend })) as *mut c_void, + last_milliseconds: -1, + clock: 0, + }); + d.private = Box::into_raw(private); + Box::into_raw(d) +} + +unsafe fn free_drive(d: *mut CdromDrive) { + if d.is_null() { + return; + } + let private = (*d).private; + if !private.is_null() { + let holder = (*private).holder as *mut BackendHolder; + if !holder.is_null() { + drop(Box::from_raw(holder)); + } + drop(Box::from_raw(private)); + (*d).private = ptr::null_mut(); + } + if !(*d).cdda_device_name.is_null() { + libc::free((*d).cdda_device_name as *mut c_void); + } + if !(*d).ioctl_device_name.is_null() { + libc::free((*d).ioctl_device_name as *mut c_void); + } + if !(*d).drive_model.is_null() { + libc::free((*d).drive_model as *mut c_void); + } + if !(*d).errorbuf.is_null() { + libc::free((*d).errorbuf as *mut c_void); + } + if !(*d).messagebuf.is_null() { + libc::free((*d).messagebuf as *mut c_void); + } + drop(Box::from_raw(d)); +} + +unsafe extern "C" fn cb_enable_cdda(_d: *mut CdromDrive, _onoff: c_int) -> c_int { + 0 +} + +unsafe extern "C" fn cb_read_toc(d: *mut CdromDrive) -> c_int { + if d.is_null() { + return -1; + } + (*d).tracks +} + +unsafe fn read_into( + d: *mut CdromDrive, + p: *mut c_void, + begin: c_long, + sectors: c_long, +) -> Result { + let h = holder(d).ok_or_else(ReadError::closed)?; + if p.is_null() { + h.drive.read_audio_raw(None, begin as i64, sectors as i64) + } else { + let len = (sectors.max(0) as usize) * cdrustanoia_core::device::CD_FRAMESIZE_RAW; + let buf = std::slice::from_raw_parts_mut(p as *mut u8, len); + h.drive + .read_audio_raw(Some(buf), begin as i64, sectors as i64) + } +} + +unsafe extern "C" fn cb_read_audio( + d: *mut CdromDrive, + p: *mut c_void, + begin: c_long, + sectors: c_long, +) -> c_long { + match read_into(d, p, begin, sectors) { + Ok(n) => n as c_long, + Err(e) => { + set_errno(e.errno); + match e.errno { + libc::ENOMEDIUM => -404, + libc::ENOMEM => -300, + _ => -7, + } + } + } +} + +unsafe extern "C" fn cb_set_speed(d: *mut CdromDrive, speed: c_int) -> c_int { + match holder(d) { + Some(h) => h.drive.speed_set(speed), + None => -405, + } +} + +fn set_errno(value: c_int) { + unsafe { + *libc::__errno_location() = value; + } +} + +fn toc_from_drive(d: &CdromDrive) -> DiscToc { + let mut toc = DiscToc::default(); + toc.tracks = d.tracks; + toc.cd_extra = d.cd_extra; + for i in 0..MAXTRK { + toc.entries[i].flags = d.disc_toc[i].b_flags; + toc.entries[i].track = d.disc_toc[i].b_track; + toc.entries[i].start_sector = d.disc_toc[i].dw_start_sector as i64; + } + toc +} + +unsafe fn fill_toc(d: *mut CdromDrive) { + let h = match holder(d) { + Some(h) => h, + None => return, + }; + let toc = h.drive.toc().clone(); + (*d).tracks = toc.tracks; + (*d).cd_extra = toc.cd_extra; + for i in 0..MAXTRK { + (*d).disc_toc[i].b_flags = toc.entries[i].flags; + (*d).disc_toc[i].b_track = toc.entries[i].track; + (*d).disc_toc[i].dw_start_sector = toc.entries[i].start_sector as i32; + } + (*d).audio_first_sector = toc.disc_firstsector() as c_long; + (*d).audio_last_sector = toc.disc_lastsector() as c_long; + (*d).bigendianp = h.drive.bigendianp(); + (*d).nsectors = h.drive.nsectors() as c_int; +} + +#[no_mangle] +pub unsafe extern "C" fn cdda_identify( + device: *const c_char, + messagedest: c_int, + messages: *mut *mut c_char, +) -> *mut CdromDrive { + if !messages.is_null() { + *messages = ptr::null_mut(); + } + if device.is_null() { + return ptr::null_mut(); + } + let device = match std::ffi::CStr::from_ptr(device).to_str() { + Ok(s) => s, + Err(_) => return ptr::null_mut(), + }; + match discover::identify_device(device, messagedest == CDDA_MESSAGE_PRINTIT) { + Ok(drive) => make_drive(device, Some(device), drive), + Err(_) => ptr::null_mut(), + } +} + +#[no_mangle] +pub unsafe extern "C" fn cdda_identify_cooked( + device: *const c_char, + messagedest: c_int, + messages: *mut *mut c_char, +) -> *mut CdromDrive { + if !messages.is_null() { + *messages = ptr::null_mut(); + } + if device.is_null() { + return ptr::null_mut(); + } + let device = match std::ffi::CStr::from_ptr(device).to_str() { + Ok(s) => s, + Err(_) => return ptr::null_mut(), + }; + match discover::identify_cooked(device, messagedest == CDDA_MESSAGE_PRINTIT) { + Ok(drive) => make_drive(device, Some(device), drive), + Err(_) => ptr::null_mut(), + } +} + +#[no_mangle] +pub unsafe extern "C" fn cdda_identify_scsi( + generic_device: *const c_char, + ioctl_device: *const c_char, + messagedest: c_int, + messages: *mut *mut c_char, +) -> *mut CdromDrive { + if !messages.is_null() { + *messages = ptr::null_mut(); + } + let generic = if generic_device.is_null() { + None + } else { + std::ffi::CStr::from_ptr(generic_device).to_str().ok() + }; + let ioctl = if ioctl_device.is_null() { + None + } else { + std::ffi::CStr::from_ptr(ioctl_device).to_str().ok() + }; + let verbose = messagedest == CDDA_MESSAGE_PRINTIT; + if let Some(dev) = generic { + if let Ok(drive) = discover::identify_sgio(dev, verbose) { + return make_drive(dev, ioctl, drive); + } + } + if let Some(dev) = ioctl { + if let Ok(drive) = discover::identify_device(dev, verbose) { + return make_drive(dev, Some(dev), drive); + } + } + ptr::null_mut() +} + +#[no_mangle] +pub unsafe extern "C" fn cdda_find_a_cdrom( + messagedest: c_int, + messages: *mut *mut c_char, +) -> *mut CdromDrive { + if !messages.is_null() { + *messages = ptr::null_mut(); + } + match discover::find_a_cdrom(messagedest == CDDA_MESSAGE_PRINTIT) { + Some(drive) => make_drive("auto", None, drive), + None => ptr::null_mut(), + } +} + +#[no_mangle] +pub unsafe extern "C" fn cdda_open(d: *mut CdromDrive) -> c_int { + if d.is_null() { + return -100; + } + if (*d).opened != 0 { + return 0; + } + if let Some(h) = holder(d) { + if let Err(e) = h.drive.open_disc() { + set_errno(libc::EIO); + set_error(d, &format!("Unable to open disc: {}\n", e)); + return if e.contains("illegal number of tracks") { + -3 + } else if e.contains("contents header") { + -4 + } else if e.contains("contents entry") { + -5 + } else if e.contains("lead-out") { + -2 + } else if e.contains("CDDA") || e.contains("command set") { + -6 + } else { + -9 + }; + } + } + fill_toc(d); + if (*d).tracks < 1 { + (*d).opened = 0; + return -9; + } + for i in 0..(*d).tracks as usize { + if (*d).disc_toc[i].dw_start_sector < 0 || (*d).disc_toc[i + 1].dw_start_sector == 0 { + (*d).opened = 0; + return -9; + } + } + (*d).opened = 1; + 0 +} + +#[no_mangle] +pub unsafe extern "C" fn cdda_close(d: *mut CdromDrive) -> c_int { + if d.is_null() { + return 0; + } + free_drive(d); + 0 +} + +#[no_mangle] +pub unsafe extern "C" fn cdda_read_timed( + d: *mut CdromDrive, + buffer: *mut c_void, + beginsector: c_long, + sectors: c_long, + ms: *mut c_int, +) -> c_long { + if !ms.is_null() { + *ms = -1; + } + if d.is_null() || (*d).opened == 0 { + set_errno(libc::EBADF); + set_error(d, "400: Device not open\n"); + return -400; + } + if sectors <= 0 { + return sectors; + } + let h = match holder(d) { + Some(h) => h, + None => { + set_errno(libc::EBADF); + return -400; + } + }; + if (*d).bigendianp >= 0 { + h.drive.set_bigendianp((*d).bigendianp); + } + if (*d).nsectors > 0 { + h.drive.set_nsectors((*d).nsectors as i64); + } + let timed = if buffer.is_null() { + h.drive.read_timed(None, beginsector as i64, sectors as i64) + } else { + let len = (sectors as usize) * cdrustanoia_core::device::CD_FRAMESIZE_RAW; + let buf = std::slice::from_raw_parts_mut(buffer as *mut u8, len); + h.drive + .read_timed(Some(buf), beginsector as i64, sectors as i64) + }; + if !ms.is_null() { + *ms = timed.milliseconds; + } + if (*d).bigendianp < 0 { + (*d).bigendianp = h.drive.bigendianp(); + } + match timed.result { + Ok(n) => n as c_long, + Err(e) => { + set_errno(e.errno); + set_error(d, &format!("SCSI read error: {}\n", e.message)); + match e.errno { + libc::ENOMEDIUM => -404, + libc::ENOMEM => -300, + _ => -7, + } + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn cdda_read( + d: *mut CdromDrive, + buffer: *mut c_void, + beginsector: c_long, + sectors: c_long, +) -> c_long { + cdda_read_timed(d, buffer, beginsector, sectors, ptr::null_mut()) +} + +#[no_mangle] +pub unsafe extern "C" fn cdda_speed_set(d: *mut CdromDrive, speed: c_int) -> c_int { + match holder(d) { + Some(h) => { + let ret = h.drive.speed_set(speed); + if ret == 0 { + 0 + } else { + -405 + } + } + None => -405, + } +} + +#[no_mangle] +pub unsafe extern "C" fn cdda_verbose_set( + d: *mut CdromDrive, + err_action: c_int, + mes_action: c_int, +) { + if !d.is_null() { + (*d).errordest = err_action; + (*d).messagedest = mes_action; + } +} + +#[no_mangle] +pub unsafe extern "C" fn cdda_messages(d: *mut CdromDrive) -> *mut c_char { + if d.is_null() { + return ptr::null_mut(); + } + let ret = (*d).messagebuf; + (*d).messagebuf = ptr::null_mut(); + ret +} + +#[no_mangle] +pub unsafe extern "C" fn cdda_errors(d: *mut CdromDrive) -> *mut c_char { + if d.is_null() { + return ptr::null_mut(); + } + let ret = (*d).errorbuf; + (*d).errorbuf = ptr::null_mut(); + ret +} + +#[no_mangle] +pub extern "C" fn cdda_version() -> *const c_char { + c"10.2".as_ptr() +} + +#[no_mangle] +pub unsafe extern "C" fn cdda_tracks(d: *mut CdromDrive) -> c_long { + if d.is_null() || (*d).opened == 0 { + return -400; + } + (*d).tracks as c_long +} + +#[no_mangle] +pub unsafe extern "C" fn cdda_track_firstsector(d: *mut CdromDrive, track: c_int) -> c_long { + if d.is_null() || (*d).opened == 0 { + return -400; + } + toc_from_drive(&*d).track_firstsector(track) as c_long +} + +#[no_mangle] +pub unsafe extern "C" fn cdda_track_lastsector(d: *mut CdromDrive, track: c_int) -> c_long { + if d.is_null() || (*d).opened == 0 { + return -400; + } + toc_from_drive(&*d).track_lastsector(track) as c_long +} + +#[no_mangle] +pub unsafe extern "C" fn cdda_disc_firstsector(d: *mut CdromDrive) -> c_long { + if d.is_null() || (*d).opened == 0 { + return -400; + } + toc_from_drive(&*d).disc_firstsector() as c_long +} + +#[no_mangle] +pub unsafe extern "C" fn cdda_disc_lastsector(d: *mut CdromDrive) -> c_long { + if d.is_null() || (*d).opened == 0 { + return -400; + } + toc_from_drive(&*d).disc_lastsector() as c_long +} + +#[no_mangle] +pub unsafe extern "C" fn cdda_sector_gettrack(d: *mut CdromDrive, sector: c_long) -> c_int { + if d.is_null() || (*d).opened == 0 { + return -400; + } + toc_from_drive(&*d).sector_gettrack(sector as i64) +} + +#[no_mangle] +pub unsafe extern "C" fn cdda_track_channels(d: *mut CdromDrive, track: c_int) -> c_int { + if d.is_null() || (*d).opened == 0 { + return -400; + } + toc_from_drive(&*d).track_channels(track) +} + +#[no_mangle] +pub unsafe extern "C" fn cdda_track_audiop(d: *mut CdromDrive, track: c_int) -> c_int { + if d.is_null() || (*d).opened == 0 { + return -400; + } + toc_from_drive(&*d).track_audiop(track) +} + +#[no_mangle] +pub unsafe extern "C" fn cdda_track_copyp(d: *mut CdromDrive, track: c_int) -> c_int { + if d.is_null() || (*d).opened == 0 { + return -400; + } + toc_from_drive(&*d).track_copyp(track) +} + +#[no_mangle] +pub unsafe extern "C" fn cdda_track_preemp(d: *mut CdromDrive, track: c_int) -> c_int { + if d.is_null() || (*d).opened == 0 { + return -400; + } + toc_from_drive(&*d).track_preemp(track) +} + +#[no_mangle] +pub unsafe extern "C" fn cdda_track_bitmap( + d: *mut CdromDrive, + track: c_int, + bit: c_int, + set: c_int, + clear: c_int, +) -> c_int { + if d.is_null() || (*d).opened == 0 { + return -400; + } + toc_from_drive(&*d).track_bitmap(track, bit as u8, set, clear) +} + +// Keep the c_long import used on all targets. +#[allow(dead_code)] +fn _typecheck(_: c_long) {} diff --git a/crates/cdda-paranoia/Cargo.toml b/crates/cdda-paranoia/Cargo.toml new file mode 100644 index 0000000..4e813b7 --- /dev/null +++ b/crates/cdda-paranoia/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "cdda_paranoia" +version.workspace = true +edition.workspace = true +license.workspace = true +publish = false + +[lib] +name = "cdda_paranoia" +crate-type = ["cdylib", "staticlib", "rlib"] +path = "src/lib.rs" + +[dependencies] +cdrustanoia_core.workspace = true +cdrustanoia_capi_types.workspace = true +libc.workspace = true + +[dev-dependencies] +cdda_interface = { path = "../cdda-interface" } diff --git a/crates/cdda-paranoia/build.rs b/crates/cdda-paranoia/build.rs new file mode 100644 index 0000000..6dbd27c --- /dev/null +++ b/crates/cdda-paranoia/build.rs @@ -0,0 +1,3 @@ +fn main() { + println!("cargo:rustc-cdylib-link-arg=-Wl,-soname,libcdda_paranoia.so.0"); +} diff --git a/crates/cdda-paranoia/src/lib.rs b/crates/cdda-paranoia/src/lib.rs new file mode 100644 index 0000000..e2e9a46 --- /dev/null +++ b/crates/cdda-paranoia/src/lib.rs @@ -0,0 +1,314 @@ +#![allow( + clippy::missing_safety_doc, + clippy::unnecessary_cast, + clippy::field_reassign_with_default +)] + +//! C ABI-compatible libcdda_paranoia implementation. + +use cdrustanoia_capi_types::*; +use cdrustanoia_core::device::{ + host_big_endian, swap_raw16, DiscToc, Drive, InterfaceKind, ReadError, TimedRead, + CD_FRAMESIZE_RAW, MAXTRK, +}; +use cdrustanoia_core::paranoia::Paranoia; +use libc::{c_char, c_int, c_long, c_void}; +use std::ffi::CStr; +use std::ptr; +use std::time::Instant; + +pub type ParanoiaCallback = Option; + +struct CAbiDrive { + d: *mut CdromDrive, + toc: DiscToc, + model: String, + bigendianp: i32, + nsectors: i64, + interface: InterfaceKind, +} + +unsafe fn toc_from_d(d: *mut CdromDrive) -> DiscToc { + let mut toc = DiscToc::default(); + if d.is_null() { + return toc; + } + toc.tracks = (*d).tracks; + toc.cd_extra = (*d).cd_extra; + for i in 0..MAXTRK { + toc.entries[i].flags = (*d).disc_toc[i].b_flags; + toc.entries[i].track = (*d).disc_toc[i].b_track; + toc.entries[i].start_sector = (*d).disc_toc[i].dw_start_sector as i64; + } + toc +} + +impl CAbiDrive { + unsafe fn new(d: *mut CdromDrive) -> Self { + let model = if (*d).drive_model.is_null() { + String::new() + } else { + CStr::from_ptr((*d).drive_model) + .to_string_lossy() + .into_owned() + }; + let interface = match (*d).interface { + INTERFACE_COOKED_IOCTL => InterfaceKind::Cooked, + INTERFACE_TEST => InterfaceKind::Image, + _ => InterfaceKind::SgIo, + }; + Self { + d, + toc: toc_from_d(d), + model, + bigendianp: (*d).bigendianp, + nsectors: (*d).nsectors as i64, + interface, + } + } + + fn refresh(&mut self) { + unsafe { + self.toc = toc_from_d(self.d); + self.bigendianp = (*self.d).bigendianp; + self.nsectors = (*self.d).nsectors as i64; + } + } +} + +impl Drive for CAbiDrive { + fn toc(&self) -> &DiscToc { + &self.toc + } + fn toc_mut(&mut self) -> &mut DiscToc { + &mut self.toc + } + fn opened(&self) -> bool { + unsafe { !self.d.is_null() && (*self.d).opened != 0 } + } + fn read_audio( + &mut self, + mut buffer: Option<&mut [u8]>, + begin: i64, + sectors: i64, + ) -> Result { + unsafe { + if self.d.is_null() || (*self.d).opened == 0 { + return Err(ReadError::closed()); + } + let f = (*self.d) + .read_audio + .ok_or_else(|| ReadError::new(libc::EIO, "No read function"))?; + let ptr = match buffer.as_mut() { + Some(b) => b.as_mut_ptr() as *mut c_void, + None => ptr::null_mut(), + }; + let ret = f(self.d, ptr, begin as c_long, sectors as c_long); + if ret < 0 { + let errno = match ret { + -404 => libc::ENOMEDIUM, + -300 => libc::ENOMEM, + _ => libc::EIO, + }; + return Err(ReadError::new(errno, "SCSI read error")); + } + let n = ret as usize; + if let Some(buf) = buffer.as_mut() { + let be = (*self.d).bigendianp; + if n > 0 && be >= 0 && (be == 1) != host_big_endian() { + let len = (n * CD_FRAMESIZE_RAW).min(buf.len()); + swap_raw16(&mut buf[..len]); + } + } + Ok(n) + } + } + fn read_timed(&mut self, buffer: Option<&mut [u8]>, begin: i64, sectors: i64) -> TimedRead { + let started = Instant::now(); + let result = self.read_audio(buffer, begin, sectors); + TimedRead { + result, + milliseconds: started.elapsed().as_millis() as i32, + } + } + fn speed_set(&mut self, speed: i32) -> i32 { + unsafe { + if self.d.is_null() { + return -405; + } + match (*self.d).set_speed { + Some(f) => f(self.d, speed as c_int), + None => -405, + } + } + } + fn close(&mut self) { + // The paranoia library never closes the caller-owned drive. + } + fn drive_model(&self) -> &str { + &self.model + } + fn bigendianp(&self) -> i32 { + self.bigendianp + } + fn set_bigendianp(&mut self, value: i32) { + self.bigendianp = value; + unsafe { + if !self.d.is_null() { + (*self.d).bigendianp = value; + } + } + } + fn nsectors(&self) -> i64 { + self.nsectors + } + fn set_nsectors(&mut self, value: i64) { + self.nsectors = value; + unsafe { + if !self.d.is_null() { + (*self.d).nsectors = value as c_int; + } + } + } + fn interface(&self) -> InterfaceKind { + self.interface + } +} + +unsafe fn p_ref<'a>(p: *mut c_void) -> Option<&'a mut Paranoia> { + if p.is_null() { + None + } else { + Some(&mut *(p as *mut Paranoia)) + } +} + +#[no_mangle] +pub unsafe extern "C" fn paranoia_version() -> *const c_char { + c"10.2".as_ptr() +} + +#[no_mangle] +pub unsafe extern "C" fn paranoia_init(d: *mut CdromDrive) -> *mut c_void { + if d.is_null() { + return ptr::null_mut(); + } + let mut adapter = CAbiDrive::new(d); + adapter.refresh(); + let p = Paranoia::new(Box::new(adapter)); + Box::into_raw(Box::new(p)) as *mut c_void +} + +#[no_mangle] +pub unsafe extern "C" fn paranoia_free(p: *mut c_void) { + if !p.is_null() { + drop(Box::from_raw(p as *mut Paranoia)); + } +} + +#[no_mangle] +pub unsafe extern "C" fn paranoia_modeset(p: *mut c_void, mode: c_int) { + if let Some(p) = p_ref(p) { + p.modeset(mode); + } +} + +#[no_mangle] +pub unsafe extern "C" fn paranoia_seek(p: *mut c_void, seek: c_long, mode: c_int) -> c_long { + match p_ref(p) { + Some(p) => p.seek(seek as i64, mode) as c_long, + None => -1, + } +} + +#[no_mangle] +pub unsafe extern "C" fn paranoia_overlapset(p: *mut c_void, overlap: c_long) { + if let Some(p) = p_ref(p) { + p.overlapset(overlap as i64); + } +} + +#[no_mangle] +pub unsafe extern "C" fn paranoia_cachemodel_size(p: *mut c_void, sectors: c_int) -> c_int { + match p_ref(p) { + Some(p) => p.cachemodel_size(sectors as i64) as c_int, + None => 0, + } +} + +#[no_mangle] +pub unsafe extern "C" fn paranoia_read(p: *mut c_void, callback: ParanoiaCallback) -> *mut i16 { + paranoia_read_limited(p, callback, 20) +} + +#[no_mangle] +pub unsafe extern "C" fn paranoia_read_limited( + p: *mut c_void, + callback: ParanoiaCallback, + max_retries: c_int, +) -> *mut i16 { + let p = match p_ref(p) { + Some(p) => p, + None => return ptr::null_mut(), + }; + let mut cb = |pos: i64, func: i32| { + if let Some(f) = callback { + f(pos as c_long, func as c_int); + } + }; + match p.read_limited(&mut cb, max_retries) { + Some(slice) => slice.as_mut_ptr(), + None => { + let errno = if p.drive().opened() { + libc::EIO + } else { + libc::EBADF + }; + *libc::__errno_location() = errno; + ptr::null_mut() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use cdrustanoia_core::device::image::ImageDrive; + use cdrustanoia_core::device::samples_to_bytes; + use cdrustanoia_core::device::CD_FRAMEWORDS; + + #[test] + fn c_api_query_and_read() { + unsafe { + let mut samples = Vec::new(); + for i in 0..(3 * 1176) { + samples.push(((i * 37) as i16).wrapping_mul(3)); + } + let bytes = samples_to_bytes(&samples); + let img = ImageDrive::from_bytes(bytes); + let d = cdda_interface::make_drive("test", None, Box::new(img)); + assert_eq!(cdda_interface::cdda_open(d), 0); + assert_eq!(cdda_interface::cdda_tracks(d), 1); + assert_eq!(cdda_interface::cdda_disc_firstsector(d), 0); + assert_eq!(cdda_interface::cdda_disc_lastsector(d), 2); + + let mut raw = vec![0u8; CD_FRAMESIZE_RAW]; + let n = cdda_interface::cdda_read(d, raw.as_mut_ptr() as *mut c_void, 0, 1); + assert_eq!(n, 1); + assert_eq!(raw, samples_to_bytes(&samples[..1176])); + + let p = paranoia_init(d); + assert!(!p.is_null()); + paranoia_modeset(p, cdrustanoia_core::paranoia::PARANOIA_MODE_FULL); + paranoia_seek(p, 0, 0); + for sector in 0..3 { + let ptr = paranoia_read(p, None); + assert!(!ptr.is_null()); + let got = std::slice::from_raw_parts(ptr, CD_FRAMEWORDS as usize); + assert_eq!(got, &samples[sector * 1176..(sector + 1) * 1176]); + } + paranoia_free(p); + cdda_interface::cdda_close(d); + } + } +} diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml new file mode 100644 index 0000000..02e3f05 --- /dev/null +++ b/crates/cli/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "cdrustanoia_cli" +version.workspace = true +edition.workspace = true +license = "GPL-2.0-or-later" +publish = false + +[[bin]] +name = "cdrustanoia" +path = "src/main.rs" + +[[bin]] +name = "cdparanoia" +path = "src/bin_cdparanoia.rs" + +[lib] +name = "cdrustanoia_cli" +path = "src/lib.rs" + +[dependencies] +cdrustanoia_core.workspace = true +libc.workspace = true + +[dev-dependencies] +tempfile = "3" diff --git a/crates/cli/src/analyze.rs b/crates/cli/src/analyze.rs new file mode 100644 index 0000000..193d376 --- /dev/null +++ b/crates/cli/src/analyze.rs @@ -0,0 +1,295 @@ +//! Drive cache/timing analysis (`-A`), based on cachetest.c. +//! +//! This is a compact but functional port: it measures seek/read timing, +//! estimates the random-access cache size, checks cache transfer speed, and +//! verifies that a backward seek flushes the cache. Output wording follows +//! the original where it matters to users. + +use crate::log::Log; +use crate::{log_c, print_c, report_c}; +use cdrustanoia_core::device::{Drive, CD_FRAMESIZE_RAW}; +use std::thread::sleep; +use std::time::Duration; + +const MIN_SEEK_MS: i32 = 6; + +fn read_timed(d: &mut dyn Drive, lba: i64, len: i64) -> (i64, i32) { + let t = d.read_timed(None, lba, len); + match t.result { + Ok(n) => (n as i64, t.milliseconds), + Err(e) => { + if e.errno == libc::ENOMEDIUM { + (-404, -1) + } else { + (-1, -1) + } + } + } +} + +fn time_drive(d: &mut dyn Drive, log: &mut Log, lba: i64, len: i64, initial_seek: bool) -> i64 { + log_c!(log, "\n"); + let mut sofar = 0i64; + let mut sum = 0f64; + let mut sumsq = 0f64; + let mut latency = 0i32; + let mut i = 0; + while sofar < len { + let toread = if i == 0 && initial_seek { + 1 + } else { + len - sofar + }; + let (ret, x) = read_timed(d, lba + sofar, toread); + if ret <= 0 { + return if ret == -404 { -404 } else { -1 }; + } + let x = x.clamp(0, 9999); + log_c!(log, "{}:{}:{} ", lba + sofar, ret, x); + sofar += ret; + if i != 0 || !initial_seek { + sum += x as f64; + sumsq += (x * x) as f64 / ret as f64; + } else { + latency = x; + } + i += 1; + } + let denom = (len - 1).max(1) as f64; + let mean = sum / denom; + let stddev = ((sumsq / denom - mean * mean).max(0.0)).sqrt(); + if initial_seek { + print_c!( + log, + "{:4}ms seek, {:.2}ms/sec read [{:.1}x]", + latency, + mean, + 1000.0 / 75.0 / mean.max(0.001) + ); + log_c!( + log, + "\n\tInitial seek latency ({} sectors): {}ms", + len, + latency + ); + } + log_c!( + log, + "\n\tAverage read latency: {:.2}ms/sector (raw speed: {:.1}x)", + mean, + 1000.0 / 75.0 / mean.max(0.001) + ); + log_c!( + log, + "\n\tRead latency standard deviation: {:.2}ms/sector", + stddev + ); + sum as i64 +} + +fn retime_drive(d: &mut dyn Drive, log: &mut Log, lba: i64, readahead: i64, oldmean: f64) -> f64 { + let mut sectors = 2000i64; + if sectors as f64 * oldmean > 5000.0 { + sectors = (5000.0 / oldmean.max(0.001)) as i64; + } + let readahead = readahead * 10 / 9; + if readahead > sectors { + sectors = readahead; + } + let total = time_drive(d, log, lba, sectors, true); + if total <= 0 { + return oldmean; + } + let newmean = total as f64 / sectors as f64; + log_c!( + log, + "\n\tOld mean={:.2}ms/sec, New mean={:.2}ms/sec\n", + oldmean, + newmean + ); + newmean.max(oldmean) +} + +#[allow(unused_assignments)] +pub fn analyze_cache(d: &mut dyn Drive, log: &mut Log, speed: i32) -> i32 { + let speed = if speed <= 0 { -1 } else { speed }; + report_c!( + log, + "\n=================== Checking drive cache/timing behavior ===================\n" + ); + + // Longest contiguous audio stretch. + let mut firstsector = -1i64; + let mut lastsector = -1i64; + let mut firsttest = -1i64; + let mut lasttest = -1i64; + for i in 1..=d.tracks() { + if d.track_audiop(i) { + if firsttest == -1 { + firsttest = d.track_firstsector(i); + } + lasttest = d.track_lastsector(i); + if lasttest - firsttest > lastsector - firstsector { + firstsector = firsttest; + lastsector = lasttest; + } + } else { + firsttest = -1; + } + } + if firstsector == -1 { + report_c!( + log, + "\n\tNo audio on disc; Cannot determine timing behavior..." + ); + return -1; + } + + let avail = lastsector - firstsector + 1; + let current = (1000i64).min((avail / 2).max(1)); + let mut offset = firstsector + 10; + if offset + current > lastsector { + offset = firstsector; + } + + report_c!(log, "\nSeek/read timing:\n"); + let total = time_drive(d, log, offset, current, true); + if total == -404 { + return -1; + } + if total > 0 { + let mspersector = total as f64 / current as f64; + report_c!( + log, + "\n\tAverage access speed: {:.2}ms/sector [{:.1}x]", + mspersector, + 1000.0 / 75.0 / mspersector.max(0.001) + ); + } + + report_c!(log, "\n\nAnalyzing cache behavior...\n"); + + // Find the end of the drive's random-access cache by preloading a + // linear run and timing a read of its first sector. + let mut cache_size = 0i64; + let mut c = 1i64; + while c <= 1200 { + let (r1, _) = read_timed(d, offset + c, 1); + if r1 <= 0 { + offset += c + 100; + if offset + c > lastsector { + break; + } + continue; + } + let (r2, ms) = read_timed(d, offset, 1); + if r2 <= 0 { + offset += c + 100; + if offset + c > lastsector { + break; + } + continue; + } + if ms >= MIN_SEEK_MS { + break; + } + cache_size = c; + c += 64; + } + + if cache_size == 0 { + report_c!( + log, + "\tDrive does not cache nonlinear access \n" + ); + return 0; + } + report_c!( + log, + "\tApproximate random access cache size: {} sector(s) \n", + cache_size + ); + let mut warn = 0; + if cache_size > 1200 { + report_c!( + log, + "\nWARNING: This drive appears to be caching more sectors of\n\ + readahead than Paranoia can currently handle!\n" + ); + warn = 1; + } + + if speed == -1 { + log_c!(log, "\tAttempting to reset read speed to full... "); + } else { + log_c!(log, "\tAttempting to reset read speed to {}... ", speed); + } + if d.speed_set(speed) != 0 { + log_c!(log, "failed.\n"); + } else { + log_c!(log, "drive said OK\n"); + } + + // Cache transfer speed and backseek flushing. + let readsize = (cache_size - 8).max(1).min(cache_size); + if readsize >= 7 { + report_c!(log, "\tTesting cache transfer speed..."); + let (_, _) = read_timed(d, offset, readsize); + let mut elapsed = 0i64; + let mut sectors = 0i64; + let start = std::time::Instant::now(); + while start.elapsed().as_millis() < 1000 { + let (n, ms) = read_timed(d, offset, readsize); + if n <= 0 { + break; + } + elapsed += ms as i64; + sectors += n; + } + let cachems = if sectors > 0 { + elapsed as f64 / sectors as f64 + } else { + 0.0 + }; + report_c!( + log, + "\t Cache read speed: {:.2}ms/sector [{}x]\n", + cachems, + (1000.0 / 75.0 / cachems.max(0.001)) as i64 + ); + + report_c!(log, "\tTesting that backseek flushes cache..."); + let seekpos = (offset + cache_size + 20000).min((lastsector - 150).max(offset + 1)); + let (r, _) = read_timed(d, seekpos, 1); + let mut readms = 0.0; + if r > 0 { + let total = time_drive(d, log, offset + 1, readsize, true); + if total > 0 { + readms = total as f64 / readsize as f64; + } + } + report_c!( + log, + "\t Access speed after backseek: {:.2}ms/sector [{}x]\n", + readms, + (1000.0 / 75.0 / readms.max(0.001)) as i64 + ); + if readms > 0.0 && readms * 2.0 < cachems.max(0.001) { + report_c!( + log, + "\tWARNING: Read timing after backseek faster than expected!\n\ + \t It's possible/likely that this drive is not\n\ + \t flushing the readahead cache on backward seeks!\n\n" + ); + warn = 1; + } else if readms > 0.0 { + report_c!(log, "\tBackseek flushes the cache as expected\n"); + } + let _ = retime_drive(d, log, offset, readsize, cachems.max(1.0)); + } + + // A short pause mirrors the original's readahead settling delay. + sleep(Duration::from_millis(50)); + let _ = CD_FRAMESIZE_RAW; + warn +} diff --git a/crates/cli/src/args.rs b/crates/cli/src/args.rs new file mode 100644 index 0000000..0f5c170 --- /dev/null +++ b/crates/cli/src/args.rs @@ -0,0 +1,420 @@ +//! GNU getopt-compatible command line parsing for cdparanoia 10.2. + +use crate::output::OutputFormat; +use cdrustanoia_core::paranoia::*; + +#[derive(Debug, Clone)] +pub struct Args { + pub analyze: bool, + pub verbose: bool, + pub quiet: bool, + pub stderr_progress: bool, + pub log_summary: Option>, + pub log_debug: Option>, + pub version: bool, + pub query: bool, + pub batch: bool, + pub search: bool, + pub help: bool, + pub output_format: OutputFormat, + pub force_endian: Option, + pub force_sectors: Option, + pub force_overlap: Option, + pub force_device: Option, + pub force_cooked_device: Option, + pub force_generic_device: Option, + pub force_speed: Option, + pub toc_offset: i32, + pub toc_bias: bool, + pub sample_offset: i32, + pub max_retries: i32, + pub paranoia_mode: i32, + pub abort_on_skip: bool, + pub info_file: Option, + pub span: Option, + pub outfile: Option, +} + +impl Default for Args { + fn default() -> Self { + Self { + analyze: false, + verbose: false, + quiet: false, + stderr_progress: false, + log_summary: None, + log_debug: None, + version: false, + query: false, + batch: false, + search: false, + help: false, + output_format: OutputFormat::Wav, + force_endian: None, + force_sectors: None, + force_overlap: None, + force_device: None, + force_cooked_device: None, + force_generic_device: None, + force_speed: None, + toc_offset: 0, + toc_bias: false, + sample_offset: 0, + max_retries: 20, + paranoia_mode: PARANOIA_MODE_FULL ^ PARANOIA_MODE_NEVERSKIP, + abort_on_skip: false, + info_file: None, + span: None, + outfile: None, + } + } +} + +fn atoi(s: &str) -> i32 { + let s = s.trim_start(); + let mut chars = s.chars().peekable(); + let mut sign = 1i64; + if let Some(c) = chars.peek() { + if *c == '-' { + sign = -1; + chars.next(); + } else if *c == '+' { + chars.next(); + } + } + let mut value = 0i64; + let mut any = false; + for c in chars { + if let Some(d) = c.to_digit(10) { + any = true; + value = value.saturating_mul(10).saturating_add(d as i64); + } else { + break; + } + } + if !any { + return 0; + } + (sign * value) as i32 +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Short { + NoArg(char), + Required(char), + Optional(char), +} + +fn short_kind(c: char) -> Option { + match c { + 'e' | 's' | 'c' | 'C' | 'p' | 'r' | 'R' | 'w' | 'a' | 'f' | 'v' | 'q' | 'V' | 'Q' | 'h' + | 'Z' | 'Y' | 'X' | 'W' | 'B' | 'F' | 'T' | 'A' => Some(Short::NoArg(c)), + 'n' | 'o' | 'O' | 'd' | 'g' | 'k' | 'S' | 'i' | 't' => Some(Short::Required(c)), + 'z' | 'l' | 'L' => Some(Short::Optional(c)), + _ => None, + } +} + +fn long_kind(name: &str) -> Option<(&'static str, Short)> { + let table: &[(&str, Short)] = &[ + ("stderr-progress", Short::NoArg('e')), + ("search-for-drive", Short::NoArg('s')), + ("force-cdrom-little-endian", Short::NoArg('c')), + ("force-cdrom-big-endian", Short::NoArg('C')), + ("force-default-sectors", Short::Required('n')), + ("force-search-overlap", Short::Required('o')), + ("force-cdrom-device", Short::Required('d')), + ("force-cooked-device", Short::Required('k')), + ("force-generic-device", Short::Required('g')), + ("force-read-speed", Short::Required('S')), + ("sample-offset", Short::Required('O')), + ("toc-offset", Short::Required('t')), + ("toc-bias", Short::NoArg('T')), + ("output-raw", Short::NoArg('p')), + ("output-raw-little-endian", Short::NoArg('r')), + ("output-raw-big-endian", Short::NoArg('R')), + ("output-wav", Short::NoArg('w')), + ("output-aiff", Short::NoArg('f')), + ("output-aifc", Short::NoArg('a')), + ("batch", Short::NoArg('B')), + ("verbose", Short::NoArg('v')), + ("quiet", Short::NoArg('q')), + ("version", Short::NoArg('V')), + ("query", Short::NoArg('Q')), + ("help", Short::NoArg('h')), + ("analyze-drive", Short::NoArg('A')), + ("disable-paranoia", Short::NoArg('Z')), + ("disable-extra-paranoia", Short::NoArg('Y')), + ("abort-on-skip", Short::NoArg('X')), + ("disable-fragmentation", Short::NoArg('F')), + ("output-info", Short::Required('i')), + ("never-skip", Short::Optional('z')), + ("log-summary", Short::Optional('l')), + ("log-debug", Short::Optional('L')), + ]; + if let Some((_, kind)) = table.iter().find(|(n, _)| *n == name) { + return Some(("", *kind)); + } + let matches: Vec<&(&str, Short)> = table.iter().filter(|(n, _)| n.starts_with(name)).collect(); + if matches.len() == 1 { + Some(("", matches[0].1)) + } else { + None + } +} + +fn apply(args: &mut Args, opt: Short, value: Option) -> Result<(), String> { + let c = match opt { + Short::NoArg(c) | Short::Required(c) | Short::Optional(c) => c, + }; + match c { + 'A' => { + args.analyze = true; + args.query = true; + args.log_debug = Some(None); + args.verbose = true; + } + 'v' => { + args.verbose = true; + args.quiet = false; + } + 'q' => { + args.verbose = false; + args.quiet = true; + } + 'e' => { + args.stderr_progress = true; + eprintln!("Sending all callbacks to stderr for wrapper script"); + } + 'l' => { + args.log_summary = Some(value); + } + 'L' => { + args.log_debug = Some(value); + } + 'V' => args.version = true, + 'Q' => args.query = true, + 's' => args.search = true, + 'h' => args.help = true, + 'p' => args.output_format = OutputFormat::RawHost, + 'r' => args.output_format = OutputFormat::RawLittle, + 'R' => args.output_format = OutputFormat::RawBig, + 'w' => args.output_format = OutputFormat::Wav, + 'f' => args.output_format = OutputFormat::Aiff, + 'a' => args.output_format = OutputFormat::Aifc, + 'c' => args.force_endian = Some(0), + 'C' => args.force_endian = Some(1), + 'n' => args.force_sectors = Some(atoi(value.as_deref().unwrap_or(""))), + 'o' => args.force_overlap = Some(atoi(value.as_deref().unwrap_or(""))), + 'd' => args.force_device = value, + 'g' => { + if args.force_cooked_device.is_some() { + return Err("-g option incompatable with -k".to_string()); + } + args.force_cooked_device = None; + args.force_generic_device = value; + } + 'k' => { + if args.force_generic_device.is_some() || args.force_device.is_some() { + return Err("-k option incompatable with -d and -g".to_string()); + } + args.force_cooked_device = value; + } + 'S' => args.force_speed = Some(atoi(value.as_deref().unwrap_or(""))), + 't' => args.toc_offset = atoi(value.as_deref().unwrap_or("")), + 'T' => args.toc_bias = true, + 'O' => args.sample_offset = atoi(value.as_deref().unwrap_or("")), + 'z' => { + if let Some(v) = value { + args.max_retries = atoi(&v); + args.paranoia_mode &= !PARANOIA_MODE_NEVERSKIP; + } else { + args.paranoia_mode |= PARANOIA_MODE_NEVERSKIP; + } + } + 'Z' => args.paranoia_mode = PARANOIA_MODE_DISABLE, + 'Y' => { + args.paranoia_mode |= PARANOIA_MODE_OVERLAP; + args.paranoia_mode &= !PARANOIA_MODE_VERIFY; + } + 'X' => args.abort_on_skip = true, + 'W' => args.paranoia_mode &= !PARANOIA_MODE_REPAIR, + 'F' => args.paranoia_mode &= !PARANOIA_MODE_FRAGMENT, + 'B' => args.batch = true, + 'i' => args.info_file = value, + _ => return Err(format!("Unknown option -{}", c)), + } + Ok(()) +} + +pub fn parse(argv: &[String]) -> Result { + let mut args = Args::default(); + let mut positionals = Vec::new(); + let mut i = 1usize; + let mut stop = false; + while i < argv.len() { + let arg = &argv[i]; + if stop || arg == "-" || !arg.starts_with('-') { + positionals.push(arg.clone()); + i += 1; + continue; + } + if arg == "--" { + stop = true; + i += 1; + continue; + } + if let Some(long) = arg.strip_prefix("--") { + let (name, inline) = match long.split_once('=') { + Some((n, v)) => (n, Some(v.to_string())), + None => (long, None), + }; + let (_, kind) = + long_kind(name).ok_or_else(|| format!("unrecognized option '--{}'", name))?; + match kind { + Short::NoArg(c) => { + if inline.is_some() { + return Err(format!("option '--{}' doesn't allow an argument", name)); + } + apply(&mut args, Short::NoArg(c), None)?; + if args.help || args.version { + return Ok(args); + } + } + Short::Required(c) => { + let value = match inline { + Some(v) => Some(v), + None => { + i += 1; + if i >= argv.len() { + return Err(format!("option '--{}' requires an argument", name)); + } + Some(argv[i].clone()) + } + }; + apply(&mut args, Short::Required(c), value)?; + } + Short::Optional(c) => apply(&mut args, Short::Optional(c), inline)?, + } + i += 1; + continue; + } + + let chars: Vec = arg[1..].chars().collect(); + let mut j = 0usize; + while j < chars.len() { + let c = chars[j]; + let kind = short_kind(c).ok_or_else(|| format!("invalid option -- '{}'", c))?; + match kind { + Short::NoArg(c) => { + apply(&mut args, Short::NoArg(c), None)?; + if args.help || args.version { + return Ok(args); + } + j += 1; + } + Short::Required(c) => { + let rest: String = chars[j + 1..].iter().collect(); + let value = if !rest.is_empty() { + rest + } else { + i += 1; + if i >= argv.len() { + return Err(format!("option requires an argument -- '{}'", c)); + } + argv[i].clone() + }; + apply(&mut args, Short::Required(c), Some(value))?; + break; + } + Short::Optional(c) => { + let rest: String = chars[j + 1..].iter().collect(); + apply( + &mut args, + Short::Optional(c), + if rest.is_empty() { None } else { Some(rest) }, + )?; + break; + } + } + } + i += 1; + } + + if !positionals.is_empty() { + args.span = Some(positionals[0].clone()); + } + if positionals.len() > 1 { + args.outfile = Some(positionals[1].clone()); + } + Ok(args) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::output::OutputFormat; + + fn p(args: &[&str]) -> Result { + let mut v = vec!["cdparanoia".to_string()]; + v.extend(args.iter().map(|s| s.to_string())); + parse(&v) + } + + #[test] + fn clustered_flags() { + let a = p(&["-vQ"]).unwrap(); + assert!(a.verbose && !a.quiet && a.query); + let a = p(&["-qv"]).unwrap(); + assert!(a.verbose && !a.quiet); + let a = p(&["-vq"]).unwrap(); + assert!(a.quiet && !a.verbose); + } + + #[test] + fn optional_arguments_are_attached_only() { + let a = p(&["-lfoo.log"]).unwrap(); + assert_eq!(a.log_summary, Some(Some("foo.log".to_string()))); + let a = p(&["-l", "foo.log"]).unwrap(); + assert_eq!(a.log_summary, Some(None)); + assert_eq!(a.span.as_deref(), Some("foo.log")); + let a = p(&["--log-summary=bar.log"]).unwrap(); + assert_eq!(a.log_summary, Some(Some("bar.log".to_string()))); + let a = p(&["--log-summary", "bar.log"]).unwrap(); + assert_eq!(a.log_summary, Some(None)); + } + + #[test] + fn never_skip_modes() { + let a = p(&["-z", "1"]).unwrap(); + assert_ne!(a.paranoia_mode & PARANOIA_MODE_NEVERSKIP, 0); + assert_eq!(a.span.as_deref(), Some("1")); + let a = p(&["-z5", "1"]).unwrap(); + assert_eq!(a.paranoia_mode & PARANOIA_MODE_NEVERSKIP, 0); + assert_eq!(a.max_retries, 5); + } + + #[test] + fn end_of_options() { + let a = p(&["--", "-3", "out.raw"]).unwrap(); + assert_eq!(a.span.as_deref(), Some("-3")); + assert_eq!(a.outfile.as_deref(), Some("out.raw")); + } + + #[test] + fn output_formats_last_wins() { + assert_eq!( + p(&["-w", "-r", "1"]).unwrap().output_format, + OutputFormat::RawLittle + ); + assert_eq!( + p(&["-p", "-a", "1"]).unwrap().output_format, + OutputFormat::Aifc + ); + } + + #[test] + fn incompatible_devices() { + assert!(p(&["-g", "/dev/sg0", "-k", "/dev/sr0", "1"]).is_err()); + } +} diff --git a/crates/cli/src/bin_cdparanoia.rs b/crates/cli/src/bin_cdparanoia.rs new file mode 100644 index 0000000..bcf74a5 --- /dev/null +++ b/crates/cli/src/bin_cdparanoia.rs @@ -0,0 +1,4 @@ +fn main() { + let argv: Vec = std::env::args().collect(); + std::process::exit(cdrustanoia_cli::run(&argv)); +} diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs new file mode 100644 index 0000000..885c2f0 --- /dev/null +++ b/crates/cli/src/lib.rs @@ -0,0 +1,806 @@ +//! cdrustanoia command line driver, ported from cdparanoia 10.2 main.c. + +mod analyze; +pub mod args; +mod log; +mod output; +mod progress; +mod span; + +use crate::log::Log; +use crate::output::{write_header, BufferedWriter, OutputFormat}; +use crate::progress::Progress; +use crate::span::parse_span; +use args::Args; +use cdrustanoia_core::device::discover::{find_a_cdrom, identify, identify_cooked, identify_sgio}; +use cdrustanoia_core::device::{host_big_endian, swap16, Drive}; +use cdrustanoia_core::paranoia::*; +use cdrustanoia_core::{VERSION, VERSIONNUM}; +use std::fs::{File, OpenOptions}; +use std::io::{self, Write}; +use std::os::unix::fs::OpenOptionsExt; +use std::os::unix::io::FromRawFd; +use std::path::Path; + +const MANUAL: &str = r#"cdparanoia III release 10.2 (September 11, 2008) + +(C) 2008 Monty and Xiph.Org + +Report bugs to paranoia@xiph.org +http://www.xiph.org/paranoia/ +USAGE: + cdparanoia [options] [outfile] + +OPTIONS: + -A --analyze-drive : run and log a complete analysis of drive + caching, timing and reading behavior; + verifies that cdparanoia is correctly + modelling a sprcific drive's cache and + read behavior. Implies -vQL + -v --verbose : extra verbose operation + -q --quiet : quiet operation + -e --stderr-progress : force output of progress information to + stderr (for wrapper scripts) + -l --log-summary [] : save result summary to file, default + filename cdparanoia.log + -L --log-debug [] : save detailed device autosense and + debugging output to file, default + filename cdparanoia.log + -V --version : print version info and quit + -Q --query : autosense drive, query disc and quit + -B --batch : 'batch' mode (saves each track to a + seperate file. + -s --search-for-drive : do an exhaustive search for drive + -h --help : print help + + -p --output-raw : output raw 16 bit PCM in host byte + order + -r --output-raw-little-endian : output raw 16 bit little-endian PCM + -R --output-raw-big-endian : output raw 16 bit big-endian PCM + -w --output-wav : output as WAV file (default) + -f --output-aiff : output as AIFF file + -a --output-aifc : output as AIFF-C file + + -c --force-cdrom-little-endian : force treating drive as little endian + -C --force-cdrom-big-endian : force treating drive as big endian + -n --force-default-sectors : force default number of sectors in read + to n sectors + -o --force-search-overlap : force minimum overlap search during + verification to n sectors + -d --force-cdrom-device : use specified device; disallow + autosense + -k --force-cooked-device : use specified cdrom device and force + use of the old 'cooked ioctl' kernel + interface. -k cannot be used with -d + or -g. + -g --force-generic-device : use specified generic scsi device and + force use of the old SG kernel + interface. -g cannot be used with -k. + -S --force-read-speed : read from device at specified speed; by + default, cdparanoia sets drive to full + speed. + -t --toc-offset : Add sectors to the values reported + when addressing tracks. May be negative + -T --toc-bias : Assume that the beginning offset of + track 1 as reported in the TOC will be + addressed as LBA 0. Necessary for some + Toshiba drives to get track boundaries + correct + -O --sample-offset : Add samples to the offset when + reading data. May be negative. + -z --never-skip[=n] : never accept any less than perfect + data reconstruction (don't allow 'V's) + but if [n] is given, skip after [n] + retries without progress. + -Z --disable-paranoia : disable all paranoia checking + -Y --disable-extra-paranoia : only do cdda2wav-style overlap checking + -X --abort-on-skip : abort on imperfect reads/skips + +OUTPUT SMILIES: + :-) Normal operation, low/no jitter + :-| Normal operation, considerable jitter + :-/ Read drift + :-P Unreported loss of streaming in atomic read operation + 8-| Finding read problems at same point during reread; hard to correct + :-0 SCSI/ATAPI transport error + :-( Scratch detected + ;-( Gave up trying to perform a correction + 8-X Aborted (as per -X) due to a scratch/skip + :^D Finished extracting + +PROGRESS BAR SYMBOLS: + No corrections needed + - Jitter correction required + + Unreported loss of streaming/other error in read + ! Errors are getting through stage 1 but corrected in stage2 + e SCSI/ATAPI transport error (corrected) + V Uncorrected error/skip + +SPAN ARGUMENT: +The span argument may be a simple track number or a offset/span +specification. The syntax of an offset/span takes the rough form: + + 1[ww:xx:yy.zz]-2[aa:bb:cc.dd] + +Here, 1 and 2 are track numbers; the numbers in brackets provide a +finer grained offset within a particular track. [aa:bb:cc.dd] is in +hours/minutes/seconds/sectors format. Zero fields need not be +specified: [::20], [:20], [20], [20.], etc, would be interpreted as +twenty seconds, [10:] would be ten minutes, [.30] would be thirty +sectors (75 sectors per second). + +When only a single offset is supplied, it is interpreted as a starting +offset and ripping will continue to the end of he track. If a single +offset is preceeded or followed by a hyphen, the implicit missing +offset is taken to be the start or end of the disc, respectively. Thus: + + 1:[20.35] Specifies ripping from track 1, second 20, sector 35 to + the end of track 1. + + 1:[20.35]- Specifies ripping from 1[20.35] to the end of the disc + + -2 Specifies ripping from the beginning of the disc up to + (and including) track 2 + + -2:[30.35] Specifies ripping from the beginning of the disc up to + 2:[30.35] + + 2-4 Specifies ripping from the beginning of track two to the + end of track 4. + +Don't forget to protect square brackets and preceeding hyphens from +the shell... + +A few examples, protected from the shell: + A) query only with exhaustive search for a drive and full reporting + of autosense: + cdparanoia -vsQ + + B) extract up to and including track 3, putting each track in a seperate + file: + cdparanoia -B -- "-3" + + C) extract from track 1, time 0:30.12 to 1:10.00: + cdparanoia "1[:30.12]-1[1:10]" + +Submit bug reports to paranoia@xiph.org + +"#; + +fn usage(out: &mut dyn Write) { + let _ = out.write_all(MANUAL.as_bytes()); +} + +fn open_log_name(name: &str) -> Result, String> { + if name == "-" { + Ok(Box::new(io::stdout())) + } else { + OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .mode(0o666) + .open(name) + .map(|f| Box::new(f) as Box) + .map_err(|e| format!("Cannot open log file {}: {}", name, e)) + } +} + +fn setup_logs(args: &Args, argv: &[String]) -> Result { + let mut log = Log::new(); + log.quiet = args.quiet; + + let summary_name = args + .log_summary + .as_ref() + .map(|v| v.clone().unwrap_or_else(|| "cdparanoia.log".to_string())); + let report_name = args + .log_debug + .as_ref() + .map(|v| v.clone().unwrap_or_else(|| "cdparanoia.log".to_string())); + + match (&summary_name, &report_name) { + (Some(s), Some(r)) if s == r && s != "-" => { + let file = OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .mode(0o666) + .open(s) + .map_err(|e| format!("Cannot open log file {}: {}", s, e))?; + let clone = file + .try_clone() + .map_err(|e| format!("Cannot duplicate log file {}: {}", s, e))?; + log.set_summary_writer(Box::new(file)); + log.set_report_writer(Box::new(clone)); + } + _ => { + if let Some(name) = &summary_name { + log.set_summary_writer(open_log_name(name)?); + } + if let Some(name) = &report_name { + log.set_report_writer(open_log_name(name)?); + } + } + } + + if log.summary_file.is_some() { + let line = argv.join(" "); + log.summary(format_args!("{}", line)); + if summary_name.as_deref() != report_name.as_deref() { + log.summary(format_args!("{}\n", VERSION)); + log.summary(format_args!("Using cdda library version: {}", VERSIONNUM)); + log.summary(format_args!( + "Using paranoia library version: {}", + VERSIONNUM + )); + } + } + if log.report_file.is_some() && summary_name.as_deref() != report_name.as_deref() { + let line = argv.join(" "); + log.report(format_args!("{}", line)); + } + log.flush(); + Ok(log) +} + +fn display_toc(d: &dyn Drive, log: &mut Log) { + let mut s = String::new(); + s.push_str("\nTable of contents (audio tracks only):\n"); + s.push_str("track length begin copy pre ch\n"); + s.push_str("==========================================================="); + let mut audiolen = 0i64; + for i in 1..=d.tracks() { + if d.track_audiop(i) { + let sec = d.track_firstsector(i); + let off = d.track_lastsector(i) - sec + 1; + s.push_str(&format!( + "\n{:3}. {:7} [{:02}:{:02}.{:02}] {:7} [{:02}:{:02}.{:02}] {} {} {}", + i, + off, + off / (60 * 75), + (off / 75) % 60, + off % 75, + sec, + sec / (60 * 75), + (sec / 75) % 60, + sec % 75, + if d.track_copyp(i) { " OK" } else { " no" }, + if d.track_preemp(i) { " yes" } else { " no" }, + if d.track_channels(i) == 2 { " 2" } else { " 4" } + )); + audiolen += off; + } + } + s.push_str(&format!( + "\nTOTAL {:7} [{:02}:{:02}.{:02}] (audio only)", + audiolen, + audiolen / (60 * 75), + (audiolen / 75) % 60, + audiolen % 75 + )); + log.report(format_args!("{}", s)); + log.report(format_args!(" ")); +} + +fn read_sector( + p: &mut Paranoia, + progress: &mut Progress, + log: &mut Log, + max_retries: i32, +) -> Option> { + let mut callback = |pos: i64, func: i32| progress.callback(pos, func, log); + p.read_limited(&mut callback, max_retries) + .map(|s| s.to_vec()) +} + +fn samples_as_bytes(samples: &[i16]) -> &[u8] { + unsafe { + std::slice::from_raw_parts( + samples.as_ptr() as *const u8, + std::mem::size_of_val(samples), + ) + } +} + +fn make_output_name( + template: &Option, + batch: bool, + track: i32, + format: OutputFormat, +) -> Option { + match template { + None => { + if batch { + Some(format!("track{:02}.{}", track, format.default_extension())) + } else { + Some(format.default_extension().to_string()) + } + } + Some(name) if name == "-" => None, + Some(name) => { + let (prefix, file) = match name.rfind('/') { + Some(pos) => (&name[..pos + 1], &name[pos + 1..]), + None => ("", name.as_str()), + }; + let mut out = if batch { + format!("{}track{:02}.{}", prefix, track, file) + } else { + format!("{}{}", prefix, file) + }; + if file.is_empty() { + out.push_str(format.default_extension()); + } + Some(out) + } + } +} + +fn open_output(name: Option<&str>, template_stdout: bool) -> Result { + if template_stdout { + let fd = unsafe { libc::dup(libc::STDOUT_FILENO) }; + if fd < 0 { + return Err("Cannot duplicate stdout".to_string()); + } + return Ok(unsafe { File::from_raw_fd(fd) }); + } + let name = name.ok_or_else(|| "Invalid output file".to_string())?; + OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(true) + .mode(0o666) + .open(name) + .map_err(|e| format!("Cannot open specified output file {}: {}", name, e)) +} + +pub fn run(argv: &[String]) -> i32 { + let args = match args::parse(argv) { + Ok(a) => a, + Err(e) => { + eprintln!("{}", e); + usage(&mut io::stderr()); + return 1; + } + }; + + if args.help { + usage(&mut io::stdout()); + return 0; + } + if args.version { + eprintln!("{}\n", VERSION); + return 0; + } + + let mut log = match setup_logs(&args, argv) { + Ok(l) => l, + Err(e) => { + eprintln!("{}", e); + return 1; + } + }; + + let span_arg = args.span.clone(); + if span_arg.is_none() && !args.query && !args.batch { + usage(&mut io::stderr()); + let _ = io::stderr().flush(); + return 1; + } + + log.report(format_args!("{}\n", VERSION)); + if args.verbose { + log.report(format_args!("Using cdda library version: {}", VERSIONNUM)); + log.report(format_args!( + "Using paranoia library version: {}", + VERSIONNUM + )); + } + + let drive_result: Result, String> = if let Some(dev) = &args.force_cooked_device + { + identify_cooked(dev, args.verbose) + } else if let Some(dev) = &args.force_generic_device { + match identify_sgio(dev, args.verbose) { + Ok(d) => Ok(d), + Err(e) => { + if let Some(spec) = &args.force_device { + identify(spec, args.verbose, false).map_err(|_| e) + } else { + Err(e) + } + } + } + } else if let Some(dev) = &args.force_device { + identify(dev, args.verbose, false) + } else if args.search { + find_a_cdrom(args.verbose).ok_or_else(|| "No cdrom drives found".to_string()) + } else if Path::new("/dev/cdrom").symlink_metadata().is_err() { + find_a_cdrom(args.verbose).ok_or_else(|| "No cdrom drives found".to_string()) + } else { + match identify("/dev/cdrom", args.verbose, false) { + Ok(d) => Ok(d), + Err(_) => { + if !args.verbose { + log.report_c(format_args!( + "\n/dev/cdrom exists but isn't accessible. By default,\n\ +cdparanoia stops searching for an accessible drive here.\n\ +Consider using -sv to force a more complete autosense\n\ +of the machine.\n\nMore information about /dev/cdrom:" + )); + let _ = identify("/dev/cdrom", true, false); + log.report(format_args!("\n")); + return 1; + } + Err("Unable to open cdrom drive".to_string()) + } + } + }; + + let mut drive = match drive_result { + Ok(d) => d, + Err(_) => { + if !args.verbose { + log.report(format_args!( + "\nUnable to open cdrom drive; -v will give more information." + )); + } + return 1; + } + }; + + if let Err(e) = drive.open_disc() { + if args.verbose { + log.report(format_args!("\nDisc open failed: {}", e)); + } + log.report(format_args!( + "\nUnable to open disc. Is there an audio CD in the drive?" + )); + return 1; + } + if !drive.opened() || drive.tracks() < 1 { + log.report(format_args!( + "\nUnable to open disc. Is there an audio CD in the drive?" + )); + return 1; + } + + if let Some(endian) = args.force_endian { + drive.set_bigendianp(endian); + log.report(format_args!( + "Forcing CDROM sense to {}-endian; ignoring preset and autosense", + if endian == 0 { "little" } else { "big" } + )); + } + if let Some(sectors) = args.force_sectors { + if !(1..=100).contains(§ors) { + log.report(format_args!( + "Default sector read size must be 1<= n <= 100" + )); + return 1; + } + log.report(format_args!( + "Forcing default to read {} sectors; ignoring preset and autosense", + sectors + )); + drive.set_nsectors(sectors as i64); + } + if let Some(overlap) = args.force_overlap { + if !(0..=75).contains(&overlap) { + log.report(format_args!("Search overlap sectors must be 0<= n <=75")); + return 1; + } + log.report(format_args!( + "Forcing search overlap to {} sectors; ignoring autosense", + overlap + )); + } + + let speed = match args.force_speed { + Some(0) | None => -1, + Some(v) => v, + }; + if speed != -1 { + log.report(format_args!("\nAttempting to set speed to {}x... ", speed)); + } else if args.verbose { + log.report(format_args!("\nAttempting to set cdrom to full speed... ")); + } + if drive.speed_set(speed) != 0 { + if args.verbose || speed != -1 { + log.report(format_args!( + "\tCDROM speed set FAILED. Continuing anyway..." + )); + } + } else if args.verbose { + log.report(format_args!("\tdrive returned OK.")); + } + + if args.analyze { + let warn = analyze::analyze_cache(drive.as_mut(), &mut log, speed); + if warn == 0 { + log.report_c(format_args!("\nDrive tests OK with Paranoia.\n\n")); + return 0; + } + log.report_c(format_args!( + "\nWARNING! PARANOIA MAY NOT BE TRUSTWORTHY WITH THIS DRIVE!\n\ +\nThe Paranoia library may not model this CDROM drive's cache\ +\ncorrectly according to this analysis run. Analysis is not\ +\nalways accurate (it can be fooled by machine load or random\ +\nkernel latencies), but if a failed result happens more often\ +\nthan one time in twenty on an unloaded machine, please mail\ +\nthe cdparanoia.log file produced by this failed analysis to\ +\nparanoia-dev@xiph.org to assist developers in extending\ +\nParanoia to handle this CDROM properly.\n\n" + )); + return 1; + } + + if args.query || args.verbose { + display_toc(drive.as_ref(), &mut log); + } + if args.query { + return 0; + } + + let mut toc_offset = args.toc_offset as i64; + let mut sample_offset = args.sample_offset; + if sample_offset != 0 { + toc_offset += (sample_offset / 588) as i64; + sample_offset %= 588; + if sample_offset < 0 { + sample_offset += 588; + toc_offset -= 1; + } + } + if args.toc_bias { + toc_offset = -drive.track_firstsector(1); + } + { + let tracks = drive.tracks(); + let toc = drive.toc_mut(); + for i in 0..=tracks as usize { + toc.entries[i].start_sector += toc_offset; + } + } + + if drive.nsectors() == 1 { + log.report(format_args!( + "WARNING: The autosensed/selected sectors per read value is\n\ + one sector, making it very unlikely Paranoia can \n\ + work.\n\n\ + Attempting to continue...\n" + )); + } + + let (first_sector, last_sector) = match &span_arg { + Some(span) => match parse_span(drive.toc(), span) { + Ok(v) => v, + Err(e) => { + log.report(format_args!("{}", e)); + return 1; + } + }, + None => (drive.disc_firstsector(), drive.disc_lastsector()), + }; + + let track1 = drive.sector_gettrack(first_sector); + let track2 = drive.sector_gettrack(last_sector); + if track1 < 0 || track2 < 0 { + log.report(format_args!("Error parsing span argument")); + return 1; + } + for i in track1..=track2 { + if !drive.track_audiop(i) { + log.report(format_args!( + "Selected span contains non audio tracks. Aborting.\n" + )); + return 1; + } + } + let off1 = first_sector - drive.track_firstsector(track1); + let off2 = last_sector - drive.track_firstsector(track2); + log.report(format_args!( + "Ripping from sector {:7} (track {:2} [{}:{:02}.{:02}])\n\t to sector {:7} (track {:2} [{}:{:02}.{:02}])", + first_sector, + track1, + off1 / (60 * 75), + (off1 / 75) % 60, + off1 % 75, + last_sector, + track2, + off2 / (60 * 75), + (off2 / 75) % 60, + off2 % 75 + )); + + let mut p = Paranoia::new(drive); + p.modeset(args.paranoia_mode); + if let Some(overlap) = args.force_overlap { + p.overlapset(overlap as i64); + } + p.seek(first_sector, 0); + + unsafe { + libc::seteuid(libc::getuid()); + libc::setegid(libc::getgid()); + } + + if sample_offset != 0 { + let tracks = p.drive().tracks() as usize; + if let Some(e) = p.drive_mut().toc_mut().entries.get_mut(tracks) { + e.start_sector += 1; + } + } + + let host_big = host_big_endian(); + let target_big = args.output_format.output_big_endian(host_big); + let mut offset_buffer = [0u8; 2352]; + let mut offset_buffer_used = 0usize; + let mut offset_skip = (sample_offset * 4) as usize; + let mut cursor = first_sector; + let mut progress = Progress::default(); + progress.callscript = args.stderr_progress; + progress.abort_on_skip = args.abort_on_skip; + + while cursor <= last_sector { + let (batch_first, batch_last, batch_track) = if args.batch { + let bt = p.drive().sector_gettrack(cursor); + if bt < 0 { + break; + } + let mut bl = p.drive().track_lastsector(bt); + if bl > last_sector { + bl = last_sector; + } + (cursor, bl, bt) + } else { + (first_sector, last_sector, -1) + }; + + progress.callbegin = batch_first; + progress.callend = batch_last; + progress.reset_batch(); + + let template_stdout = args.outfile.as_deref() == Some("-"); + let outfile_name = + make_output_name(&args.outfile, args.batch, batch_track, args.output_format); + if template_stdout { + if args.batch { + log.report(format_args!( + "Are you sure you wanted 'batch' (-B) output with stdout?" + )); + } + log.report(format_args!("outputting to stdout")); + } else { + log.report(format_args!( + "outputting to {}", + outfile_name.as_deref().unwrap_or("") + )); + } + if let Some(name) = &outfile_name { + log.summary(format_args!("outputting to {}", name)); + } else { + log.summary(format_args!("outputting to stdout")); + } + + let file = match open_output(outfile_name.as_deref(), template_stdout) { + Ok(f) => f, + Err(e) => { + log.report(format_args!("{}", e)); + return 1; + } + }; + let mut out = BufferedWriter::new(file); + let bytes = ((batch_last - batch_first + 1) * 2352) as u32; + if let Err(e) = write_header(&mut out, args.output_format, bytes) { + log.report(format_args!("Error writing output: {}", e)); + return 1; + } + + if offset_buffer_used > 0 { + cursor += 1; + if let Err(e) = out.write_all(&offset_buffer[offset_buffer_used..]) { + log.report(format_args!("Error writing output: {}", e)); + return 1; + } + } + + progress.skipped_flag = false; + while cursor <= batch_last { + let sector = match read_sector(&mut p, &mut progress, &mut log, args.max_retries) { + Some(s) => s, + None => { + progress.skipped_flag = true; + log.report(format_args!( + "\nparanoia_read: Unrecoverable error, bailing." + )); + break; + } + }; + if progress.skipped_flag && args.abort_on_skip { + cursor = batch_last + 1; + break; + } + progress.skipped_flag = false; + cursor += 1; + + let mut data = sector; + if target_big != host_big { + for s in &mut data { + *s = swap16(*s); + } + } + progress.callback(cursor * 1176 - 1, -2, &mut log); + + let data_bytes = samples_as_bytes(&data); + if let Err(e) = out.write_all(&data_bytes[offset_skip.min(data_bytes.len())..]) { + log.report(format_args!("Error writing output: {}", e)); + return 1; + } + offset_skip = 0; + + if sample_offset != 0 && cursor > batch_last { + let extra = match read_sector(&mut p, &mut progress, &mut log, args.max_retries) { + Some(s) => s, + None => { + progress.skipped_flag = true; + log.report(format_args!( + "\nparanoia_read: Unrecoverable error reading through \n\ +\t\t\tsample_offset shift\n\tat end of track, bailing." + )); + break; + } + }; + if progress.skipped_flag && args.abort_on_skip { + break; + } + progress.skipped_flag = false; + let mut extra = extra; + if target_big != host_big { + for s in &mut extra { + *s = swap16(*s); + } + } + let eb = samples_as_bytes(&extra); + offset_buffer[..eb.len().min(2352)].copy_from_slice(&eb[..eb.len().min(2352)]); + offset_buffer_used = (sample_offset as usize * 4).min(2352); + progress.callback(cursor * 1176, -2, &mut log); + if let Err(e) = out.write_all(&offset_buffer[..offset_buffer_used]) { + log.report(format_args!("Error writing output: {}", e)); + return 1; + } + } + } + + progress.callback(cursor * 1176 - 1, -1, &mut log); + if let Err(e) = out.close() { + log.report(format_args!("Error writing output: {}", e)); + return 1; + } + + if progress.skipped_flag { + if let Some(name) = &outfile_name { + log.report(format_args!("\nRemoving aborted file: {}", name)); + let _ = std::fs::remove_file(name); + } + if batch_track != -1 { + let next_track = batch_track + 1; + let next = p.drive().track_firstsector(next_track); + if next >= 0 { + cursor = next; + p.seek(cursor, 0); + } else { + cursor = last_sector + 1; + } + offset_skip = (sample_offset * 4) as usize; + offset_buffer_used = 0; + } + } + log.report(format_args!("\n")); + } + + log.report(format_args!("Done.\n\n")); + log.flush(); + 0 +} diff --git a/crates/cli/src/log.rs b/crates/cli/src/log.rs new file mode 100644 index 0000000..f04cf06 --- /dev/null +++ b/crates/cli/src/log.rs @@ -0,0 +1,121 @@ +//! Report/log destination handling, matching report.h's macros plus the +//! separate summary and debug streams from main.c. + +use std::io::{self, Write}; + +pub struct Log { + pub quiet: bool, + pub summary_file: Option>, + pub report_file: Option>, +} + +impl Default for Log { + fn default() -> Self { + Self::new() + } +} + +impl Log { + pub fn new() -> Self { + Self { + quiet: false, + summary_file: None, + report_file: None, + } + } + + pub fn set_summary_writer(&mut self, writer: Box) { + self.summary_file = Some(writer); + } + + pub fn set_report_writer(&mut self, writer: Box) { + self.report_file = Some(writer); + } + + pub fn report(&mut self, args: std::fmt::Arguments<'_>) { + if !self.quiet { + let _ = writeln!(io::stderr(), "{}", args); + } + if let Some(f) = self.report_file.as_mut() { + let _ = writeln!(f, "{}", args); + } + } + + pub fn report_c(&mut self, args: std::fmt::Arguments<'_>) { + if !self.quiet { + let _ = write!(io::stderr(), "{}", args); + } + if let Some(f) = self.report_file.as_mut() { + let _ = write!(f, "{}", args); + } + } + + pub fn print_c(&mut self, args: std::fmt::Arguments<'_>) { + if !self.quiet { + let _ = write!(io::stderr(), "{}", args); + } + } + + pub fn log_c(&mut self, args: std::fmt::Arguments<'_>) { + if let Some(f) = self.report_file.as_mut() { + let _ = write!(f, "{}", args); + } + } + + pub fn summary(&mut self, args: std::fmt::Arguments<'_>) { + if let Some(f) = self.summary_file.as_mut() { + let _ = writeln!(f, "{}", args); + } + } + + pub fn write_summary_bytes(&mut self, data: &[u8]) { + if let Some(f) = self.summary_file.as_mut() { + let _ = f.write_all(data); + let _ = f.flush(); + } + } + + pub fn flush(&mut self) { + if let Some(f) = self.summary_file.as_mut() { + let _ = f.flush(); + } + if let Some(f) = self.report_file.as_mut() { + let _ = f.flush(); + } + } +} + +#[macro_export] +macro_rules! report { + ($log:expr, $($arg:tt)*) => { + $log.report(format_args!($($arg)*)) + }; +} + +#[macro_export] +macro_rules! report_c { + ($log:expr, $($arg:tt)*) => { + $log.report_c(format_args!($($arg)*)) + }; +} + +#[macro_export] +macro_rules! print_c { + ($log:expr, $($arg:tt)*) => { + $log.print_c(format_args!($($arg)*)) + }; +} + +#[macro_export] +macro_rules! log_c { + ($log:expr, $($arg:tt)*) => { + $log.log_c(format_args!($($arg)*)) + }; +} + +#[macro_export] +macro_rules! summary { + ($log:expr, $($arg:tt)*) => { + $log.summary(format_args!($($arg)*)) + }; +} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs new file mode 100644 index 0000000..bcf74a5 --- /dev/null +++ b/crates/cli/src/main.rs @@ -0,0 +1,4 @@ +fn main() { + let argv: Vec = std::env::args().collect(); + std::process::exit(cdrustanoia_cli::run(&argv)); +} diff --git a/crates/cli/src/output.rs b/crates/cli/src/output.rs new file mode 100644 index 0000000..31545e1 --- /dev/null +++ b/crates/cli/src/output.rs @@ -0,0 +1,226 @@ +//! WAV/AIFF/AIFC headers and buffered output, ported from header.c and +//! buffering_write.c. + +use std::fs::File; +use std::io::{self, Write}; + +pub const OUTBUF_SZ: usize = 32 * 1024; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum OutputFormat { + RawHost, + RawLittle, + RawBig, + Wav, + Aiff, + Aifc, +} + +impl OutputFormat { + pub fn default_extension(self) -> &'static str { + match self { + OutputFormat::RawHost | OutputFormat::RawLittle | OutputFormat::RawBig => "cdda.raw", + OutputFormat::Wav => "cdda.wav", + OutputFormat::Aifc => "cdda.aifc", + OutputFormat::Aiff => "cdda.aiff", + } + } + + pub fn track_extension(self) -> &'static str { + match self { + OutputFormat::RawHost | OutputFormat::RawLittle | OutputFormat::RawBig => "raw", + OutputFormat::Wav => "wav", + OutputFormat::Aifc => "aifc", + OutputFormat::Aiff => "aiff", + } + } + + pub fn output_big_endian(self, host_big: bool) -> bool { + match self { + OutputFormat::RawHost => host_big, + OutputFormat::RawLittle | OutputFormat::Wav => false, + OutputFormat::RawBig | OutputFormat::Aiff | OutputFormat::Aifc => true, + } + } +} + +fn put_num(w: &mut W, num: u32, big_endian: bool, bytes: usize) -> io::Result<()> { + let mut tmp = [0u8; 4]; + if big_endian { + let b = num.to_be_bytes(); + for (i, slot) in tmp.iter_mut().take(bytes).enumerate() { + *slot = b[4 - bytes + i]; + } + } else { + let b = num.to_le_bytes(); + tmp[..bytes].copy_from_slice(&b[..bytes]); + } + w.write_all(&tmp[..bytes]) +} + +pub fn write_wav(w: &mut W, bytes: u32) -> io::Result<()> { + w.write_all(b"RIFF")?; + put_num(w, bytes.wrapping_add(44).wrapping_sub(8), false, 4)?; + w.write_all(b"WAVEfmt ")?; + put_num(w, 16, false, 4)?; + put_num(w, 1, false, 2)?; + put_num(w, 2, false, 2)?; + put_num(w, 44100, false, 4)?; + put_num(w, 44100 * 2 * 2, false, 4)?; + put_num(w, 4, false, 2)?; + put_num(w, 16, false, 2)?; + w.write_all(b"data")?; + put_num(w, bytes, false, 4)?; + Ok(()) +} + +pub fn write_aiff(w: &mut W, bytes: u32) -> io::Result<()> { + let frames = bytes / 4; + w.write_all(b"FORM")?; + put_num(w, bytes.wrapping_add(54).wrapping_sub(8), true, 4)?; + w.write_all(b"AIFF")?; + w.write_all(b"COMM")?; + put_num(w, 18, true, 4)?; + put_num(w, 2, true, 2)?; + put_num(w, frames, true, 4)?; + put_num(w, 16, true, 2)?; + w.write_all(&[0x40, 0x0e, 0xac, 0x44, 0, 0, 0, 0, 0, 0])?; + w.write_all(b"SSND")?; + put_num(w, bytes.wrapping_add(8), true, 4)?; + put_num(w, 0, true, 4)?; + put_num(w, 0, true, 4)?; + Ok(()) +} + +pub fn write_aifc(w: &mut W, bytes: u32) -> io::Result<()> { + let frames = bytes / 4; + w.write_all(b"FORM")?; + put_num(w, bytes.wrapping_add(86).wrapping_sub(8), true, 4)?; + w.write_all(b"AIFC")?; + w.write_all(b"FVER")?; + put_num(w, 4, true, 4)?; + put_num(w, 2726318400, true, 4)?; + w.write_all(b"COMM")?; + put_num(w, 38, true, 4)?; + put_num(w, 2, true, 2)?; + put_num(w, frames, true, 4)?; + put_num(w, 16, true, 2)?; + w.write_all(&[0x40, 0x0e, 0xac, 0x44, 0, 0, 0, 0, 0, 0])?; + w.write_all(b"NONE")?; + put_num(w, 14, true, 1)?; + w.write_all(b"not compressed")?; + put_num(w, 0, true, 1)?; + w.write_all(b"SSND")?; + put_num(w, bytes.wrapping_add(8), true, 4)?; + put_num(w, 0, true, 4)?; + put_num(w, 0, true, 4)?; + Ok(()) +} + +pub fn write_header(w: &mut W, format: OutputFormat, bytes: u32) -> io::Result<()> { + match format { + OutputFormat::Wav => write_wav(w, bytes), + OutputFormat::Aiff => write_aiff(w, bytes), + OutputFormat::Aifc => write_aifc(w, bytes), + _ => Ok(()), + } +} + +/// Buffered writer matching the original's 32 KiB write coalescing. +pub struct BufferedWriter { + file: File, + buf: Vec, + closed: bool, +} + +impl BufferedWriter { + pub fn new(file: File) -> Self { + Self { + file, + buf: Vec::with_capacity(OUTBUF_SZ), + closed: false, + } + } + + pub fn write_all(&mut self, mut data: &[u8]) -> io::Result<()> { + while !data.is_empty() { + let space = OUTBUF_SZ.saturating_sub(self.buf.len()); + if space == 0 { + self.flush_buf()?; + continue; + } + let n = space.min(data.len()); + self.buf.extend_from_slice(&data[..n]); + data = &data[n..]; + } + Ok(()) + } + + fn flush_buf(&mut self) -> io::Result<()> { + if !self.buf.is_empty() { + self.file.write_all(&self.buf)?; + self.buf.clear(); + } + Ok(()) + } + + pub fn flush(&mut self) -> io::Result<()> { + self.flush_buf()?; + self.file.flush() + } + + pub fn close(mut self) -> io::Result<()> { + self.closed = true; + self.flush() + } +} + +impl Write for BufferedWriter { + fn write(&mut self, data: &[u8]) -> io::Result { + self.write_all(data)?; + Ok(data.len()) + } + fn flush(&mut self) -> io::Result<()> { + BufferedWriter::flush(self) + } +} + +impl Drop for BufferedWriter { + fn drop(&mut self) { + if !self.closed { + let _ = self.flush_buf(); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn wav_header_size() { + let mut v = Vec::new(); + write_wav(&mut v, 2352).unwrap(); + assert_eq!(v.len(), 44); + assert_eq!(&v[..4], b"RIFF"); + assert_eq!(&v[8..16], b"WAVEfmt "); + assert_eq!(&v[36..40], b"data"); + } + + #[test] + fn aiff_header_size() { + let mut v = Vec::new(); + write_aiff(&mut v, 2352).unwrap(); + assert_eq!(v.len(), 54); + assert_eq!(&v[..4], b"FORM"); + assert_eq!(&v[8..12], b"AIFF"); + } + + #[test] + fn aifc_header_size() { + let mut v = Vec::new(); + write_aifc(&mut v, 2352).unwrap(); + assert_eq!(v.len(), 86); + assert_eq!(&v[8..12], b"AIFC"); + } +} diff --git a/crates/cli/src/progress.rs b/crates/cli/src/progress.rs new file mode 100644 index 0000000..dae7556 --- /dev/null +++ b/crates/cli/src/progress.rs @@ -0,0 +1,351 @@ +//! Callback and progress display, ported from the callback in main.c. + +use crate::log::Log; +use cdrustanoia_core::callback::*; +use cdrustanoia_core::device::CD_FRAMEWORDS; +use std::io::{self, Write}; +use std::time::{SystemTime, UNIX_EPOCH}; + +pub const CALLBACK_STRINGS: [&str; 16] = [ + "wrote", + "finished", + "read", + "verify", + "jitter", + "correction", + "scratch", + "scratch repair", + "skip", + "drift", + "backoff", + "overlap", + "dropped", + "duped", + "transport error", + "cache error", +]; + +const GRAPH: usize = 30; + +pub struct Progress { + pub callbegin: i64, + pub callend: i64, + pub callscript: bool, + pub abort_on_skip: bool, + pub skipped_flag: bool, + c_sector: i64, + v_sector: i64, + dispcache: [u8; GRAPH], + last: i32, + lasttime: i64, + heartbeat: u8, + slevel: i32, + slast: i32, + stimeout: i32, + cacheerr: i32, + overlap: i32, + printit: i32, +} + +impl Default for Progress { + fn default() -> Self { + Self { + callbegin: 0, + callend: 0, + callscript: false, + abort_on_skip: false, + skipped_flag: false, + c_sector: 0, + v_sector: 0, + dispcache: [b' '; GRAPH], + last: 0, + lasttime: 0, + heartbeat: b' ', + slevel: 0, + slast: 0, + stimeout: 0, + cacheerr: 0, + overlap: 0, + printit: -1, + } + } +} + +impl Progress { + pub fn reset_batch(&mut self) { + self.c_sector = 0; + self.v_sector = 0; + self.dispcache = [b' '; GRAPH]; + self.last = 0; + self.lasttime = 0; + self.heartbeat = b' '; + self.slevel = 0; + self.slast = 0; + self.stimeout = 0; + self.overlap = 0; + self.skipped_flag = false; + } + + fn now_tenths() -> i64 { + match SystemTime::now().duration_since(UNIX_EPOCH) { + Ok(d) => d.as_secs() as i64 * 10 + (d.subsec_micros() / 100_000) as i64, + Err(_) => 0, + } + } + + pub fn callback(&mut self, inpos: i64, function: i32, log: &mut Log) { + if self.callscript { + let name = if (-2..=13).contains(&function) { + CALLBACK_STRINGS[(function + 2) as usize] + } else { + "" + }; + let _ = writeln!(io::stderr(), "##: {} [{}] @ {}", function, name, inpos); + } else if function == PARANOIA_CB_CACHEERR { + if self.cacheerr == 0 { + let _ = write!( + io::stderr(), + "\rWARNING: The CDROM drive appears to be seeking impossibly quickly.\n\ +This could be due to timer bugs, a drive that really is improbably fast,\n\ +or, most likely, a bug in cdparanoia's cache modelling.\n\n\ +Please consider using the -A option to perform an analysis run, then mail\n\ +the cdparanoia.log file produced by the analysis to paranoia-dev@xiph.org\n\ +to assist developers in correcting the problem.\n\n" + ); + } + self.cacheerr += 1; + } + + if function == PARANOIA_CB_SKIP { + // Keep -X functional even when progress is redirected or quiet. + self.skipped_flag = true; + } + + if log.quiet { + return; + } + + let osector = inpos; + let sector = inpos / CD_FRAMEWORDS; + + if self.printit == -1 { + self.printit = if unsafe { libc::isatty(2) } == 1 { + 1 + } else { + 0 + }; + } + + if self.printit == 1 { + let position = if self.callend != self.callbegin { + ((sector - self.callbegin) as f64 / (self.callend - self.callbegin) as f64 + * GRAPH as f64) as i32 + } else { + 0 + }; + let aheadposition = if self.callend != self.callbegin { + ((self.c_sector - self.callbegin) as f64 / (self.callend - self.callbegin) as f64 + * GRAPH as f64) as i32 + } else { + 0 + }; + + if function == -2 { + self.v_sector = sector; + return; + } + if function == -1 { + self.last = 8; + self.heartbeat = b'*'; + self.slevel = 0; + self.v_sector = sector; + } else if position < GRAPH as i32 && position >= 0 { + let p = position as usize; + match function { + PARANOIA_CB_VERIFY => { + if self.stimeout >= 30 { + if self.overlap as i64 > CD_FRAMEWORDS { + self.slevel = 2; + } else { + self.slevel = 1; + } + } + } + PARANOIA_CB_READ => { + if sector > self.c_sector { + self.c_sector = sector; + } + } + PARANOIA_CB_FIXUP_EDGE => { + if self.stimeout >= 5 { + if self.overlap as i64 > CD_FRAMEWORDS { + self.slevel = 2; + } else { + self.slevel = 1; + } + } + if self.dispcache[p] == b' ' { + self.dispcache[p] = b'-'; + } + } + PARANOIA_CB_FIXUP_ATOM => { + if self.slevel < 3 || self.stimeout > 5 { + self.slevel = 3; + } + if self.dispcache[p] == b' ' || self.dispcache[p] == b'-' { + self.dispcache[p] = b'+'; + } + } + PARANOIA_CB_READERR => { + self.slevel = 6; + if self.dispcache[p] != b'V' && self.dispcache[p] != b'C' { + self.dispcache[p] = b'e'; + } + } + PARANOIA_CB_CACHEERR => { + self.slevel = 8; + self.dispcache[p] = b'C'; + } + PARANOIA_CB_SKIP => { + self.slevel = 8; + if self.dispcache[p] != b'C' { + self.dispcache[p] = b'V'; + } + } + PARANOIA_CB_OVERLAP => { + self.overlap = osector as i32; + } + PARANOIA_CB_SCRATCH => { + self.slevel = 7; + } + PARANOIA_CB_DRIFT => { + if self.slevel < 4 || self.stimeout > 5 { + self.slevel = 4; + } + } + PARANOIA_CB_FIXUP_DROPPED | PARANOIA_CB_FIXUP_DUPED => { + self.slevel = 5; + if matches!(self.dispcache[p], b' ' | b'-' | b'+') { + self.dispcache[p] = b'!'; + } + } + _ => {} + } + } + + let mut smilie = "= :-)"; + match self.slevel { + 0 => { + smilie = if self.skipped_flag { " 8-X" } else { " :^D" }; + } + 1 => smilie = " :-)", + 2 => smilie = " :-|", + 3 => smilie = " :-P", + 4 => smilie = " :-/", + 5 => smilie = " 8-|", + 6 => smilie = " :-0", + 7 => smilie = " :-(", + 8 => { + smilie = " ;-("; + self.skipped_flag = true; + } + _ => {} + } + + let test = Self::now_tenths(); + if self.lasttime != test || function == -1 || self.slast != self.slevel { + if self.lasttime != test || function == -1 { + self.last += 1; + self.lasttime = test; + if self.last > 7 { + self.last = 0; + } + self.stimeout += 1; + self.heartbeat = match self.last { + 0 => b' ', + 1 | 7 => b'.', + 2 | 6 => b'o', + 3 | 5 => b'0', + 4 => b'O', + _ => self.heartbeat, + }; + if function == -1 { + self.heartbeat = b'*'; + } + } + if self.slast != self.slevel { + self.stimeout = 0; + } + self.slast = self.slevel; + + let mut buffer: Vec = + if self.abort_on_skip && self.skipped_flag && function != -1 { + format!( + "\r (== PROGRESS == [{}| {:06} {:02} ] =={} {} ==) ", + " ...aborting; please wait... ", + self.v_sector, + self.overlap as i64 / CD_FRAMEWORDS, + smilie, + self.heartbeat as char + ) + .into_bytes() + } else { + let disp = String::from_utf8_lossy(&self.dispcache).to_string(); + if self.v_sector == 0 { + format!( + "\r (== PROGRESS == [{}| ...... {:02} ] =={} {} ==) ", + disp, + self.overlap as i64 / CD_FRAMEWORDS, + smilie, + self.heartbeat as char + ) + .into_bytes() + } else { + format!( + "\r (== PROGRESS == [{}| {:06} {:02} ] =={} {} ==) ", + disp, + self.v_sector, + self.overlap as i64 / CD_FRAMEWORDS, + smilie, + self.heartbeat as char + ) + .into_bytes() + } + }; + + if aheadposition >= 0 + && aheadposition < GRAPH as i32 + && function != -1 + && buffer.len() > 19 + aheadposition as usize + { + buffer[19 + aheadposition as usize] = b'>'; + } + let _ = io::stderr().write_all(&buffer); + + if function == -1 { + let mut report_bytes = buffer[1..].to_vec(); + report_bytes.extend_from_slice(b"\n\n"); + log.write_summary_bytes(&report_bytes); + } + } + } + + if function == -1 { + self.dispcache = [b' '; GRAPH]; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn skip_flag_survives_redirected_output() { + let mut p = Progress::default(); + let mut log = Log::new(); + log.quiet = true; + p.callback(0, PARANOIA_CB_SKIP, &mut log); + assert!(p.skipped_flag); + } +} diff --git a/crates/cli/src/span.rs b/crates/cli/src/span.rs new file mode 100644 index 0000000..51b9862 --- /dev/null +++ b/crates/cli/src/span.rs @@ -0,0 +1,271 @@ +//! Span argument parsing, a direct port of `parse_offset()` from main.c. + +use cdrustanoia_core::device::DiscToc; + +#[derive(Debug, Clone)] +pub struct SpanError(pub String); + +impl std::fmt::Display for SpanError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} +impl std::error::Error for SpanError {} + +fn err(msg: impl Into) -> SpanError { + SpanError(msg.into()) +} + +#[derive(Default, Debug, Clone, Copy)] +struct TimeParts { + hours: Option, + minutes: Option, + seconds: Option, + sectors: Option, +} + +fn parse_num(s: &str, original: &str) -> Result { + let digits: String = s.chars().take_while(|c| c.is_ascii_digit()).collect(); + if digits.is_empty() { + Ok(0) + } else { + digits + .parse::() + .map_err(|_| err(format!("Error parsing span argument: {original}"))) + } +} + +fn parse_time(time: &str) -> Result { + let mut parts = TimeParts::default(); + let mut rest = time; + + if let Some(pos) = rest.rfind('.') { + parts.sectors = Some(parse_num(&rest[pos + 1..], time)?); + rest = &rest[..pos]; + if rest.contains('.') { + return Err(err("Error parsing span argument")); + } + } + + let fields: Vec<&str> = rest.split(':').collect(); + if fields.len() > 3 { + return Err(err("Error parsing span argument")); + } + // The original walks right-to-left: rightmost field is seconds, then + // minutes, then hours. + match fields.len() { + 1 => { + parts.seconds = Some(parse_num(fields[0], time)?); + } + 2 => { + parts.seconds = Some(parse_num(fields[1], time)?); + parts.minutes = Some(parse_num(fields[0], time)?); + } + 3 => { + parts.seconds = Some(parse_num(fields[2], time)?); + parts.minutes = Some(parse_num(fields[1], time)?); + parts.hours = Some(parse_num(fields[0], time)?); + } + _ => return Err(err("Error parsing span argument")), + } + Ok(parts) +} + +fn has_time(parts: &TimeParts) -> bool { + parts.seconds.is_some() || parts.sectors.is_some() +} + +/// Port of `parse_offset`. `begin` is the beginning of a span, if known. +pub fn parse_offset( + toc: &DiscToc, + offset: Option<&str>, + begin: Option, +) -> Result { + let offset = match offset { + Some(s) => s, + None => return Ok(-1), + }; + + let (track_text, time_text) = if let Some(close) = offset.find(']') { + let before = &offset[..close]; + if let Some(open) = before.find('[') { + (&before[..open], Some(&before[open + 1..])) + } else { + return Err(err("Error parsing span argument")); + } + } else { + (offset, None) + }; + + let digit_count = track_text + .chars() + .take_while(|c| c.is_ascii_digit()) + .count(); + let track = if digit_count > 0 { + let t = track_text[..digit_count] + .parse::() + .map_err(|_| err("Error parsing span argument"))?; + if t < 0 || t > toc.tracks as i64 { + return Err(err(format!("Track #{t} does not exist."))); + } + Some(t) + } else { + None + }; + + let parts = match time_text { + Some(t) if !t.is_empty() => parse_time(t)?, + Some(_) => TimeParts::default(), + None => TimeParts::default(), + }; + + let ret = match track { + None => { + if !has_time(&parts) { + return Ok(-1); + } + begin.unwrap_or_else(|| toc.disc_firstsector()) + } + Some(t) => { + if !has_time(&parts) { + if begin.is_none() { + return Ok(toc.track_firstsector(t as i32)); + } + return Ok(toc.track_lastsector(t as i32)); + } + toc.track_firstsector(t as i32) + } + }; + + let mut ret = ret; + let mut add = |v: i64, scale: i64| -> Result<(), SpanError> { + ret = ret + .checked_add( + v.checked_mul(scale) + .ok_or_else(|| err("Error parsing span argument"))?, + ) + .ok_or_else(|| err("Error parsing span argument"))?; + Ok(()) + }; + if let Some(v) = parts.sectors { + add(v, 1)?; + } + if let Some(v) = parts.seconds { + add(v, 75)?; + } + if let Some(v) = parts.minutes { + add(v, 60 * 75)?; + } + if let Some(v) = parts.hours { + add(v, 60 * 60 * 75)?; + } + + if let Some(t) = track { + if toc.sector_gettrack(ret) != t as i32 { + return Err(err( + "Time/sector offset goes beyond end of specified track.", + )); + } + } + if ret > toc.disc_lastsector() { + return Err(err("Time/sector offset goes beyond end of disc.")); + } + + Ok(ret) +} + +/// Parse a complete span expression, returning inclusive first/last sectors. +pub fn parse_span(toc: &DiscToc, span: &str) -> Result<(i64, i64), SpanError> { + let first_hyphen = span.find('-'); + let last_hyphen = span.rfind('-'); + if first_hyphen != last_hyphen { + return Err(err("Error parsing span argument")); + } + + let (part1, part2, had_hyphen) = match first_hyphen { + Some(pos) => (&span[..pos], Some(&span[pos + 1..]), true), + None => (span, None, false), + }; + + let mut first = parse_offset(toc, Some(part1), None)?; + let mut last = if first == -1 { + parse_offset(toc, part2, Some(toc.disc_firstsector()))? + } else { + parse_offset(toc, part2, Some(first))? + }; + + if first == -1 { + if last == -1 { + return Err(err("Error parsing span argument")); + } + first = toc.disc_firstsector(); + } else if last == -1 { + if had_hyphen { + last = toc.disc_lastsector(); + } else { + let track = toc.sector_gettrack(first); + last = toc.track_lastsector(track); + } + } + + Ok((first, last)) +} + +#[cfg(test)] +mod tests { + use super::*; + use cdrustanoia_core::device::TocEntry; + + fn toc() -> DiscToc { + let mut t = DiscToc::default(); + t.entries[0] = TocEntry { + flags: 0, + track: 1, + start_sector: 0, + }; + t.entries[1] = TocEntry { + flags: 0, + track: 2, + start_sector: 75000, + }; + t.entries[2] = TocEntry { + flags: 0, + track: 3, + start_sector: 150000, + }; + t.entries[3] = TocEntry { + flags: 0x04, + track: 0xAA, + start_sector: 225000, + }; + t.tracks = 3; + t + } + + #[test] + fn simple_track() { + assert_eq!(parse_span(&toc(), "2").unwrap(), (75000, 149999)); + } + + #[test] + fn ranges() { + assert_eq!(parse_span(&toc(), "2-3").unwrap(), (75000, 224999)); + assert_eq!(parse_span(&toc(), "-2").unwrap(), (0, 149999)); + assert_eq!(parse_span(&toc(), "2-").unwrap(), (75000, 224999)); + } + + #[test] + fn times() { + assert_eq!(parse_span(&toc(), "1[1:00]").unwrap(), (4500, 74999)); + assert_eq!(parse_span(&toc(), "2[::20]").unwrap(), (76500, 149999)); + assert_eq!(parse_span(&toc(), "2[.30]").unwrap(), (75030, 149999)); + assert_eq!(parse_span(&toc(), "2[10:]").unwrap(), (120000, 149999)); + } + + #[test] + fn bad() { + assert!(parse_span(&toc(), "4").is_err()); + assert!(parse_span(&toc(), "1-2-3").is_err()); + assert!(parse_span(&toc(), "2[1:00:00:00]").is_err()); + } +} diff --git a/crates/cli/tests/e2e.rs b/crates/cli/tests/e2e.rs new file mode 100644 index 0000000..6a078a7 --- /dev/null +++ b/crates/cli/tests/e2e.rs @@ -0,0 +1,50 @@ +use cdrustanoia_core::device::samples_to_bytes; + +#[test] +fn end_to_end_image_rip() { + let dir = tempfile::tempdir().unwrap(); + let image = dir.path().join("disc.raw"); + let out = dir.path().join("out.raw"); + let sectors = 24usize; + let mut samples = Vec::with_capacity(sectors * 1176); + for i in 0..sectors * 1176 { + samples.push(((i as i32 * 7919) % 65536 - 32768) as i16); + } + std::fs::write(&image, samples_to_bytes(&samples)).unwrap(); + std::env::set_var("CDRUSTANOIA_TEST_IMAGE", &image); + + let argv = vec![ + "cdparanoia".to_string(), + "-q".to_string(), + "-r".to_string(), + "1".to_string(), + out.to_string_lossy().into_owned(), + ]; + assert_eq!(cdrustanoia_cli::run(&argv), 0); + let got = std::fs::read(&out).unwrap(); + assert_eq!(got, samples_to_bytes(&samples)); + std::env::remove_var("CDRUSTANOIA_TEST_IMAGE"); + + // Multi-track batch mode. + let multi = dir.path().join("multi.raw"); + let mut multi_samples = Vec::new(); + for i in 0..(48 * 1176) { + multi_samples.push((((i as i64 * 104729) % 65536) as i32 - 32768) as i16); + } + std::fs::write(&multi, samples_to_bytes(&multi_samples)).unwrap(); + std::fs::write(dir.path().join("multi.raw.toc"), "0 23 0\n24 47 0\n").unwrap(); + std::env::set_var("CDRUSTANOIA_TEST_IMAGE", &multi); + let argv = vec![ + "cdparanoia".to_string(), + "-q".to_string(), + "-B".to_string(), + "1-2".to_string(), + format!("{}/", dir.path().display()), + ]; + assert_eq!(cdrustanoia_cli::run(&argv), 0); + let t1 = std::fs::read(dir.path().join("track01.cdda.wav")).unwrap(); + let t2 = std::fs::read(dir.path().join("track02.cdda.wav")).unwrap(); + assert_eq!(&t1[44..], &samples_to_bytes(&multi_samples[..24 * 1176])); + assert_eq!(&t2[44..], &samples_to_bytes(&multi_samples[24 * 1176..])); + std::env::remove_var("CDRUSTANOIA_TEST_IMAGE"); +} diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml new file mode 100644 index 0000000..ec17ba5 --- /dev/null +++ b/crates/core/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "cdrustanoia_core" +version.workspace = true +edition.workspace = true +license = "LGPL-2.1-or-later" + +[dependencies] +libc.workspace = true + +[dev-dependencies] +tempfile = "3" diff --git a/crates/core/src/callback.rs b/crates/core/src/callback.rs new file mode 100644 index 0000000..947eacc --- /dev/null +++ b/crates/core/src/callback.rs @@ -0,0 +1,16 @@ +//! Paranoia callback event codes, from the LGPL cdda_paranoia.h API. + +pub const PARANOIA_CB_READ: i32 = 0; +pub const PARANOIA_CB_VERIFY: i32 = 1; +pub const PARANOIA_CB_FIXUP_EDGE: i32 = 2; +pub const PARANOIA_CB_FIXUP_ATOM: i32 = 3; +pub const PARANOIA_CB_SCRATCH: i32 = 4; +pub const PARANOIA_CB_REPAIR: i32 = 5; +pub const PARANOIA_CB_SKIP: i32 = 6; +pub const PARANOIA_CB_DRIFT: i32 = 7; +pub const PARANOIA_CB_BACKOFF: i32 = 8; +pub const PARANOIA_CB_OVERLAP: i32 = 9; +pub const PARANOIA_CB_FIXUP_DROPPED: i32 = 10; +pub const PARANOIA_CB_FIXUP_DUPED: i32 = 11; +pub const PARANOIA_CB_READERR: i32 = 12; +pub const PARANOIA_CB_CACHEERR: i32 = 13; diff --git a/crates/core/src/device/discover.rs b/crates/core/src/device/discover.rs new file mode 100644 index 0000000..785a607 --- /dev/null +++ b/crates/core/src/device/discover.rs @@ -0,0 +1,138 @@ +//! CD-ROM device discovery, mirroring scan_devices.c's search order. + +use crate::device::image::ImageDrive; +use crate::device::linux::LinuxDrive; +use crate::device::{Drive, InterfaceKind}; +use std::fs; +use std::os::unix::fs::FileTypeExt; +use std::path::Path; + +pub fn image_override() -> Option { + let path = std::env::var_os("CDRUSTANOIA_TEST_IMAGE")?; + ImageDrive::open_path(Path::new(&path)).ok() +} + +pub fn identify_sgio(device: &str, verbose: bool) -> Result, String> { + if verbose { + eprintln!("\tTesting {} for SCSI/MMC interface", device); + } + let drive = LinuxDrive::sg_io(device)?; + if verbose { + eprintln!("\t\tSG_IO device: {}", device); + eprintln!("\nCDROM model sensed sensed: {}", drive.drive_model()); + } + Ok(Box::new(drive)) +} + +pub fn identify_cooked(device: &str, verbose: bool) -> Result, String> { + if verbose { + eprintln!("\tTesting {} for cooked ioctl() interface", device); + } + let drive = LinuxDrive::cooked(device)?; + if verbose { + eprintln!("\t\tCDROM sensed: {}", drive.drive_model()); + } + Ok(Box::new(drive)) +} + +pub fn identify_device(device: &str, verbose: bool) -> Result, String> { + if let Some(img) = image_override() { + return Ok(Box::new(img)); + } + if verbose { + eprintln!("Checking {} for cdrom...", device); + } + let meta = fs::metadata(device).map_err(|e| format!("Could not stat {}: {}", device, e))?; + let ft = meta.file_type(); + if !ft.is_block_device() && !ft.is_char_device() { + return Err(format!("{} is not a block or character device", device)); + } + let sg_err = match identify_sgio(device, verbose) { + Ok(d) => return Ok(d), + Err(e) => e, + }; + match identify_cooked(device, verbose) { + Ok(d) => Ok(d), + Err(cooked_err) => Err(format!( + "Unable to identify {} (SG_IO: {}; cooked: {})", + device, sg_err, cooked_err + )), + } +} + +pub fn identify(device: &str, verbose: bool, force_cooked: bool) -> Result, String> { + if force_cooked { + identify_cooked(device, verbose) + } else { + identify_device(device, verbose) + } +} + +pub fn find_a_cdrom(verbose: bool) -> Option> { + if let Some(img) = image_override() { + return Some(Box::new(img)); + } + let mut candidates: Vec = Vec::new(); + candidates.push("/dev/cdrom".to_string()); + for prefix in [ + "/dev/cdroms/cdrom", + "/dev/sr", + "/dev/scd", + "/dev/hd", + "/dev/sg", + ] { + for i in 0..4 { + candidates.push(format!("{}{}", prefix, i)); + candidates.push(format!("{}{}", prefix, (b'a' + i) as char)); + } + } + for name in [ + "/dev/cdu31a", + "/dev/cdu535", + "/dev/sbpcd", + "/dev/sonycd", + "/dev/mcd", + "/dev/sjcd", + "/dev/cm206cd", + "/dev/gscd", + "/dev/optcd", + ] { + candidates.push(name.to_string()); + } + + let mut seen = std::collections::HashSet::new(); + for dev in candidates { + if !seen.insert(dev.clone()) { + continue; + } + if !Path::new(&dev).exists() { + continue; + } + match identify_device(&dev, verbose) { + Ok(d) => { + if verbose { + eprintln!("Using {}", dev); + } + return Some(d); + } + Err(_) => { + if verbose { + eprintln!(); + } + } + } + } + if verbose { + let user = std::env::var("USER").unwrap_or_else(|_| "user".to_string()); + eprintln!("\n\nNo cdrom drives accessible to {} found.", user); + } + None +} + +pub fn interface_name(d: &dyn Drive) -> &'static str { + match d.interface() { + InterfaceKind::SgIo => "SG_IO", + InterfaceKind::Cooked => "cooked ioctl", + InterfaceKind::Image => "image", + } +} diff --git a/crates/core/src/device/image.rs b/crates/core/src/device/image.rs new file mode 100644 index 0000000..c97b606 --- /dev/null +++ b/crates/core/src/device/image.rs @@ -0,0 +1,323 @@ +//! Synthetic image backend used by tests and by the hidden +//! `CDRUSTANOIA_TEST_IMAGE` development interface. + +use super::{ + bytes_to_samples, swap_raw16, DiscToc, Drive, InterfaceKind, ReadError, TimedRead, TocEntry, + CD_FRAMESIZE_RAW, CD_FRAMEWORDS, MAXTRK, +}; +use std::fs; +use std::path::Path; +use std::time::Instant; + +#[derive(Clone, Debug, Default)] +pub struct ImageFaults { + /// Shift the beginning of each read by up to this many PCM words. + pub jitter_words: i64, + /// Only every Nth read is jittered (0 = every read when jitter_words > 0). + pub jitter_period: u32, + /// Leave the first N reads unjittered to establish an absolute anchor. + pub jitter_skip: u32, + /// Return at most this many sectors per read (0 = no limit). + pub max_read_sectors: i64, + /// Consistent sample discontinuity at a sector boundary. + pub discontinuities: Vec<(i64, i64)>, // (sector, delta words) + /// Consistently corrupt this source range. + pub scratch: Option<(i64, i64)>, // (word offset, word length) + /// Simulated duration of a read in milliseconds. + pub read_ms: i32, + /// Simulated cache-friendly timing when reading backwards. + pub backward_ms: i32, +} + +#[derive(Clone, Debug)] +pub struct ImageConfig { + pub tracks: Vec<(i64, i64, u8)>, // start sector, end sector (inclusive), flags + pub faults: ImageFaults, + pub bigendianp: i32, + pub nsectors: i64, +} + +impl Default for ImageConfig { + fn default() -> Self { + Self { + tracks: Vec::new(), + faults: ImageFaults { + read_ms: 20, + backward_ms: 20, + ..Default::default() + }, + bigendianp: 0, + nsectors: 13, + } + } +} + +pub struct ImageDrive { + samples: Vec, + toc: DiscToc, + faults: ImageFaults, + bigendianp: i32, + nsectors: i64, + opened: bool, + model: String, + read_count: u32, + last_begin: i64, +} + +impl ImageDrive { + pub fn from_bytes(data: Vec) -> Self { + let sectors = (data.len() / CD_FRAMESIZE_RAW) as i64; + let tracks = if sectors > 0 { + vec![(0, sectors - 1, 0u8)] + } else { + vec![(0, 0, 0u8)] + }; + Self::with_config( + data, + ImageConfig { + tracks, + ..Default::default() + }, + ) + } + + pub fn with_config(data: Vec, config: ImageConfig) -> Self { + let samples = bytes_to_samples(&data); + let mut toc = DiscToc::default(); + let tracks = config.tracks; + let n = tracks.len().min(MAXTRK - 1); + for (i, (start, _end, flags)) in tracks.iter().take(n).enumerate() { + toc.entries[i] = TocEntry { + flags: *flags, + track: (i + 1) as u8, + start_sector: *start, + }; + } + let leadout = if n > 0 { + tracks[n - 1].1 + 1 + } else { + (data.len() / CD_FRAMESIZE_RAW) as i64 + }; + toc.entries[n] = TocEntry { + flags: 0x04, + track: 0xAA, + start_sector: leadout, + }; + toc.tracks = n as i32; + let mut d = Self { + samples, + toc, + faults: config.faults, + bigendianp: config.bigendianp, + nsectors: config.nsectors, + opened: true, + model: "File based test interface".to_string(), + read_count: 0, + last_begin: -1, + }; + d.fix_toc(); + d + } + + pub fn open_path(path: &Path) -> std::io::Result { + let data = fs::read(path)?; + let mut config = ImageConfig::default(); + let mut sectors = (data.len() / CD_FRAMESIZE_RAW) as i64; + if sectors < 1 { + sectors = 1; + } + config.tracks = vec![(0, sectors - 1, 0)]; + // Optional sidecar: one "start end flags" line per track. + let sidecar = path.with_extension(format!( + "{}.toc", + path.extension().and_then(|s| s.to_str()).unwrap_or("raw") + )); + if let Ok(text) = fs::read_to_string(&sidecar) { + let mut tracks = Vec::new(); + for line in text.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + let mut fields = line.split_whitespace(); + let start: i64 = match fields.next().and_then(|s| s.parse().ok()) { + Some(v) => v, + None => continue, + }; + let end: i64 = fields.next().and_then(|s| s.parse().ok()).unwrap_or(start); + let flags: u8 = fields + .next() + .and_then(|s| u8::from_str_radix(s, 16).ok()) + .unwrap_or(0); + tracks.push((start, end, flags)); + } + if !tracks.is_empty() { + config.tracks = tracks; + } + } + let mut d = Self::with_config(data, config); + d.model = format!("Image {}", path.display()); + d.opened = true; + Ok(d) + } + + pub fn source_samples(&self) -> &[i16] { + &self.samples + } + + fn fix_toc(&mut self) { + let tracks = self.toc.tracks.max(0) as usize; + self.toc.fixup(tracks + 1); + } + + fn mapped_sample(&self, target_word: i64) -> Option { + if target_word < 0 { + return None; + } + let mut source = target_word; + for (sector, delta) in &self.faults.discontinuities { + if target_word / CD_FRAMEWORDS >= *sector { + source += *delta; + } + } + if let Some((start, len)) = self.faults.scratch { + if target_word >= start && target_word < start + len { + // A deterministic, repeated-ish garbage pattern. + let x = (target_word as u64).wrapping_mul(0x9E3779B97F4A7C15); + return Some((x >> 48) as i16); + } + } + self.samples.get(source as usize).copied() + } + + fn read_words(&mut self, begin_sector: i64, sectors: i64) -> Vec { + let words = sectors.max(0) * CD_FRAMEWORDS; + let mut out = Vec::with_capacity(words as usize); + let mut jitter = 0i64; + if self.faults.jitter_words > 0 { + self.read_count = self.read_count.wrapping_add(1); + let period = if self.faults.jitter_period == 0 { + 1 + } else { + self.faults.jitter_period + }; + if self.read_count.is_multiple_of(period) { + let max = self.faults.jitter_words; + let x = (self.read_count as u64).wrapping_mul(6364136223846793005); + jitter = (x % (2 * max as u64 + 1)) as i64 - max; + } + if self.read_count <= self.faults.jitter_skip { + jitter = 0; + } + } + let start = begin_sector * CD_FRAMEWORDS + jitter; + for w in 0..words { + out.push(self.mapped_sample(start + w).unwrap_or(0)); + } + out + } +} + +impl Drive for ImageDrive { + fn toc(&self) -> &DiscToc { + &self.toc + } + fn toc_mut(&mut self) -> &mut DiscToc { + &mut self.toc + } + fn opened(&self) -> bool { + self.opened + } + fn read_audio( + &mut self, + buffer: Option<&mut [u8]>, + begin: i64, + sectors: i64, + ) -> Result { + if !self.opened { + return Err(ReadError::closed()); + } + if sectors <= 0 { + return Ok(0); + } + let mut sectors = sectors; + if self.faults.max_read_sectors > 0 { + sectors = sectors.min(self.faults.max_read_sectors); + } + let words = self.read_words(begin, sectors); + if let Some(buf) = buffer { + let bytes = super::samples_to_bytes(&words); + let n = bytes.len().min(buf.len()); + buf[..n].copy_from_slice(&bytes[..n]); + if super::host_big_endian() { + swap_raw16(&mut buf[..n]); + } + } + self.last_begin = begin; + Ok(sectors as usize) + } + fn read_audio_raw( + &mut self, + buffer: Option<&mut [u8]>, + begin: i64, + sectors: i64, + ) -> Result { + if !self.opened { + return Err(ReadError::closed()); + } + if sectors <= 0 { + return Ok(0); + } + let mut sectors = sectors; + if self.faults.max_read_sectors > 0 { + sectors = sectors.min(self.faults.max_read_sectors); + } + let words = self.read_words(begin, sectors); + if let Some(buf) = buffer { + let bytes = super::samples_to_bytes(&words); + let n = bytes.len().min(buf.len()); + buf[..n].copy_from_slice(&bytes[..n]); + } + self.last_begin = begin; + Ok(sectors as usize) + } + fn read_timed(&mut self, buffer: Option<&mut [u8]>, begin: i64, sectors: i64) -> TimedRead { + let start = Instant::now(); + let previous_begin = self.last_begin; + let result = self.read_audio(buffer, begin, sectors); + let _elapsed = start.elapsed(); + let ms = if begin < previous_begin { + self.faults.backward_ms + } else { + self.faults.read_ms + }; + TimedRead { + result, + milliseconds: ms, + } + } + fn speed_set(&mut self, _speed: i32) -> i32 { + 0 + } + fn close(&mut self) { + self.opened = false; + } + fn drive_model(&self) -> &str { + &self.model + } + fn bigendianp(&self) -> i32 { + self.bigendianp + } + fn set_bigendianp(&mut self, value: i32) { + self.bigendianp = value; + } + fn nsectors(&self) -> i64 { + self.nsectors + } + fn set_nsectors(&mut self, value: i64) { + self.nsectors = value; + } + fn interface(&self) -> InterfaceKind { + InterfaceKind::Image + } +} diff --git a/crates/core/src/device/linux.rs b/crates/core/src/device/linux.rs new file mode 100644 index 0000000..2a0d326 --- /dev/null +++ b/crates/core/src/device/linux.rs @@ -0,0 +1,1314 @@ +//! Linux SG_IO and cooked-ioctl CD-ROM backends. + +use crate::device::{ + host_big_endian, swap_raw16, DiscToc, Drive, InterfaceKind, ReadError, TimedRead, + CD_FRAMESIZE_RAW, MAXTRK, +}; +use libc::{c_int, c_uint, c_void}; +use std::fs::OpenOptions; +use std::io; +use std::os::unix::fs::OpenOptionsExt; +use std::os::unix::io::{IntoRawFd, RawFd}; +use std::time::Instant; + +const SG_MAX_SENSE: usize = 64; +const SG_IO: libc::c_ulong = 0x2285; +const SG_EMULATED_HOST: libc::c_ulong = 0x2203; +const SG_SCSI_RESET: libc::c_ulong = 0x2284; + +const SG_DXFER_NONE: c_int = -1; +const SG_DXFER_TO_DEV: c_int = -2; +const SG_DXFER_FROM_DEV: c_int = -3; +const SG_FLAG_DIRECT_IO: c_uint = 1; + +const CDROMREADTOCHDR: libc::c_ulong = 0x5305; +const CDROMREADTOCENTRY: libc::c_ulong = 0x5306; +const CDROMREADAUDIO: libc::c_ulong = 0x530e; +const CDROMMULTISESSION: libc::c_ulong = 0x5310; +const CDROM_SELECT_SPEED: libc::c_ulong = 0x5322; +const CDROM_GET_CAPABILITY: libc::c_ulong = 0x5331; +const CDROMVOLREAD: libc::c_ulong = 0x5313; +const CDROM_LBA: u8 = 1; +const CDROM_LEADOUT: u8 = 0xAA; +const TYPE_ROM: u8 = 5; +const TYPE_WORM: u8 = 4; +const MAX_RETRIES: i32 = 8; + +#[repr(C)] +struct SgIoHdr { + interface_id: c_int, + dxfer_direction: c_int, + cmd_len: u8, + mx_sb_len: u8, + iovec_count: u16, + dxfer_len: c_uint, + dxferp: *mut c_void, + cmdp: *mut u8, + sbp: *mut u8, + timeout: c_uint, + flags: c_uint, + pack_id: c_int, + usr_ptr: *mut c_void, + status: u8, + masked_status: u8, + msg_status: u8, + sb_len_wr: u8, + host_status: u16, + driver_status: u16, + resid: c_int, + duration: c_uint, + info: c_uint, +} + +#[repr(C)] +#[derive(Clone, Copy)] +union CdromAddr { + lba: c_int, + msf: [u8; 3], +} + +#[repr(C)] +struct CdromTocHdr { + trk0: u8, + trk1: u8, +} + +#[repr(C)] +struct CdromTocEntry { + track: u8, + adr_ctrl: u8, + format: u8, + addr: CdromAddr, + datamode: u8, +} + +#[repr(C)] +struct CdromReadAudio { + addr: CdromAddr, + addr_format: u8, + nframes: c_int, + buf: *mut u8, +} + +#[repr(C)] +struct CdromMultisession { + addr: CdromAddr, + xa_flag: u8, + addr_format: u8, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ScsiError { + pub errno: c_int, + pub key: u8, + pub asc: u8, + pub ascq: u8, + pub transport: &'static str, +} + +impl ScsiError { + fn io(e: io::Error) -> Self { + Self { + errno: e.raw_os_error().unwrap_or(libc::EIO), + key: 0, + asc: 0, + ascq: 0, + transport: "host I/O", + } + } +} + +#[allow(non_camel_case_types)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum ReadCommand { + Read28, + ReadA8, + MmcB10, + MmcB2_10, + MmcB2_F8, + MmcB6_F8, + MmcB4_F8, + Mmc2_F8, + Mmc3_F8, + Msf10, + MsfF8, + Msf3F8, + D4_10, + D4_12, + D5, + D8, +} + +fn lba_to_msf(lba: i64, m: &mut u8, s: &mut u8, f: &mut u8) { + let mut lba = lba; + if lba >= -150 { + *m = ((lba + 150) / (60 * 75)) as u8; + lba -= (*m as i64) * 60 * 75; + *s = ((lba + 150) / 75) as u8; + lba -= (*s as i64) * 75; + *f = (lba + 150) as u8; + } else { + *m = ((lba + 450150) / (60 * 75)) as u8; + lba -= (*m as i64) * 60 * 75; + *s = ((lba + 450150) / 75) as u8; + lba -= (*s as i64) * 75; + *f = (lba + 450150) as u8; + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum EnableMode { + Dummy, + ModeSelect, +} + +pub struct LinuxDrive { + fd: RawFd, + toc: DiscToc, + bigendianp: i32, + nsectors: i64, + opened: bool, + model: String, + interface: InterfaceKind, + is_atapi: bool, + is_mmc: bool, + lun: i32, + density: u8, + adjust_ssize: i64, + fua: bool, + read_cmd: ReadCommand, + enable_mode: EnableMode, +} + +impl LinuxDrive { + pub fn sg_io(device: &str) -> Result { + let fd = open_device(device)?; + let mut d = Self { + fd, + toc: DiscToc::default(), + bigendianp: -1, + nsectors: 27, + opened: false, + model: String::new(), + interface: InterfaceKind::SgIo, + is_atapi: false, + is_mmc: false, + lun: 0, + density: 0, + adjust_ssize: 1, + fua: false, + read_cmd: ReadCommand::D8, + enable_mode: EnableMode::Dummy, + }; + d.inquiry()?; + d.check_atapi(); + d.check_mmc(); + d.choose_defaults(); + Ok(d) + } + + pub fn cooked(device: &str) -> Result { + let fd = open_device_cooked(device)?; + // Ping for CD-ROM-ness. Some drives reject VOLREAD but answer the + // capability ioctl. + let cap = unsafe { libc::ioctl(fd, CDROM_GET_CAPABILITY, std::ptr::null::()) }; + if cap < 0 { + let mut vol = [0u8; 4]; + let rc = unsafe { libc::ioctl(fd, CDROMVOLREAD, vol.as_mut_ptr()) }; + if rc < 0 { + unsafe { + libc::close(fd); + } + return Err(format!("{} is not a cooked ioctl CDROM.", device)); + } + } + let d = Self { + fd, + toc: DiscToc::default(), + bigendianp: 0, + nsectors: 8, + opened: false, + model: "Generic CD-ROM (cooked ioctl)".to_string(), + interface: InterfaceKind::Cooked, + is_atapi: true, + is_mmc: false, + lun: 0, + density: 0, + adjust_ssize: 1, + fua: false, + read_cmd: ReadCommand::Read28, + enable_mode: EnableMode::Dummy, + }; + Ok(d) + } + + fn choose_defaults(&mut self) { + self.density = 0; + self.enable_mode = EnableMode::Dummy; + self.read_cmd = ReadCommand::D8; + self.fua = false; + if self.is_mmc || self.is_atapi { + self.read_cmd = ReadCommand::MmcB2_F8; + self.bigendianp = 0; + } + // Vendor exceptions, mirroring drive_exceptions.h. + if self.model.starts_with("SAMSUNG SCR-830 REV 2.09") { + self.read_cmd = ReadCommand::Mmc2_F8; + } else if self.model.starts_with("KENWOOD CD-ROM UCR") { + self.read_cmd = ReadCommand::D8; + } else if self.model.starts_with("NEC") { + self.read_cmd = ReadCommand::D4_10; + } else if self.model.starts_with("MATSHITA CD-R CW-7501") { + self.read_cmd = ReadCommand::D4_12; + } + if !self.is_mmc && !self.is_atapi { + if self.model.starts_with("TOSHIBA") + || self.model.starts_with("IBM") + || self.model.starts_with("DEC") + { + self.density = 0x82; + self.enable_mode = EnableMode::ModeSelect; + self.read_cmd = ReadCommand::Read28; + } else if self.model.starts_with("IMS") + || self.model.starts_with("KODAK") + || self.model.starts_with("RICOH") + || self.model.starts_with("HP") + || self.model.starts_with("PHILIPS") + || self.model.starts_with("PLASMON") + || self.model.starts_with("GRUNDIG CDR100IPW") + || self.model.starts_with("MITSUMI CD-R ") + { + self.enable_mode = EnableMode::ModeSelect; + self.read_cmd = ReadCommand::Read28; + self.bigendianp = 1; + } + } + } + + fn inquiry(&mut self) -> Result<(), String> { + let mut data = [0u8; 56]; + let cdb = [0x12u8, 0, 0, 0, 56, 0]; + self.sg_command(&cdb, Some(&mut data), SG_DXFER_FROM_DEV) + .map_err(|e| { + format!( + "Inquiry command failed; unable to probe drive ({})", + e.errno + ) + })?; + let mut dev_type = data[0] & 0x1f; + if data[8..15] == *b"TOSHIBA" && data[16..22] == *b"CD-ROM" && dev_type == 0 { + dev_type = TYPE_ROM; + } + if dev_type != TYPE_ROM && dev_type != TYPE_WORM { + return Err("Drive is neither a CDROM nor a WORM device".to_string()); + } + let vendor = String::from_utf8_lossy(&data[8..16]).trim().to_string(); + let model = String::from_utf8_lossy(&data[16..32]).trim().to_string(); + let rev = String::from_utf8_lossy(&data[32..36]).trim().to_string(); + self.model = [vendor, model, rev] + .into_iter() + .filter(|s| !s.is_empty()) + .collect::>() + .join(" "); + Ok(()) + } + + fn check_atapi(&mut self) { + let mut val: c_int = -1; + let rc = unsafe { libc::ioctl(self.fd, SG_EMULATED_HOST, &mut val as *mut c_int) }; + self.is_atapi = rc == 0 && val == 1; + } + + fn check_mmc(&mut self) { + self.is_mmc = false; + if let Ok((data, header_len)) = self.mode_sense(22, 0x2A) { + if data.len() > header_len { + let bdlen = if header_len == 8 { + (((data[6] as usize) << 8) | data[7] as usize) as usize + } else { + data[3] as usize + }; + let off = header_len + bdlen; + if off + 1 < data.len() && (data[off] & 0x3F) == 0x2A { + self.is_mmc = true; + if off + 5 < data.len() { + let _cdda_capable = data[off + 5] & 1 != 0; + } + } + } + } + } + + /// Returns raw mode-sense data and the size of its header (8 for + /// MODE SENSE(10), 4 for MODE SENSE(6)). + fn mode_sense(&mut self, size: usize, page: u8) -> Result<(Vec, usize), ScsiError> { + let mut data = vec![0u8; size + 8]; + let mut cdb = [0u8; 10]; + cdb[0] = 0x5A; // MODE SENSE(10) + cdb[1] = ((self.lun as u8) << 5) & 0xE0; + cdb[2] = 0x3F & page; + cdb[7] = ((size + 8) >> 8) as u8; + cdb[8] = ((size + 8) & 0xFF) as u8; + if self + .sg_command(&cdb, Some(&mut data), SG_DXFER_FROM_DEV) + .is_ok() + { + return Ok((data, 8)); + } + // Fallback to MODE SENSE(6). + let mut data6 = vec![0u8; size]; + let mut cdb6 = [0u8; 6]; + cdb6[0] = 0x1A; + cdb6[1] = ((self.lun as u8) << 5) & 0xE0; + cdb6[2] = 0x3F & page; + cdb6[4] = size as u8; + self.sg_command(&cdb6, Some(&mut data6), SG_DXFER_FROM_DEV)?; + Ok((data6, 4)) + } + + fn mode_select(&mut self, density: u8, secsize: u32) -> Result<(), ScsiError> { + let mut data = vec![0u8; 12]; + data[3] = 8; // block descriptor length + data[4] = density; + data[9] = (secsize >> 8) as u8; + data[10] = secsize as u8; + let mut cdb = [0u8; 6]; + cdb[0] = 0x15; + cdb[1] = 0x10; + cdb[4] = data.len() as u8; + self.sg_command(&cdb, Some(&mut data), SG_DXFER_TO_DEV)?; + Ok(()) + } + + fn enable_cdda(&mut self) -> Result<(), ScsiError> { + if self.enable_mode == EnableMode::ModeSelect { + self.mode_select(self.density, CD_FRAMESIZE_RAW as u32)?; + } + Ok(()) + } + + fn read_toc_sgio(&mut self) -> Result<(), String> { + let mut header = [0u8; 12]; + let mut cdb = [0u8; 10]; + cdb[0] = 0x43; + cdb[1] = ((self.lun as u8) << 5) & 0xE0; + cdb[6] = 1; + cdb[7] = 0; + cdb[8] = 12; + self.sg_command(&cdb, Some(&mut header), SG_DXFER_FROM_DEV) + .map_err(|_| "Unable to read table of contents header".to_string())?; + let first = header[2] as i32; + let last = header[3] as i32; + if last > MAXTRK as i32 || first > MAXTRK as i32 || last < first || first < 1 { + return Err("CDROM reporting illegal number of tracks".to_string()); + } + let tracks = last - first + 1; + if tracks as usize >= MAXTRK { + return Err("CDROM reporting illegal number of tracks".to_string()); + } + for track in first..=last { + let mut data = [0u8; 12]; + let mut cdb = [0u8; 10]; + cdb[0] = 0x43; + cdb[1] = ((self.lun as u8) << 5) & 0xE0; + cdb[6] = track as u8; + cdb[8] = 12; + self.sg_command(&cdb, Some(&mut data), SG_DXFER_FROM_DEV) + .map_err(|_| "Unable to read table of contents entry".to_string())?; + let idx = (track - first) as usize; + self.toc.entries[idx].flags = data[5]; + self.toc.entries[idx].track = track as u8; + self.toc.entries[idx].start_sector = self.adjust_ssize + * i32::from_be_bytes([data[8], data[9], data[10], data[11]]) as i64; + } + let mut data = [0u8; 12]; + let mut cdb = [0u8; 10]; + cdb[0] = 0x43; + cdb[1] = ((self.lun as u8) << 5) & 0xE0; + cdb[6] = 0xAA; + cdb[8] = 12; + self.sg_command(&cdb, Some(&mut data), SG_DXFER_FROM_DEV) + .map_err(|_| "Unable to read table of contents lead-out".to_string())?; + let idx = tracks as usize; + self.toc.entries[idx].flags = data[5]; + self.toc.entries[idx].track = 0xAA; + self.toc.entries[idx].start_sector = + self.adjust_ssize * i32::from_be_bytes([data[8], data[9], data[10], data[11]]) as i64; + self.toc.tracks = tracks; + self.toc.fixup((tracks + 1) as usize); + Ok(()) + } + + fn fixup_toc_multisession(&mut self) { + let mut ms = CdromMultisession { + addr: CdromAddr { lba: 0 }, + xa_flag: 0, + addr_format: CDROM_LBA, + }; + let rc = unsafe { libc::ioctl(self.fd, CDROMMULTISESSION, &mut ms as *mut _) }; + if rc != 0 { + self.toc.cd_extra = -1; + return; + } + let lba = unsafe { ms.addr.lba } as i64; + self.toc.cd_extra = 0; + if lba > 100 { + let tracks = self.toc.tracks as usize; + for j in (0..tracks).rev() { + if j > 0 && !self.toc.entries[j].audio() && self.toc.entries[j - 1].audio() { + if self.toc.entries[j].start_sector > lba - 11400 + && lba - 11400 > self.toc.entries[j - 1].start_sector + { + self.toc.entries[j].start_sector = lba - 11400; + } + break; + } + } + self.toc.cd_extra = 1; + } + } + + fn read_toc_cooked(&mut self) -> Result<(), String> { + let mut hdr = CdromTocHdr { trk0: 0, trk1: 0 }; + let rc = unsafe { libc::ioctl(self.fd, CDROMREADTOCHDR, &mut hdr as *mut _) }; + if rc != 0 { + return Err(format!( + "Unable to read table of contents header: {}", + io::Error::last_os_error() + )); + } + let tracks = hdr.trk1 as i32; + if tracks < 1 || tracks as usize >= MAXTRK { + return Err("CDROM reporting illegal number of tracks".to_string()); + } + for i in 1..=tracks { + let mut e = CdromTocEntry { + track: i as u8, + adr_ctrl: 0, + format: CDROM_LBA, + addr: CdromAddr { lba: 0 }, + datamode: 0, + }; + let rc = unsafe { libc::ioctl(self.fd, CDROMREADTOCENTRY, &mut e as *mut _) }; + if rc != 0 { + return Err("Unable to read table of contents entry".to_string()); + } + let adr = e.adr_ctrl & 0x0F; + let ctrl = e.adr_ctrl >> 4; + self.toc.entries[i as usize - 1].flags = (adr << 4) | (ctrl & 0x0F); + self.toc.entries[i as usize - 1].track = i as u8; + self.toc.entries[i as usize - 1].start_sector = unsafe { e.addr.lba } as i64; + } + let mut e = CdromTocEntry { + track: CDROM_LEADOUT, + adr_ctrl: 0, + format: CDROM_LBA, + addr: CdromAddr { lba: 0 }, + datamode: 0, + }; + let rc = unsafe { libc::ioctl(self.fd, CDROMREADTOCENTRY, &mut e as *mut _) }; + if rc != 0 { + return Err("Unable to read table of contents lead-out".to_string()); + } + let adr = e.adr_ctrl & 0x0F; + let ctrl = e.adr_ctrl >> 4; + self.toc.entries[tracks as usize].flags = (adr << 4) | (ctrl & 0x0F); + self.toc.entries[tracks as usize].track = 0xAA; + self.toc.entries[tracks as usize].start_sector = unsafe { e.addr.lba } as i64; + self.toc.tracks = tracks; + self.toc.fixup((tracks + 1) as usize); + self.fixup_toc_multisession(); + Ok(()) + } + + fn sg_command( + &mut self, + cdb: &[u8], + data: Option<&mut [u8]>, + direction: c_int, + ) -> Result { + let mut sense = [0u8; SG_MAX_SENSE]; + let mut cdb_buf = [0u8; 16]; + let n = cdb.len().min(16); + cdb_buf[..n].copy_from_slice(&cdb[..n]); + let (dxferp, dxfer_len, dir) = match data { + Some(ref buf) if !buf.is_empty() => { + (buf.as_ptr() as *mut c_void, buf.len() as c_uint, direction) + } + _ => (std::ptr::null_mut(), 0, SG_DXFER_NONE), + }; + let mut hdr = SgIoHdr { + interface_id: 'S' as c_int, + dxfer_direction: dir, + cmd_len: n as u8, + mx_sb_len: SG_MAX_SENSE as u8, + iovec_count: 0, + dxfer_len, + dxferp, + cmdp: cdb_buf.as_mut_ptr(), + sbp: sense.as_mut_ptr(), + timeout: 50_000, + flags: SG_FLAG_DIRECT_IO, + pack_id: 0, + usr_ptr: std::ptr::null_mut(), + status: 0, + masked_status: 0, + msg_status: 0, + sb_len_wr: 0, + host_status: 0, + driver_status: 0, + resid: 0, + duration: 0, + info: 0, + }; + let rc = unsafe { libc::ioctl(self.fd, SG_IO, &mut hdr as *mut SgIoHdr) }; + if rc < 0 { + return Err(ScsiError::io(io::Error::last_os_error())); + } + if hdr.status != 0 || hdr.host_status != 0 || hdr.driver_status != 0 { + let err = check_sbp(hdr.status, &sense); + return Err(err); + } + if hdr.resid < 0 { + return Err(ScsiError { + errno: libc::EIO, + key: 0, + asc: 0, + ascq: 0, + transport: "overrun", + }); + } + Ok(dxfer_len.saturating_sub(hdr.resid as c_uint) as usize) + } + + fn read_once( + &mut self, + command: ReadCommand, + begin: i64, + sectors: i64, + buf: &mut [u8], + ) -> Result { + let need = (sectors * CD_FRAMESIZE_RAW as i64) as usize; + if buf.len() < need { + return Err(ScsiError { + errno: libc::EINVAL, + key: 0, + asc: 0, + ascq: 0, + transport: "buffer too small", + }); + } + for b in buf[..need].iter_mut() { + *b = 0x7F; + } + let mut cdb = [0u8; 12]; + let len; + let mut use_msf = false; + match command { + ReadCommand::Read28 => { + cdb[0] = 0x28; + if self.fua { + cdb[1] |= 0x08; + } + cdb[3] = ((begin >> 16) & 0xFF) as u8; + cdb[4] = ((begin >> 8) & 0xFF) as u8; + cdb[5] = begin as u8; + cdb[8] = sectors as u8; + len = 10; + } + ReadCommand::ReadA8 => { + cdb[0] = 0xA8; + if self.fua { + cdb[1] |= 0x08; + } + cdb[3] = ((begin >> 16) & 0xFF) as u8; + cdb[4] = ((begin >> 8) & 0xFF) as u8; + cdb[5] = begin as u8; + cdb[9] = sectors as u8; + len = 12; + } + ReadCommand::D4_10 | ReadCommand::D4_12 | ReadCommand::D5 | ReadCommand::D8 => { + cdb[0] = match command { + ReadCommand::D4_10 | ReadCommand::D4_12 => 0xD4, + ReadCommand::D5 => 0xD5, + _ => 0xD8, + }; + if self.fua { + cdb[1] |= 0x08; + } + cdb[3] = ((begin >> 16) & 0xFF) as u8; + cdb[4] = ((begin >> 8) & 0xFF) as u8; + cdb[5] = begin as u8; + if matches!(command, ReadCommand::D4_12 | ReadCommand::D8) { + cdb[9] = sectors as u8; + len = 12; + } else { + cdb[8] = sectors as u8; + len = 10; + } + } + ReadCommand::MmcB10 + | ReadCommand::MmcB2_10 + | ReadCommand::MmcB2_F8 + | ReadCommand::MmcB6_F8 + | ReadCommand::MmcB4_F8 + | ReadCommand::Mmc2_F8 + | ReadCommand::Mmc3_F8 => { + cdb[0] = 0xBE; + cdb[1] = match command { + ReadCommand::MmcB10 | ReadCommand::MmcB2_F8 => 0x00, + ReadCommand::MmcB2_10 | ReadCommand::Mmc2_F8 => 0x02, + ReadCommand::MmcB6_F8 | ReadCommand::Mmc3_F8 => 0x06, + ReadCommand::MmcB4_F8 => 0x04, + _ => 0, + }; + cdb[3] = ((begin >> 16) & 0xFF) as u8; + cdb[4] = ((begin >> 8) & 0xFF) as u8; + cdb[5] = begin as u8; + cdb[8] = sectors as u8; + cdb[9] = if matches!(command, ReadCommand::MmcB10 | ReadCommand::MmcB2_10) { + 0x10 + } else { + 0xF8 + }; + len = 12; + } + ReadCommand::Msf10 | ReadCommand::MsfF8 | ReadCommand::Msf3F8 => { + cdb[0] = 0xB9; + cdb[1] = if command == ReadCommand::Msf3F8 { 4 } else { 0 }; + let (mut m, mut s, mut f) = (0u8, 0u8, 0u8); + lba_to_msf(begin, &mut m, &mut s, &mut f); + cdb[3] = m; + cdb[4] = s; + cdb[5] = f; + let (mut m, mut s, mut f) = (0u8, 0u8, 0u8); + lba_to_msf(begin + sectors, &mut m, &mut s, &mut f); + cdb[6] = m; + cdb[7] = s; + cdb[8] = f; + cdb[9] = if command == ReadCommand::Msf10 { + 0x10 + } else { + 0xF8 + }; + len = 12; + use_msf = true; + } + } + let _ = use_msf; + cdb[1] |= ((self.lun as u8) & 0x07) << 5; + let actual = self.sg_command(&cdb[..len], Some(&mut buf[..need]), SG_DXFER_FROM_DEV)?; + let count = count_2352_bytes(&buf[..need]); + let by_count = count / CD_FRAMESIZE_RAW; + let by_actual = actual / CD_FRAMESIZE_RAW; + Ok(by_count.min(by_actual)) + } + + fn scsi_read_map( + &mut self, + command: ReadCommand, + p: Option<&mut [u8]>, + begin: i64, + sectors: i64, + ) -> Result { + let mut requested = sectors.min(self.nsectors).max(1); + let mut retry_count = 0; + let mut scratch; + let buffer: &mut [u8] = match p { + Some(b) => b, + None => { + scratch = vec![0u8; (requested * CD_FRAMESIZE_RAW as i64) as usize]; + &mut scratch + } + }; + if buffer.len() < (requested * CD_FRAMESIZE_RAW as i64) as usize { + requested = (buffer.len() / CD_FRAMESIZE_RAW) as i64; + } + loop { + match self.read_once(command, begin, requested, buffer) { + Ok(n) if n > 0 => return Ok(n), + Ok(_) => {} + Err(e) => { + let errno = e.errno; + if errno == libc::EINTR { + continue; + } + if errno == libc::ENOMEM { + if requested == 1 { + return Err(ReadError::new(libc::ENOMEM, "Kernel memory error")); + } + requested -= 1; + continue; + } + if errno == libc::ENOMEDIUM { + return Err(ReadError::no_medium()); + } + if requested == 1 && retry_count > MAX_RETRIES - 1 { + return Err(ReadError::new( + errno, + format!("Unable to access sector {}", begin), + )); + } + self.reset_scsi(); + } + } + retry_count += 1; + if requested == 1 && retry_count > MAX_RETRIES { + return Err(ReadError::new( + libc::EIO, + "Unknown, unrecoverable error reading data", + )); + } + if requested > 1 { + requested /= 2; + } + let _ = self.enable_cdda(); + } + } + + fn reset_scsi(&mut self) { + unsafe { + let _ = libc::ioctl(self.fd, SG_SCSI_RESET, 1); + } + } + + fn read_raw( + &mut self, + p: Option<&mut [u8]>, + begin: i64, + sectors: i64, + ) -> Result { + if !self.opened { + return Err(ReadError::closed()); + } + if sectors <= 0 { + return Ok(0); + } + match self.interface { + InterfaceKind::SgIo => self.scsi_read_map(self.read_cmd, p, begin, sectors), + InterfaceKind::Cooked => self.cooked_read(p, begin, sectors), + InterfaceKind::Image => unreachable!(), + } + } + + fn cooked_read( + &mut self, + p: Option<&mut [u8]>, + begin: i64, + sectors: i64, + ) -> Result { + let mut requested = sectors.min(self.nsectors).max(1); + let mut retry = 0; + let mut scratch; + let buf: &mut [u8] = match p { + Some(b) => b, + None => { + scratch = vec![0u8; (requested * CD_FRAMESIZE_RAW as i64) as usize]; + &mut scratch + } + }; + if buf.len() < (requested * CD_FRAMESIZE_RAW as i64) as usize { + requested = (buf.len() / CD_FRAMESIZE_RAW) as i64; + } + loop { + let mut arg = CdromReadAudio { + addr: CdromAddr { + lba: begin as c_int, + }, + addr_format: CDROM_LBA, + nframes: requested as c_int, + buf: buf.as_mut_ptr(), + }; + let rc = unsafe { libc::ioctl(self.fd, CDROMREADAUDIO, &mut arg as *mut _) }; + if rc == 0 { + return Ok(requested as usize); + } + let err = io::Error::last_os_error(); + let errno = err.raw_os_error().unwrap_or(libc::EIO); + if errno == libc::ENXIO || errno == libc::EBADF || errno == libc::ENOMEDIUM { + return Err(ReadError::no_medium()); + } + if errno == libc::ENOMEM && requested == 1 { + return Err(ReadError::new(libc::ENOMEM, "Kernel memory error")); + } + if requested == 1 && retry > MAX_RETRIES - 1 { + return Err(ReadError::new( + errno, + format!("Unable to access sector {}", begin), + )); + } + retry += 1; + if retry > MAX_RETRIES { + return Err(ReadError::new( + libc::EIO, + "Unknown, unrecoverable error reading data", + )); + } + if retry > 4 && requested > 1 { + requested = requested * 3 / 4; + } + } + } + + fn verify_read_command(&mut self) -> Result<(), String> { + let mut buf = vec![0u8; CD_FRAMESIZE_RAW]; + let mut tried_any = false; + let tried = [self.read_cmd]; + for command in tried { + if self.try_read_command(command, &mut buf) { + self.read_cmd = command; + return Ok(()); + } + tried_any = true; + } + if tried_any { + let all = [ + ReadCommand::Read28, + ReadCommand::ReadA8, + ReadCommand::MmcB10, + ReadCommand::MmcB2_10, + ReadCommand::MmcB6_F8, + ReadCommand::MmcB2_F8, + ReadCommand::MmcB4_F8, + ReadCommand::Mmc2_F8, + ReadCommand::Mmc3_F8, + ReadCommand::Msf10, + ReadCommand::MsfF8, + ReadCommand::Msf3F8, + ReadCommand::D4_10, + ReadCommand::D4_12, + ReadCommand::D5, + ReadCommand::D8, + ]; + for command in all { + if self.try_read_command(command, &mut buf) { + self.read_cmd = command; + return Ok(()); + } + } + } + Err( + "Unable to find any suitable command set from probe; drive probably not CDDA capable" + .to_string(), + ) + } + + fn try_read_command(&mut self, command: ReadCommand, buf: &mut [u8]) -> bool { + if self.enable_cdda().is_err() { + return false; + } + for t in 1..=self.toc.tracks { + if self.toc.track_audiop(t) == 1 { + let first = self.toc.track_firstsector(t); + let last = self.toc.track_lastsector(t); + let sector = (first + last) >> 1; + if let Ok(n) = self.read_once(command, sector, 1, buf) { + if n > 0 + && count_2352_bytes(buf) == CD_FRAMESIZE_RAW + && buf.iter().any(|b| *b != 0) + { + return true; + } + } + } + } + false + } + + fn verify_cooked(&mut self) -> Result<(), String> { + let mut buf = vec![0u8; CD_FRAMESIZE_RAW]; + for t in 1..=self.toc.tracks { + if self.toc.track_audiop(t) == 1 { + let first = self.toc.track_firstsector(t); + let last = self.toc.track_lastsector(t); + let sector = (first + last) >> 1; + if let Ok(n) = self.cooked_read(Some(&mut buf), sector, 1) { + if n > 0 { + return Ok(()); + } + } + } + } + Err("Unable to read any data; drive probably not CDDA capable".to_string()) + } + + fn detect_endian(&mut self) -> i32 { + self.bigendianp = -1; + let mut votes_lsb = 0.0f64; + let mut votes_msb = 0.0f64; + let mut checked = 0; + let mut buf = vec![0u8; 5 * CD_FRAMESIZE_RAW]; + for t in 1..=self.toc.tracks { + if self.toc.track_audiop(t) != 1 { + continue; + } + let mut first = self.toc.track_firstsector(t); + let last = self.toc.track_lastsector(t); + let mut beginsec = 0; + let mut zeroflag = true; + while first + 5 <= last { + if self + .read_raw(Some(&mut buf), first, 5) + .map(|n| n > 0) + .unwrap_or(false) + { + for s in 0..5 { + let off = s * CD_FRAMESIZE_RAW; + for j in 460..460 + 128 { + if buf[off + j * 2] != 0 || buf[off + j * 2 + 1] != 0 { + zeroflag = false; + beginsec = s; + break; + } + } + if !zeroflag { + break; + } + } + if !zeroflag { + break; + } + first += 5; + } else { + return -1; + } + } + if !zeroflag { + let base = beginsec * CD_FRAMESIZE_RAW + 460 * 2; + let lsb_energy = fft_energy(&buf[base..base + 512], false); + let msb_energy = fft_energy(&buf[base..base + 512], true); + if lsb_energy < msb_energy { + votes_lsb += msb_energy / lsb_energy.max(1.0); + checked += 1; + } else if lsb_energy > msb_energy { + votes_msb += lsb_energy / msb_energy.max(1.0); + checked += 1; + } + } + if checked == 5 && (votes_lsb == 0.0 || votes_msb == 0.0) { + break; + } + } + if votes_lsb > votes_msb { + 0 + } else if votes_msb > votes_lsb { + 1 + } else if !host_big_endian() { + 0 + } else { + 1 + } + } +} + +fn fft_energy(bytes: &[u8], big: bool) -> f64 { + let mut re = vec![0.0f64; 128]; + let mut im = vec![0.0f64; 128]; + for i in 0..128 { + let b0 = bytes[i * 2]; + let b1 = bytes[i * 2 + 1]; + let v = if big { + i16::from_be_bytes([b0, b1]) + } else { + i16::from_le_bytes([b0, b1]) + }; + re[i] = v as f64; + } + fft(&mut re, &mut im); + let mut energy = 0.0; + for i in 1..64 { + energy += (re[i] * re[i] + im[i] * im[i]).sqrt(); + } + energy +} + +fn fft(re: &mut [f64], im: &mut [f64]) { + let n = re.len(); + let mut j = 0usize; + for i in 1..n { + let mut bit = n >> 1; + while j & bit != 0 { + j ^= bit; + bit >>= 1; + } + j ^= bit; + if i < j { + re.swap(i, j); + im.swap(i, j); + } + } + let mut len = 2; + while len <= n { + let ang = -2.0 * std::f64::consts::PI / len as f64; + let wlen_re = ang.cos(); + let wlen_im = ang.sin(); + let mut i = 0; + while i < n { + let mut w_re = 1.0; + let mut w_im = 0.0; + for k in 0..len / 2 { + let u_re = re[i + k]; + let u_im = im[i + k]; + let v_re = re[i + k + len / 2] * w_re - im[i + k + len / 2] * w_im; + let v_im = re[i + k + len / 2] * w_im + im[i + k + len / 2] * w_re; + re[i + k] = u_re + v_re; + im[i + k] = u_im + v_im; + re[i + k + len / 2] = u_re - v_re; + im[i + k + len / 2] = u_im - v_im; + let nw_re = w_re * wlen_re - w_im * wlen_im; + let nw_im = w_re * wlen_im + w_im * wlen_re; + w_re = nw_re; + w_im = nw_im; + } + i += len; + } + len <<= 1; + } +} + +fn count_2352_bytes(buf: &[u8]) -> usize { + for i in (0..buf.len()).rev() { + if buf[i] != 0x7F { + return ((i + 3) >> 2) << 2; + } + } + 0 +} + +fn check_sbp(status: u8, sense: &[u8]) -> ScsiError { + let key = sense.get(2).copied().unwrap_or(0) & 0x0F; + let asc = sense.get(12).copied().unwrap_or(0); + let ascq = sense.get(13).copied().unwrap_or(0); + if status == 8 { + return ScsiError { + errno: libc::EBUSY, + key, + asc, + ascq, + transport: "device busy", + }; + } + let (errno, transport) = match key { + 0 => (libc::EIO, "unknown"), + 2 => (libc::ENOMEDIUM, "device not ready"), + 3 if asc == 0x0C && ascq == 0x09 => (libc::EIO, "loss of streaming"), + 3 => (libc::EIO, "medium error"), + 4 => (libc::EIO, "hardware fault"), + 5 => (libc::EINVAL, "illegal request"), + _ => (libc::EIO, "unknown"), + }; + ScsiError { + errno, + key, + asc, + ascq, + transport, + } +} + +fn open_device(device: &str) -> Result { + let mut last = String::new(); + for flags in [ + libc::O_RDWR | libc::O_NONBLOCK, + libc::O_RDONLY | libc::O_NONBLOCK, + ] { + let file = OpenOptions::new() + .read(true) + .write(flags & libc::O_RDWR != 0) + .custom_flags(flags) + .open(device); + match file { + Ok(f) => return Ok(f.into_raw_fd()), + Err(e) => last = e.to_string(), + } + } + Err(format!("Could not open {}: {}", device, last)) +} + +fn open_device_cooked(device: &str) -> Result { + OpenOptions::new() + .read(true) + .custom_flags(libc::O_RDONLY | libc::O_NONBLOCK) + .open(device) + .map(|f| f.into_raw_fd()) + .map_err(|e| format!("Could not open {}: {}", device, e)) +} + +fn buffer_sectors() -> i64 { + if std::env::var_os("CDDA_IGNORE_BUFSIZE_LIMIT").is_some() { + 100 + } else { + // The original caps its SG buffer at 64 KiB. + ((64 * 1024) / CD_FRAMESIZE_RAW) as i64 + } +} + +impl Drive for LinuxDrive { + fn toc(&self) -> &DiscToc { + &self.toc + } + fn toc_mut(&mut self) -> &mut DiscToc { + &mut self.toc + } + fn opened(&self) -> bool { + self.opened + } + fn open_disc(&mut self) -> Result<(), String> { + if self.opened { + return Ok(()); + } + match self.interface { + InterfaceKind::SgIo => { + self.read_toc_sgio()?; + self.fixup_toc_multisession(); + self.nsectors = buffer_sectors(); + self.opened = true; + if let Err(e) = self.verify_read_command() { + self.opened = false; + return Err(e); + } + if !self.is_atapi && !self.is_mmc { + self.bigendianp = self.detect_endian(); + } else { + self.bigendianp = 0; + } + } + InterfaceKind::Cooked => { + self.read_toc_cooked()?; + self.opened = true; + if let Err(e) = self.verify_cooked() { + self.opened = false; + return Err(e); + } + } + InterfaceKind::Image => { + self.opened = true; + } + } + Ok(()) + } + fn read_audio( + &mut self, + mut buffer: Option<&mut [u8]>, + begin: i64, + sectors: i64, + ) -> Result { + let n = self.read_raw(buffer.as_deref_mut(), begin, sectors)?; + if let Some(buf) = buffer { + if n > 0 && self.bigendianp >= 0 && (self.bigendianp == 1) != host_big_endian() { + let len = (n * CD_FRAMESIZE_RAW).min(buf.len()); + swap_raw16(&mut buf[..len]); + } + } + Ok(n) + } + fn read_audio_raw( + &mut self, + buffer: Option<&mut [u8]>, + begin: i64, + sectors: i64, + ) -> Result { + self.read_raw(buffer, begin, sectors) + } + fn read_timed(&mut self, buffer: Option<&mut [u8]>, begin: i64, sectors: i64) -> TimedRead { + let started = Instant::now(); + let result = self.read_audio(buffer, begin, sectors); + let elapsed = started.elapsed().as_millis() as i32; + TimedRead { + result, + milliseconds: elapsed, + } + } + fn speed_set(&mut self, speed: i32) -> i32 { + if self.interface == InterfaceKind::Cooked { + let rc = unsafe { libc::ioctl(self.fd, CDROM_SELECT_SPEED, speed) }; + return if rc == 0 { 0 } else { -405 }; + } + let mut cdb = [0u8; 12]; + cdb[0] = 0xBB; + cdb[2] = 0xFF; + cdb[3] = 0xFF; + if speed >= 0 { + let v = (speed as i64 * 44100 * 4 / 1024).clamp(0, 0xFFFF); + cdb[2] = ((v >> 8) & 0xFF) as u8; + cdb[3] = (v & 0xFF) as u8; + } + match self.sg_command(&cdb, None, SG_DXFER_NONE) { + Ok(_) => 0, + Err(_) => -405, + } + } + fn close(&mut self) { + if self.fd >= 0 { + unsafe { + let _ = libc::close(self.fd); + } + self.fd = -1; + } + self.opened = false; + } + fn drive_model(&self) -> &str { + &self.model + } + fn bigendianp(&self) -> i32 { + self.bigendianp + } + fn set_bigendianp(&mut self, value: i32) { + self.bigendianp = value; + } + fn nsectors(&self) -> i64 { + self.nsectors + } + fn set_nsectors(&mut self, value: i64) { + self.nsectors = value; + } + fn interface(&self) -> InterfaceKind { + self.interface + } +} + +impl Drop for LinuxDrive { + fn drop(&mut self) { + self.close(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn msf_conversion() { + let (mut m, mut s, mut f) = (0, 0, 0); + lba_to_msf(0, &mut m, &mut s, &mut f); + assert_eq!((m, s, f), (0, 2, 0)); + lba_to_msf(75, &mut m, &mut s, &mut f); + assert_eq!((m, s, f), (0, 3, 0)); + lba_to_msf(4500, &mut m, &mut s, &mut f); + assert_eq!((m, s, f), (1, 2, 0)); + } + + #[test] + fn fill_byte_counting() { + let mut buf = vec![0x7Fu8; 2352 * 2]; + assert_eq!(count_2352_bytes(&buf), 0); + buf[10] = 0; + assert_eq!(count_2352_bytes(&buf), 12); + for b in buf.iter_mut() { + *b = 0; + } + assert_eq!(count_2352_bytes(&buf), buf.len()); + } + + #[test] + fn sense_mapping() { + let mut sense = [0u8; SG_MAX_SENSE]; + sense[2] = 0x02; + assert_eq!(check_sbp(0, &sense).errno, libc::ENOMEDIUM); + sense[2] = 0x03; + sense[12] = 0x0C; + sense[13] = 0x09; + assert_eq!(check_sbp(0, &sense).transport, "loss of streaming"); + assert_eq!(check_sbp(8, &sense).errno, libc::EBUSY); + } +} diff --git a/crates/core/src/device/mod.rs b/crates/core/src/device/mod.rs new file mode 100644 index 0000000..249ecfd --- /dev/null +++ b/crates/core/src/device/mod.rs @@ -0,0 +1,422 @@ +//! Common CD-ROM device abstractions and TOC helpers. +//! +//! The layout and behaviour intentionally mirror cdparanoia 10.2's +//! `cdda_interface` API. Device specific backends live in sibling modules. + +pub mod discover; +pub mod image; +pub mod linux; + +use std::fmt; + +pub const CD_FRAMESIZE_RAW: usize = 2352; +pub const CD_FRAMESIZE: usize = 2048; +pub const CD_FRAMEWORDS: i64 = 1176; +pub const CD_FRAMESAMPLES: usize = CD_FRAMESIZE_RAW / 4; +pub const MAXTRK: usize = 100; + +/// Error codes used by the original cdda_interface library. +pub mod error { + pub const DEVICE_NOT_OPEN: i64 = -400; + pub const INVALID_TRACK: i64 = -401; + pub const TRACK_NOT_AUDIO: i64 = -402; + pub const NO_AUDIO_TRACKS: i64 = -403; + pub const NO_MEDIUM: i64 = -404; + pub const OPTION_NOT_SUPPORTED: i64 = -405; + pub const INTERFACE_NOT_SUPPORTED: i64 = -100; + pub const PERMISSION_IOCTL: i64 = -102; + pub const PERMISSION_DATA: i64 = -103; + pub const KERNEL_MEMORY: i64 = -300; + pub const CANT_SET_CDDA: i64 = -1; + pub const CANT_READ_LEADOUT: i64 = -2; + pub const ILLEGAL_TRACKS: i64 = -3; + pub const CANT_READ_TOC_HEADER: i64 = -4; + pub const CANT_READ_TOC_ENTRY: i64 = -5; + pub const CANT_READ_ANY_DATA: i64 = -6; + pub const UNKNOWN_READ: i64 = -7; + pub const CANT_IDENTIFY: i64 = -8; + pub const ILLEGAL_TOC: i64 = -9; + pub const UNREADABLE_SECTOR: i64 = -10; +} + +#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)] +pub struct TocEntry { + pub flags: u8, + pub track: u8, + pub start_sector: i64, +} + +impl TocEntry { + pub fn audio(&self) -> bool { + self.flags & 0x04 == 0 + } + pub fn copyp(&self) -> bool { + self.flags & 0x02 != 0 + } + pub fn preemp(&self) -> bool { + self.flags & 0x01 != 0 + } + pub fn channels(&self) -> i32 { + if self.flags & 0x08 != 0 { + 4 + } else { + 2 + } + } +} + +#[derive(Clone, Debug)] +pub struct DiscToc { + pub entries: [TocEntry; MAXTRK], + pub tracks: i32, + pub cd_extra: i32, +} + +impl Default for DiscToc { + fn default() -> Self { + Self { + entries: [TocEntry::default(); MAXTRK], + tracks: 0, + cd_extra: 0, + } + } +} + +impl DiscToc { + pub fn track_firstsector(&self, track: i32) -> i64 { + if self.tracks <= 0 { + return error::DEVICE_NOT_OPEN; + } + if track == 0 { + if self.entries[0].start_sector == 0 { + return error::INVALID_TRACK; + } + return 0; + } + if track < 0 || track as usize > self.tracks as usize { + return error::INVALID_TRACK; + } + self.entries[track as usize - 1].start_sector + } + + pub fn track_lastsector(&self, track: i32) -> i64 { + if self.tracks <= 0 { + return error::DEVICE_NOT_OPEN; + } + if track == 0 { + if self.entries[0].start_sector == 0 { + return error::INVALID_TRACK; + } + return self.entries[0].start_sector - 1; + } + if track < 1 || track as usize > self.tracks as usize { + return error::INVALID_TRACK; + } + self.entries[track as usize].start_sector - 1 + } + + pub fn disc_firstsector(&self) -> i64 { + if self.tracks <= 0 { + return error::DEVICE_NOT_OPEN; + } + for i in 0..self.tracks as usize { + if self.entries[i].audio() { + if i == 0 { + return 0; + } + return self.entries[i].start_sector; + } + } + error::NO_AUDIO_TRACKS + } + + pub fn disc_lastsector(&self) -> i64 { + if self.tracks <= 0 { + return error::DEVICE_NOT_OPEN; + } + for i in (0..self.tracks as usize).rev() { + if self.entries[i].audio() { + return self.entries[i + 1].start_sector - 1; + } + } + error::NO_AUDIO_TRACKS + } + + pub fn sector_gettrack(&self, sector: i64) -> i32 { + if self.tracks <= 0 { + return error::DEVICE_NOT_OPEN as i32; + } + if sector < self.entries[0].start_sector { + return 0; + } + for i in 0..self.tracks as usize { + if self.entries[i].start_sector <= sector && self.entries[i + 1].start_sector > sector { + return (i + 1) as i32; + } + } + error::INVALID_TRACK as i32 + } + + pub fn track_bitmap(&self, track: i32, bit: u8, set: i32, clear: i32) -> i32 { + let track = if track == 0 { 1 } else { track }; + if track < 1 || track as usize > self.tracks as usize { + return error::INVALID_TRACK as i32; + } + if self.entries[track as usize - 1].flags & bit != 0 { + set + } else { + clear + } + } + + pub fn track_channels(&self, track: i32) -> i32 { + self.track_bitmap(track, 8, 4, 2) + } + pub fn track_audiop(&self, track: i32) -> i32 { + self.track_bitmap(track, 4, 0, 1) + } + pub fn track_copyp(&self, track: i32) -> i32 { + self.track_bitmap(track, 2, 1, 0) + } + pub fn track_preemp(&self, track: i32) -> i32 { + self.track_bitmap(track, 1, 1, 0) + } + + pub fn is_audio_index(&self, idx: usize, tracks: usize) -> bool { + if idx >= tracks { + return false; + } + self.entries[idx].audio() + } + + /// Reproduces `FixupTOC`'s non-ioctl repairs. + pub fn fixup(&mut self, tracks: usize) { + for j in 0..tracks { + if self.entries[j].start_sector < 0 { + self.entries[j].start_sector = 0; + } + if j + 1 < tracks && self.entries[j].start_sector > self.entries[j + 1].start_sector { + self.entries[j].start_sector = 0; + } + } + if tracks == 0 { + return; + } + let mut last = self.entries[0].start_sector; + for j in 1..tracks { + if self.entries[j].start_sector < last { + self.entries[j].start_sector = last; + } + last = self.entries[j].start_sector; + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum InterfaceKind { + SgIo, + Cooked, + Image, +} + +#[derive(Debug, Clone)] +pub struct ReadError { + pub errno: i32, + pub message: String, +} + +impl ReadError { + pub fn new(errno: i32, message: impl Into) -> Self { + Self { + errno, + message: message.into(), + } + } + pub fn no_medium() -> Self { + Self::new(libc::ENOMEDIUM, "No medium present") + } + pub fn closed() -> Self { + Self::new(libc::EBADF, "Device not open") + } + pub fn io(message: impl Into) -> Self { + Self::new(libc::EIO, message) + } +} + +impl fmt::Display for ReadError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} (errno {})", self.message, self.errno) + } +} + +#[derive(Debug, Clone)] +pub struct TimedRead { + pub result: Result, + pub milliseconds: i32, +} + +/// Backend interface used by both the command line program and the C ABI. +/// +/// Positions are absolute CD sectors (not sample positions). Audio data is +/// returned in host byte order, exactly like `cdda_read`. +pub trait Drive { + fn toc(&self) -> &DiscToc; + fn toc_mut(&mut self) -> &mut DiscToc; + fn opened(&self) -> bool; + /// Perform the media-dependent part of opening a drive (TOC read, + /// CDDA capability probe, endianness detection). Identify-only + /// operations leave this undone, just like cdda_identify/cdda_open. + fn open_disc(&mut self) -> Result<(), String> { + Ok(()) + } + fn read_audio( + &mut self, + buffer: Option<&mut [u8]>, + begin: i64, + sectors: i64, + ) -> Result; + /// Low-level read without host-endian conversion, matching the original + /// `cdrom_drive::read_audio` function pointer. + fn read_audio_raw( + &mut self, + buffer: Option<&mut [u8]>, + begin: i64, + sectors: i64, + ) -> Result { + self.read_audio(buffer, begin, sectors) + } + fn read_timed(&mut self, buffer: Option<&mut [u8]>, begin: i64, sectors: i64) -> TimedRead; + fn speed_set(&mut self, speed: i32) -> i32; + fn close(&mut self); + fn drive_model(&self) -> &str; + fn bigendianp(&self) -> i32; + fn set_bigendianp(&mut self, value: i32); + fn nsectors(&self) -> i64; + fn set_nsectors(&mut self, value: i64); + fn interface(&self) -> InterfaceKind; + fn cdda_version(&self) -> &'static str { + crate::VERSIONNUM + } + + // Convenience TOC wrappers. + fn tracks(&self) -> i32 { + self.toc().tracks + } + fn track_firstsector(&self, track: i32) -> i64 { + self.toc().track_firstsector(track) + } + fn track_lastsector(&self, track: i32) -> i64 { + self.toc().track_lastsector(track) + } + fn disc_firstsector(&self) -> i64 { + self.toc().disc_firstsector() + } + fn disc_lastsector(&self) -> i64 { + self.toc().disc_lastsector() + } + fn sector_gettrack(&self, sector: i64) -> i32 { + self.toc().sector_gettrack(sector) + } + fn track_audiop(&self, track: i32) -> bool { + self.toc().track_audiop(track) == 1 + } + fn track_copyp(&self, track: i32) -> bool { + self.toc().track_copyp(track) == 1 + } + fn track_preemp(&self, track: i32) -> bool { + self.toc().track_preemp(track) == 1 + } + fn track_channels(&self, track: i32) -> i32 { + self.toc().track_channels(track) + } +} + +pub fn swap16(x: i16) -> i16 { + (x as u16).swap_bytes() as i16 +} + +pub fn host_big_endian() -> bool { + cfg!(target_endian = "big") +} + +/// Swap a buffer of host endian samples to the requested byte order. +pub fn swap_samples(buffer: &mut [i16], target_big_endian: bool) { + if host_big_endian() != target_big_endian { + for s in buffer { + *s = swap16(*s); + } + } +} + +/// Convert raw CDDA bytes into host endian i16 samples. +pub fn bytes_to_samples(bytes: &[u8]) -> Vec { + let mut out = Vec::with_capacity(bytes.len() / 2); + for chunk in bytes.chunks_exact(2) { + out.push(i16::from_ne_bytes([chunk[0], chunk[1]])); + } + out +} + +pub fn samples_to_bytes(samples: &[i16]) -> Vec { + let mut out = Vec::with_capacity(samples.len() * 2); + for s in samples { + out.extend_from_slice(&s.to_ne_bytes()); + } + out +} + +/// Swap bytes in a raw buffer between little and big endian 16-bit words. +pub fn swap_raw16(buffer: &mut [u8]) { + for chunk in buffer.chunks_exact_mut(2) { + chunk.swap(0, 1); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn toc() -> DiscToc { + let mut t = DiscToc::default(); + t.tracks = 2; + t.entries[0] = TocEntry { + flags: 0, + track: 1, + start_sector: 0, + }; + t.entries[1] = TocEntry { + flags: 0, + track: 2, + start_sector: 100, + }; + t.entries[2] = TocEntry { + flags: 4, + track: 0xAA, + start_sector: 200, + }; + t + } + + #[test] + fn toc_queries() { + let t = toc(); + assert_eq!(t.track_firstsector(1), 0); + assert_eq!(t.track_lastsector(1), 99); + assert_eq!(t.track_firstsector(2), 100); + assert_eq!(t.track_lastsector(2), 199); + assert_eq!(t.disc_firstsector(), 0); + assert_eq!(t.disc_lastsector(), 199); + assert_eq!(t.sector_gettrack(0), 1); + assert_eq!(t.sector_gettrack(99), 1); + assert_eq!(t.sector_gettrack(100), 2); + assert_eq!(t.track_audiop(1), 1); + assert_eq!(t.track_audiop(2), 1); + } + + #[test] + fn byte_conversion() { + let s = [1i16, -2, 300]; + let b = samples_to_bytes(&s); + assert_eq!(bytes_to_samples(&b), s); + } +} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs new file mode 100644 index 0000000..45f86b3 --- /dev/null +++ b/crates/core/src/lib.rs @@ -0,0 +1,27 @@ +#![allow( + clippy::too_many_arguments, + clippy::missing_safety_doc, + clippy::while_let_loop, + clippy::unnecessary_cast, + clippy::field_reassign_with_default +)] + +//! cdrustanoia core library. +//! +//! This is a Rust port of the cdparanoia III 10.2 interface and paranoia +//! libraries, plus the command line helper code. + +pub mod callback; +pub mod device; +pub mod paranoia; + +pub const VERSIONNUM: &str = "10.2"; +pub const VERSION: &str = "cdparanoia III release 10.2 (September 11, 2008)"; + +pub fn paranoia_version() -> &'static str { + VERSIONNUM +} + +pub fn cdda_version() -> &'static str { + VERSIONNUM +} diff --git a/crates/core/src/paranoia/block.rs b/crates/core/src/paranoia/block.rs new file mode 100644 index 0000000..9633463 --- /dev/null +++ b/crates/core/src/paranoia/block.rs @@ -0,0 +1,195 @@ +//! c_block/v_fragment/root data structures and vector operations. + +pub const FLAGS_EDGE: u8 = 0x1; +pub const FLAGS_UNREAD: u8 = 0x2; +pub const FLAGS_VERIFIED: u8 = 0x4; + +#[derive(Clone, Debug)] +pub struct CBlock { + pub vector: Vec, + pub flags: Option>, + pub begin: i64, + pub size: i64, + pub lastsector: bool, +} + +impl Default for CBlock { + fn default() -> Self { + Self::new() + } +} + +impl CBlock { + pub fn new() -> Self { + Self { + vector: Vec::new(), + flags: None, + begin: 0, + size: 0, + lastsector: false, + } + } + + pub fn from_parts(vector: Vec, begin: i64, lastsector: bool) -> Self { + let size = vector.len() as i64; + Self { + vector, + flags: None, + begin, + size, + lastsector, + } + } + + #[inline] + pub fn end(&self) -> i64 { + self.begin + self.size + } + + #[inline] + pub fn begin(&self) -> i64 { + self.begin + } + + #[inline] + pub fn size(&self) -> i64 { + self.size + } + + pub fn insert(&mut self, pos: i64, data: &[i16]) { + let vs = self.size; + if pos < 0 || pos > vs { + return; + } + let pos = pos as usize; + self.vector.splice(pos..pos, data.iter().copied()); + self.size += data.len() as i64; + if let Some(flags) = self.flags.as_mut() { + let n = data.len(); + let pos = pos.min(flags.len()); + flags.splice(pos..pos, std::iter::repeat_n(0, n)); + } + } + + pub fn remove(&mut self, cutpos: i64, mut cutsize: i64) { + let vs = self.size; + if cutpos < 0 || cutpos > vs { + return; + } + if cutpos + cutsize > vs { + cutsize = vs - cutpos; + } + if cutsize < 0 { + cutsize = vs - cutpos; + } + if cutsize < 1 { + return; + } + let pos = cutpos as usize; + let n = cutsize as usize; + self.vector.drain(pos..pos + n); + self.size -= cutsize; + if let Some(flags) = self.flags.as_mut() { + let pos = pos.min(flags.len()); + let end = (pos + n).min(flags.len()); + flags.drain(pos..end); + } + } + + pub fn overwrite(&mut self, pos: i64, data: &[i16]) { + if pos < 0 { + return; + } + let mut size = data.len() as i64; + if pos + size > self.size { + size = self.size - pos; + } + if size <= 0 { + return; + } + let p = pos as usize; + let n = size as usize; + self.vector[p..p + n].copy_from_slice(&data[..n]); + } + + pub fn append(&mut self, data: &[i16]) { + self.vector.extend_from_slice(data); + self.size += data.len() as i64; + if let Some(flags) = self.flags.as_mut() { + flags.extend(std::iter::repeat_n(0, data.len())); + } + } + + pub fn remove_front(&mut self, cut: i64) { + if cut <= 0 { + return; + } + let n = (cut as usize).min(self.vector.len()); + self.vector.drain(..n); + self.size -= n as i64; + self.begin += n as i64; + if let Some(flags) = self.flags.as_mut() { + let n = n.min(flags.len()); + flags.drain(..n); + } + } + + pub fn flag_at(&self, abs_word: i64) -> u8 { + let idx = abs_word - self.begin; + if idx < 0 { + return 0; + } + self.flags + .as_ref() + .and_then(|f| f.get(idx as usize).copied()) + .unwrap_or(0) + } +} + +#[derive(Clone, Debug)] +pub struct VFragment { + pub vector: Vec, + pub begin: i64, + pub size: i64, + pub lastsector: bool, +} + +impl VFragment { + pub fn end(&self) -> i64 { + self.begin + self.size + } +} + +#[derive(Clone, Debug, Default)] +pub struct RootBlock { + pub returned_limit: i64, + pub lastsector: bool, + pub vector: Option, + pub silence_flag: bool, + pub silence_begin: i64, +} + +impl RootBlock { + #[inline] + pub fn begin(&self) -> i64 { + self.vector.as_ref().map(|v| v.begin).unwrap_or(-1) + } + #[inline] + pub fn end(&self) -> i64 { + self.vector.as_ref().map(|v| v.end()).unwrap_or(-1) + } + #[inline] + pub fn is_empty(&self) -> bool { + self.vector.is_none() + } +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct Offsets { + pub offpoints: i64, + pub newpoints: i64, + pub offaccum: i64, + pub offdiff: i64, + pub offmin: i64, + pub offmax: i64, +} diff --git a/crates/core/src/paranoia/mod.rs b/crates/core/src/paranoia/mod.rs new file mode 100644 index 0000000..2df4b55 --- /dev/null +++ b/crates/core/src/paranoia/mod.rs @@ -0,0 +1,2043 @@ +//! Rust port of the cdparanoia III 10.2 verification/reconstruction engine. + +pub mod block; +pub mod sort; + +use crate::callback::*; +use crate::device::{Drive, CD_FRAMESIZE_RAW, CD_FRAMEWORDS}; +use block::*; +use sort::SortIndex; +use std::collections::VecDeque; + +pub const MIN_WORDS_OVERLAP: i64 = 64; +pub const MIN_WORDS_SEARCH: i64 = 64; +pub const MIN_WORDS_RIFT: i64 = 16; +pub const MAX_SECTOR_OVERLAP: i64 = 32; +pub const MIN_SECTOR_EPSILON: i64 = 128; +pub const JIGGLE_MODULO: i64 = 15; +pub const MIN_SILENCE_BOUNDARY: i64 = 1024; +pub const CACHEMODEL_SECTORS: i64 = 1200; +pub const OVERLAP_ADJ: i64 = MIN_WORDS_OVERLAP / 2 - 1; +pub const MIN_SEEK_MS: i32 = 6; + +pub const PARANOIA_MODE_FULL: i32 = 0xff; +pub const PARANOIA_MODE_DISABLE: i32 = 0; +pub const PARANOIA_MODE_VERIFY: i32 = 1; +pub const PARANOIA_MODE_FRAGMENT: i32 = 2; +pub const PARANOIA_MODE_OVERLAP: i32 = 4; +pub const PARANOIA_MODE_SCRATCH: i32 = 8; +pub const PARANOIA_MODE_REPAIR: i32 = 16; +pub const PARANOIA_MODE_NEVERSKIP: i32 = 32; + +const SEEK_SET: i32 = 0; +const SEEK_END: i32 = 2; + +pub type CallbackFn<'a> = dyn FnMut(i64, i32) + 'a; + +#[derive(Clone, Copy, Debug)] +struct SyncResult { + offset: i64, + begin: i64, + end: i64, +} + +pub struct Paranoia { + d: Box, + pub root: RootBlock, + cache: VecDeque, + cache_limit: usize, + fragments: Vec>, + sortcache: SortIndex, + pub cdcache_size: i64, + cdcache_begin: i64, + cdcache_end: i64, + jitter: i64, + pub enable: i32, + pub cursor: i64, + current_lastsector: i64, + current_firstsector: i64, + pub stage1: Offsets, + pub stage2: Offsets, + pub dynoverlap: i64, + pub dyndrift: i64, + stage2_adjust_pending: bool, +} + +// --------------------------------------------------------------------------- +// Matching primitives +// --------------------------------------------------------------------------- + +fn overlap( + buff_a: &[i16], + buff_b: &[i16], + offset_a: i64, + offset_b: i64, + size_a: i64, + size_b: i64, + ret_begin: Option<&mut i64>, + ret_end: Option<&mut i64>, +) -> i64 { + let mut begin_a = offset_a; + let mut begin_b = offset_b; + while begin_a >= 0 && begin_b >= 0 && buff_a[begin_a as usize] == buff_b[begin_b as usize] { + begin_a -= 1; + begin_b -= 1; + } + begin_a += 1; + let _ = begin_b; + + let mut end_a = offset_a; + let mut end_b = offset_b; + while end_a < size_a && end_b < size_b && buff_a[end_a as usize] == buff_b[end_b as usize] { + end_a += 1; + end_b += 1; + } + if let Some(b) = ret_begin { + *b = begin_a; + } + if let Some(e) = ret_end { + *e = end_a; + } + end_a - begin_a +} + +#[allow(clippy::too_many_arguments)] +fn overlap2( + buff_a: &[i16], + buff_b: &[i16], + flags_a: &[u8], + flags_b: &[u8], + offset_a: i64, + offset_b: i64, + size_a: i64, + size_b: i64, + ret_begin: Option<&mut i64>, + ret_end: Option<&mut i64>, +) -> i64 { + let mut begin_a = offset_a; + let mut begin_b = offset_b; + while begin_a >= 0 && begin_b >= 0 { + if buff_a[begin_a as usize] != buff_b[begin_b as usize] { + break; + } + if flags_a[begin_a as usize] & flags_b[begin_b as usize] & FLAGS_EDGE != 0 { + begin_a -= 1; + begin_b -= 1; + break; + } + if flags_a[begin_a as usize] & FLAGS_UNREAD != 0 + || flags_b[begin_b as usize] & FLAGS_UNREAD != 0 + { + break; + } + begin_a -= 1; + begin_b -= 1; + } + begin_a += 1; + let _ = begin_b; + + let mut end_a = offset_a; + let mut end_b = offset_b; + while end_a < size_a && end_b < size_b { + if buff_a[end_a as usize] != buff_b[end_b as usize] { + break; + } + if flags_a[end_a as usize] & flags_b[end_b as usize] & FLAGS_EDGE != 0 && end_a != begin_a { + break; + } + if flags_a[end_a as usize] & FLAGS_UNREAD != 0 + || flags_b[end_b as usize] & FLAGS_UNREAD != 0 + { + break; + } + end_a += 1; + end_b += 1; + } + if let Some(b) = ret_begin { + *b = begin_a; + } + if let Some(e) = ret_end { + *e = end_a; + } + end_a - begin_a +} + +#[allow(clippy::too_many_arguments)] +fn do_const_sync( + a: &CBlock, + b_samples: &[i16], + b_flags: Option<&[u8]>, + b_begin: i64, + pos_a: i64, + pos_b: i64, + begin: &mut i64, + end: &mut i64, + offset: &mut i64, +) -> i64 { + let flag_a = a.flags.as_deref(); + let ret = match (flag_a, b_flags) { + (_, None) => overlap( + &a.vector, + b_samples, + pos_a, + pos_b, + a.size, + b_samples.len() as i64, + Some(begin), + Some(end), + ), + (Some(fa), Some(fb)) => { + if fb.get(pos_b as usize).copied().unwrap_or(0) & FLAGS_UNREAD == 0 { + overlap2( + &a.vector, + b_samples, + fa, + fb, + pos_a, + pos_b, + a.size, + b_samples.len() as i64, + Some(begin), + Some(end), + ) + } else { + 0 + } + } + (None, Some(_)) => 0, + }; + + if ret > MIN_WORDS_SEARCH { + *offset = (pos_a + a.begin) - (pos_b + b_begin); + *begin += a.begin; + *end += a.begin; + ret + } else { + 0 + } +} + +fn offset_add_stage1( + stage1: &mut Offsets, + dynoverlap: &mut i64, + value: i64, + callback: &mut dyn FnMut(i64, i32), +) { + if stage1.offpoints == -1 { + return; + } + stage1.offdiff += value.abs(); + stage1.offpoints += 1; + stage1.newpoints += 1; + stage1.offaccum += value; + if value < stage1.offmin { + stage1.offmin = value; + } + if value > stage1.offmax { + stage1.offmax = value; + } + if stage1.newpoints >= 10 { + adjust_stage1(stage1, dynoverlap, callback); + } +} + +fn adjust_stage1(stage1: &mut Offsets, dynoverlap: &mut i64, callback: &mut dyn FnMut(i64, i32)) { + if stage1.offpoints >= 10 { + *dynoverlap = if stage1.offpoints != 0 { + stage1.offdiff / stage1.offpoints * 3 + } else { + CD_FRAMEWORDS + }; + + let min_limit = (-(stage1.offmin as f64) * 1.5) as i64; + if *dynoverlap < min_limit { + *dynoverlap = min_limit; + } + let max_limit = (stage1.offmax as f64 * 1.5) as i64; + if *dynoverlap < max_limit { + *dynoverlap = max_limit; + } + *dynoverlap = (*dynoverlap).clamp(MIN_SECTOR_EPSILON, MAX_SECTOR_OVERLAP * CD_FRAMEWORDS); + callback(*dynoverlap, PARANOIA_CB_OVERLAP); + + if stage1.offpoints > 600 { + stage1.offpoints = (stage1.offpoints as f64 / 1.2) as i64; + stage1.offaccum = (stage1.offaccum as f64 / 1.2) as i64; + stage1.offdiff = (stage1.offdiff as f64 / 1.2) as i64; + } + stage1.offmin = 0; + stage1.offmax = 0; + stage1.newpoints = 0; + } +} + +#[allow(clippy::too_many_arguments)] +fn try_sort_sync( + sort: &mut SortIndex, + a_vector: &[i16], + a_begin: i64, + a_size: i64, + a_flags: Option<&[u8]>, + b: &CBlock, + post: i64, + begin: &mut i64, + end: &mut i64, + offset: &mut i64, + stage1: &mut Offsets, + dynoverlap: &mut i64, + callback: &mut dyn FnMut(i64, i32), +) -> bool { + let b_idx = post - b.begin; + if b_idx < 0 || b_idx as usize >= b.vector.len() { + return false; + } + let bflags = b.flags.as_deref(); + if let Some(f) = bflags { + let flag = f.get(b_idx as usize).copied().unwrap_or(0); + if flag & FLAGS_UNREAD != 0 { + return false; + } + } + + // Try the absolute zero-offset position first. + let zero_pos = post - a_begin; + if zero_pos >= 0 && zero_pos < a_size && b.vector[b_idx as usize] == a_vector[zero_pos as usize] + { + { + let mut bbegin = 0; + let mut bend = 0; + if do_const_sync( + b, + a_vector, + a_flags, + a_begin, + b_idx, + zero_pos, + &mut bbegin, + &mut bend, + offset, + ) != 0 + { + offset_add_stage1(stage1, dynoverlap, *offset, callback); + *begin = bbegin; + *end = bend; + return true; + } + } + } + + // Search for a matching sample within dynoverlap. + let search_post = post - a_begin; + if sort + .getmatch(search_post, *dynoverlap, b.vector[b_idx as usize]) + .is_some() + { + loop { + let pos_a = match sort.cursor_position() { + Some(p) => p, + None => break, + }; + let mut bbegin = 0; + let mut bend = 0; + if do_const_sync( + b, + a_vector, + a_flags, + a_begin, + b_idx, + pos_a, + &mut bbegin, + &mut bend, + offset, + ) != 0 + { + offset_add_stage1(stage1, dynoverlap, *offset, callback); + *begin = bbegin; + *end = bend; + return true; + } + if !sort.advance() { + break; + } + } + } + + *begin = -1; + *end = -1; + *offset = -1; + false +} + +// --------------------------------------------------------------------------- +// Stage 1 +// --------------------------------------------------------------------------- + +fn stage1_matched( + old: &mut CBlock, + new: &mut CBlock, + matchbegin: i64, + matchend: i64, + matchoffset: i64, + callback: &mut dyn FnMut(i64, i32), +) { + let old_adj_begin = matchbegin - old.begin; + let old_adj_end = matchend - old.begin; + let new_adj_begin = matchbegin - matchoffset - new.begin; + let new_adj_end = matchend - matchoffset - new.begin; + + let new_flag = |idx: i64| -> u8 { + new.flags + .as_ref() + .and_then(|f| f.get(idx as usize)) + .copied() + .unwrap_or(0) + }; + let old_flag = |idx: i64| -> u8 { + old.flags + .as_ref() + .and_then(|f| f.get(idx as usize)) + .copied() + .unwrap_or(0) + }; + + if matchbegin - matchoffset <= new.begin + || matchbegin <= old.begin + || new_flag(new_adj_begin) & FLAGS_EDGE != 0 + || old_flag(old_adj_begin) & FLAGS_EDGE != 0 + { + if matchoffset != 0 { + callback(matchbegin, PARANOIA_CB_FIXUP_EDGE); + } + } else { + callback(matchbegin, PARANOIA_CB_FIXUP_ATOM); + } + + if matchend - matchoffset >= new.end() + || new_flag(new_adj_end) & FLAGS_EDGE != 0 + || matchend >= old.end() + || old_flag(old_adj_end) & FLAGS_EDGE != 0 + { + if matchoffset != 0 { + callback(matchend, PARANOIA_CB_FIXUP_EDGE); + } + } else { + callback(matchend, PARANOIA_CB_FIXUP_ATOM); + } + + if let Some(flags) = new.flags.as_mut() { + let mut start = (new_adj_begin + OVERLAP_ADJ).max(0); + let stop = (new_adj_end - OVERLAP_ADJ).min(new.size); + if start < stop { + while start < stop { + flags[start as usize] |= FLAGS_VERIFIED; + start += 1; + } + } + } + if let Some(flags) = old.flags.as_mut() { + let mut start = (old_adj_begin + OVERLAP_ADJ).max(0); + let stop = (old_adj_end - OVERLAP_ADJ).min(old.size); + if start < stop { + while start < stop { + flags[start as usize] |= FLAGS_VERIFIED; + start += 1; + } + } + } +} + +fn iterate_stage1( + old: &mut CBlock, + new: &mut CBlock, + sort: &mut SortIndex, + stage1: &mut Offsets, + dynoverlap: &mut i64, + callback: &mut dyn FnMut(i64, i32), +) -> i64 { + let mut matchbegin = -1; + let mut matchend = -1; + let mut matchoffset = 0; + let searchbegin = old.begin.max(new.begin); + let searchend = old.end().min(new.end()); + if searchend - searchbegin <= 0 { + return 0; + } + let mut ret = 0; + let mut j = searchbegin; + while j < searchend { + let new_idx = j - new.begin; + let flag = new + .flags + .as_ref() + .and_then(|f| f.get(new_idx as usize)) + .copied() + .unwrap_or(0); + if flag & (FLAGS_VERIFIED | FLAGS_UNREAD) == 0 { + let a_flags = new.flags.as_deref(); + if try_sort_sync( + sort, + &new.vector, + new.begin, + new.size, + a_flags, + old, + j, + &mut matchbegin, + &mut matchend, + &mut matchoffset, + stage1, + dynoverlap, + callback, + ) { + let mut cosmetic = matchbegin - old.begin; + let end = matchend - old.begin; + while cosmetic < end && old.vector[cosmetic as usize] == 0 { + cosmetic += 1; + } + if cosmetic < end { + stage1_matched(old, new, matchbegin, matchend, matchoffset, callback); + } else { + stage1_matched(old, new, matchbegin, matchend, matchoffset, &mut |_, _| {}); + } + ret += 1; + if matchend - 1 > j { + j = matchend - 1; + } + } + } + j += 23; + } + ret +} + +// --------------------------------------------------------------------------- +// Gap/rift analysis (gap.c) +// --------------------------------------------------------------------------- + +fn overlap_r(buff_a: &[i16], buff_b: &[i16], offset_a: i64, offset_b: i64) -> i64 { + let mut begin_a = offset_a; + let mut begin_b = offset_b; + while begin_a >= 0 && begin_b >= 0 && buff_a[begin_a as usize] == buff_b[begin_b as usize] { + begin_a -= 1; + begin_b -= 1; + } + offset_a - begin_a +} + +fn overlap_f( + buff_a: &[i16], + buff_b: &[i16], + offset_a: i64, + offset_b: i64, + size_a: i64, + size_b: i64, +) -> i64 { + let mut end_a = offset_a; + let mut end_b = offset_b; + while end_a < size_a && end_b < size_b && buff_a[end_a as usize] == buff_b[end_b as usize] { + end_a += 1; + end_b += 1; + } + end_a - offset_a +} + +fn stutter_or_gap(a: &[i16], b: &[i16], mut off_a: i64, mut off_b: i64, mut gap: i64) -> bool { + if off_a < 0 { + off_b -= off_a; + gap += off_a; + off_a = 0; + } + if gap <= 0 { + return false; + } + let a_start = off_a as usize; + let b_start = off_b as usize; + let n = gap as usize; + if a_start + n > a.len() || b_start + n > b.len() { + return true; + } + a[a_start..a_start + n] != b[b_start..b_start + n] +} + +fn analyze_rift_f( + a: &[i16], + b: &[i16], + size_a: i64, + size_b: i64, + aoffset: i64, + boffset: i64, + match_a: &mut i64, + match_b: &mut i64, + match_c: &mut i64, +) { + let apast = size_a - aoffset; + let bpast = size_b - boffset; + *match_a = 0; + *match_b = 0; + *match_c = 0; + + let mut i = 1; + loop { + if i < bpast && overlap_f(a, b, aoffset, boffset + i, size_a, size_b) >= MIN_WORDS_RIFT { + *match_a = i; + break; + } + if i < apast { + if overlap_f(a, b, aoffset + i, boffset, size_a, size_b) >= MIN_WORDS_RIFT { + *match_b = i; + break; + } + if i < bpast + && overlap_f(a, b, aoffset + i, boffset + i, size_a, size_b) >= MIN_WORDS_RIFT + { + *match_c = i; + break; + } + } else if i >= bpast { + break; + } + i += 1; + } + + if *match_a == 0 && *match_b == 0 && *match_c == 0 { + return; + } + if *match_c != 0 { + return; + } + + if *match_a != 0 { + if stutter_or_gap(a, b, aoffset - *match_a, boffset, *match_a) { + return; + } + *match_b = -*match_a; + *match_a = 0; + } else { + if stutter_or_gap(b, a, boffset - *match_b, aoffset, *match_b) { + return; + } + *match_a = -*match_b; + *match_b = 0; + } +} + +#[allow(clippy::too_many_arguments)] +fn analyze_rift_r( + a: &[i16], + b: &[i16], + _size_a: i64, + _size_b: i64, + aoffset: i64, + boffset: i64, + match_a: &mut i64, + match_b: &mut i64, + match_c: &mut i64, +) { + let apast = aoffset + 1; + let bpast = boffset + 1; + *match_a = 0; + *match_b = 0; + *match_c = 0; + + let mut i = 1; + loop { + if i < bpast && overlap_r(a, b, aoffset, boffset - i) >= MIN_WORDS_RIFT { + *match_a = i; + break; + } + if i < apast { + if overlap_r(a, b, aoffset - i, boffset) >= MIN_WORDS_RIFT { + *match_b = i; + break; + } + if i < bpast && overlap_r(a, b, aoffset - i, boffset - i) >= MIN_WORDS_RIFT { + *match_c = i; + break; + } + } else if i >= bpast { + break; + } + i += 1; + } + + if *match_a == 0 && *match_b == 0 && *match_c == 0 { + return; + } + if *match_c != 0 { + return; + } + + if *match_a != 0 { + if stutter_or_gap(a, b, aoffset + 1, boffset - *match_a + 1, *match_a) { + return; + } + *match_b = -*match_a; + *match_a = 0; + } else { + if stutter_or_gap(b, a, boffset + 1, aoffset - *match_b + 1, *match_b) { + return; + } + *match_a = -*match_b; + *match_b = 0; + } +} + +fn analyze_rift_silence_f( + a: &[i16], + b: &[i16], + size_a: i64, + size_b: i64, + aoffset: i64, + boffset: i64, + match_a: &mut i64, + match_b: &mut i64, +) { + *match_a = -1; + *match_b = -1; + let size_a = size_a.min(aoffset + MIN_WORDS_RIFT); + let size_b = size_b.min(boffset + MIN_WORDS_RIFT); + let mut ao = aoffset + 1; + let mut bo = boffset + 1; + while ao < size_a { + if a[ao as usize] != a[(ao - 1) as usize] { + *match_a = 0; + break; + } + ao += 1; + } + while bo < size_b { + if b[bo as usize] != b[(bo - 1) as usize] { + *match_b = 0; + break; + } + bo += 1; + } +} + +// --------------------------------------------------------------------------- +// Stage 2 +// --------------------------------------------------------------------------- + +fn iterate_stage2( + sort: &mut SortIndex, + stage1: &mut Offsets, + dynoverlap: &mut i64, + root: &CBlock, + v: &VFragment, + callback: &mut dyn FnMut(i64, i32), +) -> Option { + if (v.end() + *dynoverlap).min(root.end()) - (v.begin - *dynoverlap).max(root.begin) <= 0 { + return None; + } + callback(v.begin, PARANOIA_CB_VERIFY); + + let mut fbv = v.begin.max(root.begin - *dynoverlap); + while fbv < v.end() && v.vector[(fbv - v.begin) as usize] == 0 { + fbv += 1; + } + if fbv == v.end() { + return None; + } + let fev = fbv + .saturating_add(256) + .min(root.end() + *dynoverlap) + .min(v.end()); + let searchend = (fev + *dynoverlap).min(root.end()); + let searchbegin = (fbv - *dynoverlap).max(root.begin); + sort.setup(&v.vector, v.begin, fbv, fev); + + let mut j = searchbegin; + while j < searchend { + while j < searchend && root.vector[(j - root.begin) as usize] == 0 { + j += 1; + } + if j == searchend { + break; + } + let mut begin = -1; + let mut end = -1; + let mut offset = 0; + if try_sort_sync( + sort, + &v.vector, + v.begin, + v.size, + None, + root, + j, + &mut begin, + &mut end, + &mut offset, + stage1, + dynoverlap, + callback, + ) { + let r = SyncResult { + begin, + end, + offset: -offset, + }; + if offset != 0 { + callback(r.begin, PARANOIA_CB_FIXUP_EDGE); + } + return Some(r); + } + j += 23; + } + None +} + +fn silence_test( + root: &mut CBlock, + returned_limit: i64, + silence_flag: &mut bool, + silence_begin: &mut i64, +) { + let end = root.size - 1; + let mut j = end - 1; + while j >= 0 { + if root.vector[j as usize] != 0 { + break; + } + j -= 1; + } + if j < 0 || end - j > MIN_SILENCE_BOUNDARY { + *silence_flag = true; + *silence_begin = root.begin + j + 1; + if *silence_begin < returned_limit { + *silence_begin = returned_limit; + } + } +} + +#[allow(clippy::too_many_arguments)] +fn silence_match( + root: &mut CBlock, + returned_limit: i64, + silence_flag: &mut bool, + silence_begin: &mut i64, + v: &VFragment, + dynoverlap: i64, + stage2: &mut Offsets, + stage2_pending: &mut bool, + callback: &mut dyn FnMut(i64, i32), +) -> bool { + let end = v.size; + if end < MIN_SILENCE_BOUNDARY { + return false; + } + let mut j = 0; + while j < end && v.vector[j as usize] == 0 { + j += 1; + } + if j < MIN_SILENCE_BOUNDARY { + return false; + } + j += v.begin; + + if v.begin >= root.end() && v.begin - dynoverlap < root.end() { + let addto = v.begin + MIN_SILENCE_BOUNDARY - root.end(); + if addto > 0 { + root.append(&vec![0i16; addto as usize]); + } + } + + let begin = v.begin.max(*silence_begin); + let end = j.min(root.end()); + + if begin < end { + if v.end() > root.end() { + let voff = begin - v.begin; + root.remove(begin - root.begin, root.size - (begin - root.begin)); + root.append(&v.vector[voff as usize..]); + } + add_stage2_value(stage2, 0, stage2_pending); + } else if j < begin { + let voff = j - v.begin; + if begin + v.size - voff > root.end() { + root.remove( + *silence_begin - root.begin, + root.size - (*silence_begin - root.begin), + ); + root.append(&v.vector[voff as usize..]); + } + add_stage2_value(stage2, end - begin, stage2_pending); + } else { + return false; + } + + *silence_flag = false; + silence_test(root, returned_limit, silence_flag, silence_begin); + let _ = callback; + true +} + +fn add_stage2_value(stage2: &mut Offsets, value: i64, pending: &mut bool) { + if stage2.offpoints == -1 { + return; + } + stage2.offdiff += value.abs(); + stage2.offpoints += 1; + stage2.newpoints += 1; + stage2.offaccum += value; + if value < stage2.offmin { + stage2.offmin = value; + } + if value > stage2.offmax { + stage2.offmax = value; + } + if stage2.newpoints >= 10 { + *pending = true; + } +} + +#[allow(clippy::too_many_arguments)] +fn stage2_each( + root: &mut CBlock, + root_returned_limit: &mut i64, + root_lastsector: &mut bool, + root_silence_flag: &mut bool, + root_silence_begin: &mut i64, + v: &mut VFragment, + sort: &mut SortIndex, + stage1: &mut Offsets, + dynoverlap: &mut i64, + stage2: &mut Offsets, + stage2_pending: &mut bool, + callback: &mut dyn FnMut(i64, i32), +) -> MergeOutcome { + let dyno = *dynoverlap / 2 * 2; + if v.size <= 0 || v.vector.is_empty() { + return MergeOutcome::Discarded; + } + + let sync = match iterate_stage2(sort, stage1, dynoverlap, root, v, callback) { + Some(s) => s, + None => { + return if v.end() + dyno < root.end() && !*root_silence_flag { + MergeOutcome::Discarded + } else { + MergeOutcome::Kept + }; + } + }; + + let mut begin = sync.begin - root.begin; + let mut end = sync.end - root.begin; + let mut offset = sync.begin + sync.offset - v.begin - begin; + let mut scratch: Option = None; + + // Leading rifts. + while begin + offset > 0 && begin > 0 { + let begin_l = begin + offset; + if scratch.is_none() { + scratch = Some(CBlock::from_parts(v.vector.clone(), v.begin, v.lastsector)); + } + let (mut match_a, mut match_b, mut match_c) = (0, 0, 0); + { + let l = scratch.as_ref().unwrap(); + analyze_rift_r( + &root.vector, + &l.vector, + root.size, + l.size, + begin - 1, + begin_l - 1, + &mut match_a, + &mut match_b, + &mut match_c, + ); + } + + if match_a != 0 { + if match_a > 0 { + callback(begin + root.begin - 1, PARANOIA_CB_FIXUP_DROPPED); + if root.begin + begin < *root_returned_limit { + break; + } + let l = scratch.as_ref().unwrap(); + let data: Vec = + l.vector[(begin_l - match_a) as usize..(begin_l) as usize].to_vec(); + root.insert(begin, &data); + offset -= match_a; + begin += match_a; + end += match_a; + } else { + callback(begin + root.begin - 1, PARANOIA_CB_FIXUP_DUPED); + if root.begin + begin + match_a < *root_returned_limit { + break; + } + root.remove(begin + match_a, -match_a); + offset -= match_a; + begin += match_a; + end += match_a; + } + } else if match_b != 0 { + if match_b > 0 { + callback(begin + root.begin - 1, PARANOIA_CB_FIXUP_DROPPED); + let data: Vec = + root.vector[(begin - match_b) as usize..begin as usize].to_vec(); + let l = scratch.as_mut().unwrap(); + l.insert(begin_l, &data); + offset += match_b; + } else { + callback(begin + root.begin - 1, PARANOIA_CB_FIXUP_DUPED); + let l = scratch.as_mut().unwrap(); + l.remove(begin_l + match_b, -match_b); + offset += match_b; + } + } else if match_c != 0 { + if root.begin + begin - match_c < *root_returned_limit { + break; + } + let l = scratch.as_ref().unwrap(); + let data: Vec = l.vector[(begin_l - match_c) as usize..begin_l as usize].to_vec(); + root.overwrite(begin - match_c, &data); + } else { + break; + } + + let begin_l = begin + offset; + let l = scratch.as_ref().unwrap(); + let mut nb = begin; + let mut ne = end; + overlap( + &root.vector, + &l.vector, + begin, + begin_l, + root.size, + l.size, + Some(&mut nb), + Some(&mut ne), + ); + begin = nb; + end = ne; + } + + // Trailing rifts. + let mut temp = scratch.as_ref().map(|l| l.size).unwrap_or(v.size); + while end + offset < temp && end < root.size { + let begin_l = begin + offset; + let end_l = end + offset; + if scratch.is_none() { + scratch = Some(CBlock::from_parts(v.vector.clone(), v.begin, v.lastsector)); + } + let (mut match_a, mut match_b, mut match_c) = (0, 0, 0); + { + let l = scratch.as_ref().unwrap(); + analyze_rift_f( + &root.vector, + &l.vector, + root.size, + l.size, + end, + end_l, + &mut match_a, + &mut match_b, + &mut match_c, + ); + } + + if match_a != 0 { + if match_a > 0 { + callback(end + root.begin, PARANOIA_CB_FIXUP_DROPPED); + if end + root.begin < *root_returned_limit { + break; + } + let l = scratch.as_ref().unwrap(); + let data: Vec = l.vector[end_l as usize..(end_l + match_a) as usize].to_vec(); + root.insert(end, &data); + } else { + callback(end + root.begin, PARANOIA_CB_FIXUP_DUPED); + if end + root.begin < *root_returned_limit { + break; + } + root.remove(end, -match_a); + } + } else if match_b != 0 { + if match_b > 0 { + callback(end + root.begin, PARANOIA_CB_FIXUP_DROPPED); + let data: Vec = root.vector[end as usize..(end + match_b) as usize].to_vec(); + let l = scratch.as_mut().unwrap(); + l.insert(end_l, &data); + } else { + callback(end + root.begin, PARANOIA_CB_FIXUP_DUPED); + let l = scratch.as_mut().unwrap(); + l.remove(end_l, -match_b); + } + } else if match_c != 0 { + if end + root.begin < *root_returned_limit { + break; + } + let l = scratch.as_ref().unwrap(); + let data: Vec = l.vector[end_l as usize..(end_l + match_c) as usize].to_vec(); + root.overwrite(end, &data); + } else { + let (mut ma, mut mb) = (0, 0); + { + let l = scratch.as_ref().unwrap(); + analyze_rift_silence_f( + &root.vector, + &l.vector, + root.size, + l.size, + end, + end_l, + &mut ma, + &mut mb, + ); + } + if ma != 0 { + if end + root.begin >= *root_returned_limit { + root.remove(end, root.size - end); + } + } else if mb != 0 { + return MergeOutcome::Merged; + } else { + break; + } + } + + let l = scratch.as_ref().unwrap(); + let mut ne = end; + overlap( + &root.vector, + &l.vector, + begin, + begin_l, + root.size, + l.size, + None, + Some(&mut ne), + ); + end = ne; + temp = scratch.as_ref().map(|l| l.size).unwrap_or(v.size); + } + + // Third: extend the root tail with the fragment. + let size_a = root.size; + let (size_b, vec_begin) = match scratch.as_ref() { + Some(l) => (l.size, l.begin), + None => (v.size, v.begin), + }; + if size_b - offset > size_a || v.lastsector { + if v.lastsector { + *root_lastsector = true; + } + if end < size_a { + root.remove(end, size_a - end); + } + let data: Vec = match scratch.as_ref() { + Some(l) => l.vector[(end + offset) as usize..size_b as usize].to_vec(), + None => v.vector[(end + offset) as usize..size_b as usize].to_vec(), + }; + if !data.is_empty() { + root.append(&data); + } + silence_test( + root, + *root_returned_limit, + root_silence_flag, + root_silence_begin, + ); + add_stage2_value(stage2, offset + vec_begin - root.begin, stage2_pending); + } else { + let _ = vec_begin; + } + + MergeOutcome::Merged +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum MergeOutcome { + Merged, + Kept, + Discarded, +} + +// --------------------------------------------------------------------------- +// Paranoia state machine +// --------------------------------------------------------------------------- + +impl Paranoia { + pub fn new(d: Box) -> Self { + let cursor = d.disc_firstsector(); + let mut p = Self { + d, + root: RootBlock::default(), + cache: VecDeque::new(), + cache_limit: JIGGLE_MODULO as usize, + fragments: Vec::new(), + sortcache: SortIndex::new((CACHEMODEL_SECTORS * CD_FRAMEWORDS) as usize), + cdcache_size: CACHEMODEL_SECTORS, + cdcache_begin: 9_999_999, + cdcache_end: 9_999_999, + jitter: 0, + enable: PARANOIA_MODE_FULL, + cursor, + current_lastsector: -1, + current_firstsector: -1, + stage1: Offsets::default(), + stage2: Offsets::default(), + dynoverlap: MAX_SECTOR_OVERLAP * CD_FRAMEWORDS, + dyndrift: 0, + stage2_adjust_pending: false, + }; + p.firstlast(); + p + } + + pub fn drive(&self) -> &dyn Drive { + self.d.as_ref() + } + pub fn drive_mut(&mut self) -> &mut dyn Drive { + self.d.as_mut() + } + + pub fn modeset(&mut self, mode: i32) { + self.enable = mode; + } + + pub fn overlapset(&mut self, overlap: i64) { + self.dynoverlap = overlap * CD_FRAMEWORDS; + self.stage1.offpoints = -1; + } + + pub fn cachemodel_size(&mut self, sectors: i64) -> i64 { + let ret = self.cdcache_size; + if sectors >= 0 { + self.cdcache_size = sectors; + } + ret + } + + fn firstlast(&mut self) { + let tracks = self.d.tracks(); + let curtrack = self.d.sector_gettrack(self.cursor); + + self.current_lastsector = -1; + let start = curtrack.max(0); + for i in start..tracks { + if !self.d.track_audiop(i) { + self.current_lastsector = self.d.track_lastsector(i - 1); + } + } + if self.current_lastsector == -1 { + self.current_lastsector = self.d.disc_lastsector(); + } + + self.current_firstsector = -1; + let mut i = curtrack; + while i > 0 { + if !self.d.track_audiop(i) { + self.current_firstsector = self.d.track_firstsector(i + 1); + } + i -= 1; + } + if self.current_firstsector == -1 { + self.current_firstsector = self.d.disc_firstsector(); + } + } + + pub fn seek(&mut self, seek: i64, mode: i32) -> i64 { + let sector = match mode { + SEEK_SET => seek, + SEEK_END => self.d.disc_lastsector() + seek, + _ => self.cursor + seek, + }; + if self.d.sector_gettrack(sector) < 0 { + return -1; + } + self.root.vector = None; + self.root.lastsector = false; + self.root.returned_limit = 0; + self.root.silence_flag = false; + let ret = self.cursor; + self.cursor = sector; + self.firstlast(); + self.current_firstsector = sector; + ret + } + + fn resetall(&mut self) { + self.root.returned_limit = 0; + self.dyndrift = 0; + self.root.lastsector = false; + self.root.vector = None; + self.root.silence_flag = false; + self.resetcache(); + } + + fn resetcache(&mut self) { + self.cache.clear(); + self.fragments.clear(); + } + + fn recover_cache(&mut self) { + while self.cache.len() > self.cache_limit { + self.cache.pop_back(); + } + } + + fn cache_update(&mut self, mut lba: i64, mut sectors: i64) { + if lba + sectors > self.cdcache_size { + let end = lba + sectors; + lba = end - self.cdcache_size; + sectors = end - lba; + } + if lba < self.cdcache_begin { + self.cdcache_begin = lba; + self.cdcache_end = lba + sectors; + } else { + if lba + sectors > self.cdcache_end { + self.cdcache_end = lba + sectors; + } + if lba + sectors - self.cdcache_size > self.cdcache_begin { + if lba + sectors - self.cdcache_size < self.cdcache_end { + self.cdcache_begin = lba + sectors - self.cdcache_size; + } else { + self.cdcache_begin = lba; + } + } + } + } + + fn cache_handler(&mut self, lba: i64, callback: &mut dyn FnMut(i64, i32)) { + if lba >= self.cdcache_end { + return; + } + let lba = lba.max(0); + let first = self.d.disc_firstsector(); + let seekpos = if lba < self.cdcache_begin { + if lba == 0 || lba - 1 < first { + lba + } else { + lba - 1 + } + } else { + let pre = self.cdcache_begin - 1; + if pre < first { + lba + self.cdcache_size + } else { + pre + } + }; + let timed = self.d.read_timed(None, seekpos, 1); + if timed.result.as_ref().map(|n| *n == 1).unwrap_or(false) + && seekpos < self.cdcache_begin + && timed.milliseconds < MIN_SEEK_MS + { + callback(seekpos * CD_FRAMEWORDS, PARANOIA_CB_CACHEERR); + } + self.cache_update(seekpos, 1); + } + + fn read_c_block( + &mut self, + beginword: i64, + _endword: i64, + callback: &mut dyn FnMut(i64, i32), + ) -> Option { + let totaltoread = self.cdcache_size; + let sectatonce = self.d.nsectors().max(1); + let driftcomp = (self.dyndrift as f64 / CD_FRAMEWORDS as f64 + 0.5) as i64; + let dynoverlap_words = (self.dynoverlap + CD_FRAMEWORDS - 1) / CD_FRAMEWORDS; + + let mut readat = if self.enable & (PARANOIA_MODE_VERIFY | PARANOIA_MODE_OVERLAP) != 0 { + let target = match self.root.vector.as_ref() { + None => self.cursor - dynoverlap_words, + Some(r) if r.begin > beginword => self.cursor - dynoverlap_words, + Some(r) => r.end() / CD_FRAMEWORDS - dynoverlap_words, + }; + let mut r = (target & !(JIGGLE_MODULO - 1)) + self.jitter; + if r > target { + r -= JIGGLE_MODULO; + } + self.jitter -= 1; + if self.jitter < 0 { + self.jitter += JIGGLE_MODULO; + } + r + } else { + self.cursor + }; + readat += driftcomp; + + if self.enable & (PARANOIA_MODE_OVERLAP | PARANOIA_MODE_VERIFY) == 0 { + self.resetall(); + } + + let mut new = CBlock::new(); + new.flags = if self.enable & (PARANOIA_MODE_OVERLAP | PARANOIA_MODE_VERIFY) != 0 { + Some(vec![0u8; (totaltoread * CD_FRAMEWORDS) as usize]) + } else { + None + }; + self.cache.push_front(new); + self.recover_cache(); + + let mut raw = vec![0u8; (totaltoread * CD_FRAMESIZE_RAW as i64) as usize]; + let mut sofar = 0i64; + let mut firstread = -1i64; + let mut anyflag = false; + + self.cache_handler(readat, callback); + + while sofar < totaltoread { + let mut secread = sectatonce; + let mut adjread = readat; + if adjread < self.current_firstsector { + secread -= self.current_firstsector - adjread; + adjread = self.current_firstsector; + } + if adjread + secread - 1 > self.current_lastsector { + secread = self.current_lastsector - adjread + 1; + } + if sofar + secread > totaltoread { + secread = totaltoread - sofar; + } + + if secread > 0 { + if firstread < 0 { + firstread = adjread; + } + let start = (sofar * CD_FRAMESIZE_RAW as i64) as usize; + let end = start + (secread * CD_FRAMESIZE_RAW as i64) as usize; + let end = end.min(raw.len()); + let result = self + .d + .read_audio(Some(&mut raw[start..end]), adjread, secread); + let thisread = match result { + Ok(n) => { + let mut n = n as i64; + if n > secread { + n = secread; + } + n + } + Err(e) => { + if e.errno == libc::ENOMEDIUM { + self.cache.pop_front(); + return None; + } + 0 + } + }; + if thisread < secread { + if thisread >= 0 { + callback((adjread + thisread) * CD_FRAMEWORDS, PARANOIA_CB_READERR); + } + let start = (sofar + thisread) as usize * CD_FRAMESIZE_RAW; + let end = (sofar + secread) as usize * CD_FRAMESIZE_RAW; + if start < raw.len() { + let end = end.min(raw.len()); + raw[start..end].fill(0); + } + if let Some(flags) = self.cache.front_mut().and_then(|c| c.flags.as_mut()) { + let start = ((sofar + thisread) * CD_FRAMEWORDS) as usize; + let end = ((sofar + secread) * CD_FRAMEWORDS) as usize; + if start < flags.len() { + let end = end.min(flags.len()); + flags[start..end].fill(FLAGS_UNREAD); + } + } + } + if thisread != 0 { + anyflag = true; + } + + if let Some(c) = self.cache.front_mut() { + if let Some(flags) = c.flags.as_mut() { + if sofar != 0 { + let center = sofar * CD_FRAMEWORDS; + let lo = center - MIN_WORDS_OVERLAP / 2; + let hi = center + MIN_WORDS_OVERLAP / 2; + for i in lo.max(0)..hi.min(flags.len() as i64) { + flags[i as usize] |= FLAGS_EDGE; + } + } + } + if adjread + secread - 1 == self.current_lastsector { + c.lastsector = true; + } + } + + callback((adjread + secread - 1) * CD_FRAMEWORDS, PARANOIA_CB_READ); + self.cache_update(adjread, secread); + sofar += secread; + readat = adjread + secread; + } else if readat < self.current_firstsector { + readat += sectatonce; + } else { + break; + } + } + + if anyflag { + let words = (sofar * CD_FRAMEWORDS) as usize; + let bytes = (sofar * CD_FRAMESIZE_RAW as i64) as usize; + raw.truncate(bytes); + let samples = crate::device::bytes_to_samples(&raw); + let mut flags = self.cache.front().and_then(|c| c.flags.clone()); + if let Some(f) = flags.as_mut() { + f.truncate(words); + } + let mut new = self.cache.pop_front().unwrap(); + new.vector = samples; + new.flags = flags; + new.begin = firstread * CD_FRAMEWORDS - self.dyndrift; + new.size = words as i64; + self.cache.push_front(new); + Some(self.cache.front().unwrap().clone()) + } else { + self.cache.pop_front(); + None + } + } + + fn stage1(&mut self, callback: &mut dyn FnMut(i64, i32)) -> i64 { + let mut new = match self.cache.pop_front() { + Some(n) => n, + None => return 0, + }; + let mut ret = 0; + self.sortcache + .setup(&new.vector, new.begin, new.begin, new.end()); + + let cache_len = self.cache.len(); + for idx in (0..cache_len).rev() { + callback(new.begin, PARANOIA_CB_VERIFY); + let old = &mut self.cache[idx]; + let sort = &mut self.sortcache; + let stage1 = &mut self.stage1; + let dynoverlap = &mut self.dynoverlap; + ret += iterate_stage1(old, &mut new, sort, stage1, dynoverlap, callback); + } + + // Create fragments from verified runs. + let mut begin = 0i64; + while begin < new.size { + while begin < new.size + && new.flags.as_ref().unwrap()[begin as usize] & FLAGS_VERIFIED == 0 + { + begin += 1; + } + let mut end = begin; + while end < new.size && new.flags.as_ref().unwrap()[end as usize] & FLAGS_VERIFIED != 0 + { + end += 1; + } + if begin >= new.size { + break; + } + let frag_begin = new.begin + (begin - OVERLAP_ADJ).max(0); + let frag_end = new.begin + (end + OVERLAP_ADJ).min(new.size); + let lastsector = end + OVERLAP_ADJ >= new.size && new.lastsector; + let start_idx = (frag_begin - new.begin) as usize; + let end_idx = (frag_end - new.begin) as usize; + self.fragments.push(Some(VFragment { + vector: new.vector[start_idx..end_idx].to_vec(), + begin: frag_begin, + size: frag_end - frag_begin, + lastsector, + })); + begin = end; + } + + self.cache.push_front(new); + ret + } + + fn stage2(&mut self, beginword: i64, _endword: i64, callback: &mut dyn FnMut(i64, i32)) -> i64 { + let mut root = match self.root.vector.take() { + Some(r) => r, + None => { + // Try to initialize from a fragment containing beginword. + let mut candidates: Vec<(i64, usize)> = self + .fragments + .iter() + .enumerate() + .filter_map(|(id, v)| { + v.as_ref().and_then(|v| { + if v.begin <= beginword && v.end() > beginword { + Some((v.begin, id)) + } else { + None + } + }) + }) + .collect(); + candidates.sort_by_key(|(begin, _)| *begin); + let mut initialized = candidates.first().map(|(_, id)| *id); + if initialized.is_none() && beginword < 0 { + // Addresses before LBA 0 are legal for some shifted-TOC + // setups. Start the verified root at the earliest + // fragment; the pregap zeros are prepended by the read + // loop. + let mut all: Vec<(i64, usize)> = self + .fragments + .iter() + .enumerate() + .filter_map(|(id, v)| v.as_ref().map(|v| (v.begin, id))) + .collect(); + all.sort_by_key(|(begin, _)| *begin); + initialized = all.first().map(|(_, id)| *id); + } + if let Some(id) = initialized { + let v = self.fragments[id].take().unwrap(); + let c = CBlock::from_parts(v.vector, v.begin, v.lastsector); + self.root.lastsector = v.lastsector; + self.root.returned_limit = beginword; + self.root.silence_flag = false; + self.root.silence_begin = 0; + self.root.vector = Some(c); + let mut r = self.root.vector.take().unwrap(); + silence_test( + &mut r, + self.root.returned_limit, + &mut self.root.silence_flag, + &mut self.root.silence_begin, + ); + self.root.vector = Some(r); + self.adjust_stage2(callback); + return 1; + } + return 0; + } + }; + + let mut returned_limit = self.root.returned_limit; + let mut root_lastsector = self.root.lastsector; + let mut silence_flag = self.root.silence_flag; + let mut silence_begin = self.root.silence_begin; + silence_test( + &mut root, + returned_limit, + &mut silence_flag, + &mut silence_begin, + ); + + let mut ret = 0; + let mut flag = true; + while flag { + flag = false; + let mut ids: Vec = self + .fragments + .iter() + .enumerate() + .filter_map(|(i, v)| v.as_ref().map(|v| (i, v.begin))) + .map(|(i, _)| i) + .collect(); + ids.sort_by_key(|&id| self.fragments[id].as_ref().unwrap().begin); + + for id in ids { + if self.fragments[id].is_none() { + continue; + } + let mut v = self.fragments[id].take().unwrap(); + let outcome = stage2_each( + &mut root, + &mut returned_limit, + &mut root_lastsector, + &mut silence_flag, + &mut silence_begin, + &mut v, + &mut self.sortcache, + &mut self.stage1, + &mut self.dynoverlap, + &mut self.stage2, + &mut self.stage2_adjust_pending, + callback, + ); + match outcome { + MergeOutcome::Merged => { + ret += 1; + flag = true; + } + MergeOutcome::Discarded => {} + MergeOutcome::Kept => { + self.fragments[id] = Some(v); + } + } + } + + if !flag && silence_flag { + let mut ids: Vec = self + .fragments + .iter() + .enumerate() + .filter_map(|(i, v)| v.as_ref().map(|v| (i, v.begin))) + .map(|(i, _)| i) + .collect(); + ids.sort_by_key(|&id| self.fragments[id].as_ref().unwrap().begin); + for id in ids { + if self.fragments[id].is_none() { + continue; + } + let v = self.fragments[id].take().unwrap(); + if silence_match( + &mut root, + returned_limit, + &mut silence_flag, + &mut silence_begin, + &v, + self.dynoverlap, + &mut self.stage2, + &mut self.stage2_adjust_pending, + callback, + ) { + ret += 1; + flag = true; + } else { + self.fragments[id] = Some(v); + } + } + } + } + + self.root.vector = Some(root); + self.root.returned_limit = returned_limit; + self.root.lastsector = root_lastsector; + self.root.silence_flag = silence_flag; + self.root.silence_begin = silence_begin; + self.adjust_stage2(callback); + ret + } + + fn adjust_stage2(&mut self, callback: &mut dyn FnMut(i64, i32)) { + if !self.stage2_adjust_pending { + return; + } + self.stage2_adjust_pending = false; + if self.stage2.offpoints >= 10 { + let av = if self.stage2.offpoints != 0 { + self.stage2.offaccum / self.stage2.offpoints + } else { + 0 + }; + if av.abs() > self.dynoverlap / 4 { + let av = (av / MIN_SECTOR_EPSILON) * MIN_SECTOR_EPSILON; + let root_end = self.root.end(); + callback(root_end, PARANOIA_CB_DRIFT); + self.dyndrift += av; + + for v in self.fragments.iter_mut().flatten() { + if v.begin < av { + // Drop fragments that would move before absolute zero. + *v = VFragment { + vector: Vec::new(), + begin: 0, + size: -1, + lastsector: false, + }; + } else { + v.begin -= av; + } + } + self.fragments + .retain(|v| v.as_ref().map(|f| f.size >= 0).unwrap_or(false)); + + for c in self.cache.iter_mut() { + let adj = av.min(c.begin); + c.begin -= adj; + } + + self.stage2.offaccum = 0; + self.stage2.offmin = 0; + self.stage2.offmax = 0; + self.stage2.offpoints = 0; + self.stage2.newpoints = 0; + self.stage2.offdiff = 0; + } + } + } + + fn trim(&mut self, beginword: i64) { + if let Some(root) = self.root.vector.as_mut() { + let target = beginword - MAX_SECTOR_OVERLAP * CD_FRAMEWORDS; + let rbegin = root.begin; + let rend = root.end(); + if rbegin > beginword { + self.root.vector = None; + self.root.returned_limit = -1; + self.root.lastsector = false; + return; + } + if rbegin + MAX_SECTOR_OVERLAP * CD_FRAMEWORDS < beginword { + if target + MIN_WORDS_OVERLAP > rend { + self.root.vector = None; + self.root.returned_limit = -1; + self.root.lastsector = false; + return; + } + let offset = target - rbegin; + root.remove_front(offset); + } + } + let cutoff = beginword - MAX_SECTOR_OVERLAP * CD_FRAMEWORDS; + self.cache.retain(|c| c.end() >= cutoff); + } + + fn end_case(&mut self, endword: i64) { + if !self.root.lastsector { + return; + } + let root_end = self.root.end(); + if endword < root_end { + return; + } + let addto = endword - root_end; + if addto > 0 { + if let Some(root) = self.root.vector.as_mut() { + root.append(&vec![0i16; addto as usize]); + } + } + self.resetcache(); + } + + fn verify_skip_case(&mut self, callback: &mut dyn FnMut(i64, i32)) { + let post = if self.root.is_empty() { + 0 + } else { + self.root.end() + } + .max(0); + callback(post, PARANOIA_CB_SKIP); + + let mut graft: Option = None; + let mut gend = 0i64; + let mut vflag = false; + + for (idx, c) in self.cache.iter().enumerate() { + let cbegin = c.begin; + let cend = c.end(); + if cbegin <= post && cend > post { + let mut vend = post; + let verified = c + .flags + .as_ref() + .map(|f| f[(post - cbegin) as usize] & FLAGS_VERIFIED != 0) + .unwrap_or(false); + if verified { + while vend < cend + && c.flags.as_ref().unwrap()[(vend - cbegin) as usize] & FLAGS_VERIFIED != 0 + { + vend += 1; + } + if !vflag || vend > gend { + graft = Some(idx); + gend = vend; + } + vflag = true; + } else if !vflag { + while vend < cend + && c.flags.as_ref().unwrap()[(vend - cbegin) as usize] & FLAGS_VERIFIED == 0 + { + vend += 1; + } + if graft.is_none() || gend > vend { + graft = Some(idx); + gend = vend; + } + } + } + } + + if let Some(idx) = graft { + let c = &self.cache[idx]; + let cbegin = c.begin; + let cend = c.end(); + while gend < cend + && c.flags.as_ref().unwrap()[(gend - cbegin) as usize] & FLAGS_VERIFIED != 0 + { + gend += 1; + } + gend = (gend + OVERLAP_ADJ).min(cend); + if self.root.is_empty() { + let c = &self.cache[idx]; + self.root.vector = Some(CBlock::from_parts(c.vector.clone(), cbegin, false)); + } else { + let c = &self.cache[idx]; + let data: Vec = + c.vector[(post - cbegin) as usize..(gend - cbegin) as usize].to_vec(); + if let Some(root) = self.root.vector.as_mut() { + root.append(&data); + } + } + self.root.returned_limit = self.root.end(); + return; + } + + let zeros = vec![0i16; CD_FRAMESIZE_RAW]; + if self.root.is_empty() { + self.root.vector = Some(CBlock::from_parts(zeros, post, false)); + } else if let Some(root) = self.root.vector.as_mut() { + root.append(&zeros); + } + self.root.returned_limit = self.root.end(); + } + + pub fn read(&mut self, callback: &mut dyn FnMut(i64, i32)) -> Option<&mut [i16]> { + self.read_limited(callback, 20) + } + + pub fn read_limited( + &mut self, + callback: &mut dyn FnMut(i64, i32), + max_retries: i32, + ) -> Option<&mut [i16]> { + if !self.d.opened() { + return None; + } + let beginword = self.cursor * CD_FRAMEWORDS; + let endword = beginword + CD_FRAMEWORDS; + let mut retry_count = 0i32; + let mut lastend = self.root.end(); + + if beginword > self.root.returned_limit { + self.root.returned_limit = beginword; + } + + loop { + if beginword < 0 { + if let Some(root) = self.root.vector.as_mut() { + if root.begin > beginword { + let add = (root.begin - beginword) as usize; + root.vector.splice(0..0, std::iter::repeat_n(0i16, add)); + root.size += add as i64; + root.begin = beginword; + } + } + } + let need_more = self.root.vector.is_none() + || self.root.begin() > beginword + || (self.root.end() < endword + MAX_SECTOR_OVERLAP * CD_FRAMEWORDS + && self.enable & (PARANOIA_MODE_VERIFY | PARANOIA_MODE_OVERLAP) != 0) + || self.root.end() < endword; + if !need_more { + break; + } + + if self.enable & (PARANOIA_MODE_VERIFY | PARANOIA_MODE_OVERLAP) != 0 { + self.trim(beginword); + self.recover_cache(); + if self.root.begin() != -1 && self.root.lastsector { + self.end_case(endword + MAX_SECTOR_OVERLAP * CD_FRAMEWORDS); + } else { + self.stage2( + beginword, + endword + MAX_SECTOR_OVERLAP * CD_FRAMEWORDS, + callback, + ); + } + } else { + self.end_case(endword + MAX_SECTOR_OVERLAP * CD_FRAMEWORDS); + } + + let satisfied = !(self.root.begin() == -1 + || self.root.begin() > beginword + || self.root.end() < endword + MAX_SECTOR_OVERLAP * CD_FRAMEWORDS); + if std::env::var_os("CDR_NEG").is_some() { + eprintln!( + "neg loop cursor={} root={:?}..{:?} need={} satisfied={} retry={}", + self.cursor, + self.root.begin(), + self.root.end(), + need_more, + satisfied, + retry_count + ); + } + if satisfied { + break; + } + + let new = self.read_c_block(beginword, endword, callback); + if std::env::var_os("CDR_NEG").is_some() { + eprintln!( + " new={:?} frags={}", + new.as_ref().map(|b| (b.begin, b.size)), + self.fragments.iter().filter(|f| f.is_some()).count() + ); + } + if let Some(mut new) = new { + if self.enable & (PARANOIA_MODE_OVERLAP | PARANOIA_MODE_VERIFY) != 0 { + if self.enable & PARANOIA_MODE_VERIFY != 0 { + // stage1() expects the new block at the head. + self.stage1(callback); + } else { + let mut begin = 0i64; + while begin < new.size { + while begin < new.size + && new.flags.as_ref().unwrap()[begin as usize] & FLAGS_EDGE != 0 + { + begin += 1; + } + let mut end = begin + 1; + while end < new.size + && new.flags.as_ref().unwrap()[end as usize] & FLAGS_EDGE == 0 + { + end += 1; + } + let frag_begin = new.begin + begin; + let frag_end = new.begin + end; + let lastsector = new.lastsector && new.begin + end == new.end(); + self.fragments.push(Some(VFragment { + vector: new.vector[begin as usize..end as usize].to_vec(), + begin: frag_begin, + size: frag_end - frag_begin, + lastsector, + })); + begin = end; + } + } + } else { + self.root.vector = Some(CBlock { + vector: std::mem::take(&mut new.vector), + flags: None, + begin: new.begin, + size: new.size, + lastsector: new.lastsector, + }); + self.end_case(endword + MAX_SECTOR_OVERLAP * CD_FRAMEWORDS); + } + } else if self.d.opened() { + // No medium or fatal read error. + return None; + } + + if self.root.begin() != -1 && lastend + 588 < self.root.end() { + lastend = self.root.end(); + retry_count = 0; + } else { + retry_count += 1; + if retry_count % 5 == 0 { + if self.dynoverlap == MAX_SECTOR_OVERLAP * CD_FRAMEWORDS + || retry_count == max_retries + { + if self.enable & PARANOIA_MODE_NEVERSKIP == 0 { + self.verify_skip_case(callback); + } + retry_count = 0; + } else if self.stage1.offpoints != -1 { + self.dynoverlap = ((self.dynoverlap as f64) * 1.5) as i64; + if self.dynoverlap > MAX_SECTOR_OVERLAP * CD_FRAMEWORDS { + self.dynoverlap = MAX_SECTOR_OVERLAP * CD_FRAMEWORDS; + } + callback(self.dynoverlap, PARANOIA_CB_OVERLAP); + } + } + } + } + + self.cursor += 1; + let root = self.root.vector.as_mut()?; + let start = (beginword - root.begin) as usize; + let end = start + CD_FRAMEWORDS as usize; + if end > root.vector.len() { + return None; + } + Some(&mut root.vector[start..end]) + } +} diff --git a/crates/core/src/paranoia/sort.rs b/crates/core/src/paranoia/sort.rs new file mode 100644 index 0000000..16f806b --- /dev/null +++ b/crates/core/src/paranoia/sort.rs @@ -0,0 +1,87 @@ +//! Sample-value index, a safe Rust equivalent of isort.c. + +use std::collections::HashMap; + +pub struct SortIndex { + buckets: HashMap>, + pub begin: i64, + pub size: usize, + cursor: Option<(i16, usize, usize)>, // value, next bucket index, hi +} + +impl SortIndex { + pub fn new(_maxsize: usize) -> Self { + Self { + buckets: HashMap::new(), + begin: 0, + size: 0, + cursor: None, + } + } + + pub fn setup(&mut self, samples: &[i16], begin_abs: i64, sortlo: i64, sorthi: i64) { + self.buckets.clear(); + self.begin = begin_abs; + self.size = samples.len(); + self.cursor = None; + let lo = ((sortlo - begin_abs).max(0) as usize).min(samples.len()); + let hi = ((sorthi - begin_abs).max(0) as usize).min(samples.len()); + if lo >= hi { + return; + } + for (i, sample) in samples.iter().enumerate().take(hi).skip(lo) { + self.buckets.entry(*sample).or_default().push(i as u32); + } + } + + /// Equivalent of sort_getmatch(). Returns the first matching relative + /// position, and leaves a cursor for sort_nextmatch(). + pub fn getmatch(&mut self, post: i64, overlap: i64, value: i16) -> Option { + let post = post.clamp(0, self.size as i64); + let lo = (post - overlap).max(0) as usize; + let hi = ((post + overlap).min(self.size as i64)).max(0) as usize; + self.cursor = None; + let positions = self.buckets.get(&value)?; + let idx = positions.partition_point(|p| (*p as usize) < lo); + if idx >= positions.len() { + return None; + } + let pos = positions[idx] as usize; + if pos >= hi { + return None; + } + self.cursor = Some((value, idx + 1, hi)); + Some(pos) + } + + pub fn nextmatch(&mut self) -> Option { + let (value, idx, hi) = self.cursor?; + let positions = self.buckets.get(&value)?; + if idx >= positions.len() { + self.cursor = None; + return None; + } + let pos = positions[idx] as usize; + if pos >= hi { + self.cursor = None; + return None; + } + self.cursor = Some((value, idx + 1, hi)); + Some(pos) + } + + /// Position most recently returned by getmatch()/advance(). + pub fn cursor_position(&self) -> Option { + let (value, idx, _hi) = self.cursor?; + let positions = self.buckets.get(&value)?; + if idx == 0 || idx > positions.len() { + return None; + } + Some(positions[idx - 1] as i64) + } + + /// Advance to the next match. Returns false when exhausted. + pub fn advance(&mut self) -> bool { + self.nextmatch().is_some() + } +} diff --git a/crates/core/tests/paranoia_image.rs b/crates/core/tests/paranoia_image.rs new file mode 100644 index 0000000..8db1787 --- /dev/null +++ b/crates/core/tests/paranoia_image.rs @@ -0,0 +1,130 @@ +use cdrustanoia_core::device::image::ImageDrive; +use cdrustanoia_core::device::{bytes_to_samples, samples_to_bytes, Drive, CD_FRAMESIZE_RAW}; +use cdrustanoia_core::paranoia::{Paranoia, PARANOIA_MODE_FULL}; + +fn make_source(sectors: usize) -> Vec { + let mut out = Vec::with_capacity(sectors * 1176); + let mut x: u64 = 0x1234_5678_9abc_def0; + for i in 0..sectors * 1176 { + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + // Mix in some silence and a distinct waveform. + let v = if i % 5000 < 1000 { + 0 + } else { + (x as i16).wrapping_add((i as i16).wrapping_mul(7)) + }; + out.push(v); + } + out +} + +#[test] +fn clean_image_roundtrip() { + let sectors = 80usize; + let source = make_source(sectors); + let bytes = samples_to_bytes(&source); + let drive = ImageDrive::from_bytes(bytes); + let mut p = Paranoia::new(Box::new(drive)); + p.modeset(PARANOIA_MODE_FULL); + let mut got = Vec::with_capacity(sectors * 1176); + let mut cb = |_: i64, _: i32| {}; + for _ in 0..sectors { + let sec = p.read(&mut cb).expect("read sector"); + got.extend_from_slice(sec); + } + assert_eq!(got.len(), source.len()); + assert_eq!(got, source); +} + +#[test] +fn drive_toc_helpers() { + let bytes = vec![0u8; CD_FRAMESIZE_RAW * 10]; + let drive = ImageDrive::from_bytes(bytes); + assert_eq!(drive.tracks(), 1); + assert_eq!(drive.disc_firstsector(), 0); + assert_eq!(drive.disc_lastsector(), 9); + assert_eq!(drive.track_firstsector(1), 0); + assert_eq!(drive.track_lastsector(1), 9); + let _ = bytes_to_samples(&[0, 0]); +} + +fn run_config(sectors: usize, config: cdrustanoia_core::device::image::ImageConfig) -> Vec { + let source = make_source(sectors); + let bytes = samples_to_bytes(&source); + let drive = ImageDrive::with_config(bytes, config); + let mut p = Paranoia::new(Box::new(drive)); + p.modeset(PARANOIA_MODE_FULL); + let start = 5usize.min(sectors); + p.seek(start as i64, 0); + let mut got = Vec::with_capacity((sectors - start) * 1176); + let mut cb = |_: i64, _: i32| {}; + for _ in start..sectors { + let sec = p.read(&mut cb).expect("read sector"); + got.extend_from_slice(sec); + } + let source = &source[start * 1176..]; + assert_eq!(got.len(), source.len()); + if got != source { + let mut best: Option<(i64, usize)> = None; + for shift in -2000i64..=2000 { + let mut bad = 0usize; + for (i, value) in got.iter().enumerate().take(source.len()) { + let j = i as i64 + shift; + if j < 0 || j as usize >= source.len() || *value != source[j as usize] { + bad += 1; + if bad > 8 { + break; + } + } + } + if bad <= 8 { + best = Some((shift, bad)); + break; + } + } + if let Some((shift, bad)) = best { + panic!( + "data shifted by {} words ({} mismatches); engine did not align absolute position", + shift, bad + ); + } + let idx = got + .iter() + .zip(source.iter()) + .position(|(a, b)| a != b) + .unwrap_or(0); + let where0 = source.iter().position(|x| *x == got[0]); + panic!( + "mismatch at word {} (sector {} offset {}): got {} expected {}; got0 found at {:?}; got[..8]={:?} src[..8]={:?}", + idx, + idx / 1176, + idx % 1176, + got[idx], + source[idx], + where0, + &got[..got.len().min(8)], + &source[..source.len().min(8)] + ); + } + got +} + +#[test] +fn jittered_image_roundtrip() { + use cdrustanoia_core::device::image::{ImageConfig, ImageFaults}; + let sectors = 20usize; + let config = ImageConfig { + tracks: vec![(0, sectors as i64 - 1, 0)], + faults: ImageFaults { + jitter_words: 180, + jitter_period: 1, + jitter_skip: 10, + max_read_sectors: 0, + ..Default::default() + }, + ..Default::default() + }; + run_config(sectors, config); +} diff --git a/include/cdda_interface.h b/include/cdda_interface.h new file mode 100644 index 0000000..8487cb7 --- /dev/null +++ b/include/cdda_interface.h @@ -0,0 +1,207 @@ +/****************************************************************** + * CopyPolicy: GNU Lesser General Public License 2.1 applies + * Copyright (C) 2001-2008 Xiph.org + * Original version by Heiko Eissfeldt heiko@escape.colossus.de + * + * Toplevel interface header; applications include this + * + ******************************************************************/ + +#ifndef _cdda_interface_h_ +#define _cdda_interface_h_ + +#ifndef CD_FRAMESIZE +#define CD_FRAMESIZE 2048 +#endif +#ifndef CD_FRAMESIZE_RAW +#define CD_FRAMESIZE_RAW 2352 +#endif +#define CD_FRAMESAMPLES (CD_FRAMESIZE_RAW / 4) + +#include +#include + +#define MAXTRK 100 + +typedef struct TOC { /* structure of table of contents */ + unsigned char bFlags; + unsigned char bTrack; + int32_t dwStartSector; +} TOC; + +/* interface types */ +#define GENERIC_SCSI 0 +#define COOKED_IOCTL 1 +#define TEST_INTERFACE 2 +#define SGIO_SCSI 3 +#define SGIO_SCSI_BUGGY1 4 + +#define CDDA_MESSAGE_FORGETIT 0 +#define CDDA_MESSAGE_PRINTIT 1 +#define CDDA_MESSAGE_LOGIT 2 + +/* cdrom access function pointer */ + +typedef struct cdda_private_data cdda_private_data_t; + +typedef struct cdrom_drive{ + + int opened; /* This struct may just represent a candidate for opening */ + + char *cdda_device_name; + char *ioctl_device_name; + + int cdda_fd; + int ioctl_fd; + + char *drive_model; + int drive_type; + int interface; + int bigendianp; + int nsectors; + + int cd_extra; + int tracks; + TOC disc_toc[MAXTRK]; + long audio_first_sector; + long audio_last_sector; + + int errordest; + int messagedest; + char *errorbuf; + char *messagebuf; + + /* functions specific to particular drives/interfaces */ + + int (*enable_cdda) (struct cdrom_drive *d, int onoff); + int (*read_toc) (struct cdrom_drive *d); + long (*read_audio) (struct cdrom_drive *d, void *p, long begin, + long sectors); + int (*set_speed) (struct cdrom_drive *d, int speed); + int error_retry; + int report_all; + + int is_atapi; + int is_mmc; + + cdda_private_data_t *private; + void *reserved; + unsigned char inqbytes[4]; + + /* Scsi parameters and state */ + unsigned char density; + unsigned char orgdens; + unsigned int orgsize; + long bigbuff; + int adjust_ssize; + + int fua; + int lun; + + sigset_t sigset; + +} cdrom_drive; + +#define IS_AUDIO(d,i) (!(d->disc_toc[i].bFlags & 0x04)) + +/******** Identification/autosense functions */ + +extern cdrom_drive *cdda_find_a_cdrom(int messagedest, char **message); +extern cdrom_drive *cdda_identify(const char *device, int messagedest, + char **message); +extern cdrom_drive *cdda_identify_cooked(const char *device,int messagedest, + char **message); +extern cdrom_drive *cdda_identify_scsi(const char *generic_device, + const char *ioctl_device, + int messagedest, char **message); +#ifdef CDDA_TEST +extern cdrom_drive *cdda_identify_test(const char *filename, + int messagedest, char **message); +#endif + +/******** Drive oriented functions */ + +extern char *cdda_version(); +extern int cdda_speed_set(cdrom_drive *d, int speed); +extern void cdda_verbose_set(cdrom_drive *d,int err_action, int mes_action); +extern char *cdda_messages(cdrom_drive *d); +extern char *cdda_errors(cdrom_drive *d); + +extern int cdda_close(cdrom_drive *d); +extern int cdda_open(cdrom_drive *d); +extern long cdda_read(cdrom_drive *d, void *buffer, + long beginsector, long sectors); +extern long cdda_read_timed(cdrom_drive *d, void *buffer, + long beginsector, long sectors, int *milliseconds); + +extern long cdda_track_firstsector(cdrom_drive *d,int track); +extern long cdda_track_lastsector(cdrom_drive *d,int track); +extern long cdda_tracks(cdrom_drive *d); +extern int cdda_sector_gettrack(cdrom_drive *d,long sector); +extern int cdda_track_channels(cdrom_drive *d,int track); +extern int cdda_track_audiop(cdrom_drive *d,int track); +extern int cdda_track_copyp(cdrom_drive *d,int track); +extern int cdda_track_preemp(cdrom_drive *d,int track); +extern long cdda_disc_firstsector(cdrom_drive *d); +extern long cdda_disc_lastsector(cdrom_drive *d); + +/* transport errors: */ + +#define TR_OK 0 +#define TR_EWRITE 1 /* Error writing packet command (transport) */ +#define TR_EREAD 2 /* Error reading packet data (transport) */ +#define TR_UNDERRUN 3 /* Read underrun */ +#define TR_OVERRUN 4 /* Read overrun */ +#define TR_ILLEGAL 5 /* Illegal/rejected request */ +#define TR_MEDIUM 6 /* Medium error */ +#define TR_BUSY 7 /* Device busy */ +#define TR_NOTREADY 8 /* Device not ready */ +#define TR_FAULT 9 /* Devive failure */ +#define TR_UNKNOWN 10 /* Unspecified error */ +#define TR_STREAMING 11 /* loss of streaming */ + +static char *strerror_tr[]={ + "Success", + "Error writing packet command to device", + "Error reading command from device", + "SCSI packet data underrun (too little data)", + "SCSI packet data overrun (too much data)", + "Illegal SCSI request (rejected by target)", + "Medium reading data from medium", + "Device busy", + "Device not ready", + "Target hardware fault", + "Unspecified error", + "Drive lost streaming" +}; + +/* Errors returned by lib: + +001: Unable to set CDROM to read audio mode +002: Unable to read table of contents lead-out +003: CDROM reporting illegal number of tracks +004: Unable to read table of contents header +005: Unable to read table of contents entry +006: Could not read any data from drive +007: Unknown, unrecoverable error reading data +008: Unable to identify CDROM model +009: CDROM reporting illegal table of contents +010: Unaddressable sector + +100: Interface not supported +101: Drive is neither a CDROM nor a WORM device +102: Permision denied on cdrom (ioctl) device +103: Permision denied on cdrom (data) device + +300: Kernel memory error + +400: Device not open +401: Invalid track number +402: Track not audio data +403: No audio tracks on disc +404: No medium present +405: Option not supported by drive + +*/ +#endif + diff --git a/include/cdda_paranoia.h b/include/cdda_paranoia.h new file mode 100644 index 0000000..1acf0a5 --- /dev/null +++ b/include/cdda_paranoia.h @@ -0,0 +1,52 @@ +/*** + * CopyPolicy: GNU Lesser General Public License 2.1 applies + * Copyright (C) by Monty (xiphmont@mit.edu) + * + ***/ + +#ifndef _CDROM_PARANOIA_ +#define _CDROM_PARANOIA_ + +#define CD_FRAMEWORDS (CD_FRAMESIZE_RAW/2) + +#define PARANOIA_CB_READ 0 +#define PARANOIA_CB_VERIFY 1 +#define PARANOIA_CB_FIXUP_EDGE 2 +#define PARANOIA_CB_FIXUP_ATOM 3 +#define PARANOIA_CB_SCRATCH 4 +#define PARANOIA_CB_REPAIR 5 +#define PARANOIA_CB_SKIP 6 +#define PARANOIA_CB_DRIFT 7 +#define PARANOIA_CB_BACKOFF 8 +#define PARANOIA_CB_OVERLAP 9 +#define PARANOIA_CB_FIXUP_DROPPED 10 +#define PARANOIA_CB_FIXUP_DUPED 11 +#define PARANOIA_CB_READERR 12 +#define PARANOIA_CB_CACHEERR 13 + +#define PARANOIA_MODE_FULL 0xff +#define PARANOIA_MODE_DISABLE 0 + +#define PARANOIA_MODE_VERIFY 1 +#define PARANOIA_MODE_FRAGMENT 2 +#define PARANOIA_MODE_OVERLAP 4 +#define PARANOIA_MODE_SCRATCH 8 +#define PARANOIA_MODE_REPAIR 16 +#define PARANOIA_MODE_NEVERSKIP 32 + +#ifndef CDP_COMPILE +typedef void cdrom_paranoia; +#endif + +#include + +extern char *paranoia_version(); +extern cdrom_paranoia *paranoia_init(cdrom_drive *d); +extern void paranoia_modeset(cdrom_paranoia *p,int mode); +extern long paranoia_seek(cdrom_paranoia *p,long seek,int mode); +extern int16_t *paranoia_read(cdrom_paranoia *p,void(*callback)(long,int)); +extern int16_t *paranoia_read_limited(cdrom_paranoia *p,void(*callback)(long,int),int maxretries); +extern void paranoia_free(cdrom_paranoia *p); +extern void paranoia_overlapset(cdrom_paranoia *p,long overlap); +extern int paranoia_cachemodel_size(cdrom_paranoia *p,int sectors); +#endif diff --git a/include/utils.h b/include/utils.h new file mode 100644 index 0000000..10dce58 --- /dev/null +++ b/include/utils.h @@ -0,0 +1,106 @@ +#include +#include +#include +#include +#include + +extern long buffering_write(int outf, char *buffer, long num); +extern int buffering_close(int fd); + +/* I wonder how many alignment issues this is gonna trip in the + future... it shouldn't trip any... I guess we'll find out :) */ + +static inline int bigendianp(void){ + int test=1; + char *hack=(char *)(&test); + if(hack[0])return(0); + return(1); +} + +static inline int32_t swap32(int32_t x){ + return((((u_int32_t)x & 0x000000ffU) << 24) | + (((u_int32_t)x & 0x0000ff00U) << 8) | + (((u_int32_t)x & 0x00ff0000U) >> 8) | + (((u_int32_t)x & 0xff000000U) >> 24)); +} + +static inline int16_t swap16(int16_t x){ + return((((u_int16_t)x & 0x00ffU) << 8) | + (((u_int16_t)x & 0xff00U) >> 8)); +} + +#if BYTE_ORDER == LITTLE_ENDIAN + +static inline int32_t be32_to_cpu(int32_t x){ + return(swap32(x)); +} + +static inline int16_t be16_to_cpu(int16_t x){ + return(swap16(x)); +} + +static inline int32_t le32_to_cpu(int32_t x){ + return(x); +} + +static inline int16_t le16_to_cpu(int16_t x){ + return(x); +} + +#else + +static inline int32_t be32_to_cpu(int32_t x){ + return(x); +} + +static inline int16_t be16_to_cpu(int16_t x){ + return(x); +} + +static inline int32_t le32_to_cpu(int32_t x){ + return(swap32(x)); +} + +static inline int16_t le16_to_cpu(int16_t x){ + return(swap16(x)); +} + + +#endif + +static inline int32_t cpu_to_be32(int32_t x){ + return(be32_to_cpu(x)); +} + +static inline int32_t cpu_to_le32(int32_t x){ + return(le32_to_cpu(x)); +} + +static inline int16_t cpu_to_be16(int16_t x){ + return(be16_to_cpu(x)); +} + +static inline int16_t cpu_to_le16(int16_t x){ + return(le16_to_cpu(x)); +} + +static inline char *copystring(const char *s){ + if(s){ + char *ret=malloc((strlen(s)+1)*sizeof(char)); + strcpy(ret,s); + return(ret); + } + return(NULL); +} + +static inline char *catstring(char *buff,const char *s){ + if(s){ + if(buff) + buff=realloc(buff,strlen(buff)+strlen(s)+1); + else + buff=calloc(strlen(s)+1,1); + strcat(buff,s); + } + return(buff); +} + diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..bd9e63b --- /dev/null +++ b/install.sh @@ -0,0 +1,5 @@ +#!/bin/sh +set -eu +PREFIX="${PREFIX:-/usr/local}" +DESTDIR="${DESTDIR:-}" +exec make -C "$(dirname "$0")" install PREFIX="$PREFIX" DESTDIR="$DESTDIR" diff --git a/man/cdparanoia.1 b/man/cdparanoia.1 new file mode 100644 index 0000000..cb625cc --- /dev/null +++ b/man/cdparanoia.1 @@ -0,0 +1,385 @@ +.TH CDPARANOIA 1 "11 Sep 2008" +.SH NAME +cdparanoia 10.2 (Paranoia release III) \- an audio CD reading utility which includes extra data verification features +.SH SYNOPSIS +.B cdparanoia +.RB [ options ] +.B span +.RB [ outfile ] +.SH DESCRIPTION +.B cdparanoia +retrieves audio tracks from CDDA capable CDROM drives. The data can +be saved to a file or directed to standard output in WAV, AIFF, AIFF-C +or raw format. Most ATAPI, SCSI and several proprietary CDROM drive +makes are supported; +.B cdparanoia +can determine if the target drive is CDDA capable. +.P +In addition to simple reading, +.B cdparanoia +adds extra-robust data verification, synchronization, error handling +and scratch reconstruction capability. +.SH OPTIONS + +.TP +.B \-A --analyze-drive +Run and log a complete analysis of drive caching, timing and reading behavior; +verifies that cdparanoia is correctly modelling a sprcific drive's cache and +read behavior. Implies -vQL. + +.TP +.B \-v --verbose +Be absurdly verbose about the autosensing and reading process. Good +for setup and debugging. + +.TP +.B \-q --quiet +Do not print any progress or error information during the reading process. + +.TP +.B \-e --stderr-progress +Force output of progress information to stderr (for wrapper scripts). + +.TP +.B \-l --log-summary [file] +Save result summary to file, default filename cdparanoia.log. + +.TP +.B \-L --log-debug [file] +Save detailed device autosense and debugging output to a file, default filename cdparanoia.log. + +.TP +.B \-V --version +Print the program version and quit. + +.TP +.B \-Q --query +Perform CDROM drive autosense, query and print the CDROM table of +contents, then quit. + +.TP +.B \-s --search-for-drive +Forces a complete search for a cdrom drive, even if the /dev/cdrom link exists. + +.TP +.B \-h --help +Print a brief synopsis of +.B cdparanoia +usage and options. + +.TP +.B \-p --output-raw +Output headerless data as raw 16 bit PCM data with interleaved samples in host byte order. To force little or big endian byte order, use +.B \-r +or +.B \-R +as described below. + +.TP +.B \-r --output-raw-little-endian +Output headerless data as raw 16 bit PCM data with interleaved samples in LSB first byte order. + +.TP +.B \-R --output-raw-big-endian +Output headerless data as raw 16 bit PCM data with interleaved samples in MSB first byte order. + +.TP +.B \-w --output-wav +Output data in Micro$oft RIFF WAV format (note that WAV data is always +LSB first byte order). + +.TP +.B \-f --output-aiff +Output data in Apple AIFF format (note that AIFC data is +always in MSB first byte order). + +.TP +.B \-a --output-aifc +Output data in uncompressed Apple AIFF-C format (note that AIFF-C data is +always in MSB first byte order). + +.TP +.BI "\-B --batch " + +Cdda2wav-style batch output flag; cdparanoia will split the output +into multiple files at track boundaries. Output file names are +prepended with 'track#.' + +.TP +.B \-c --force-cdrom-little-endian +Some CDROM drives misreport their endianness (or do not report it at +all); it's possible that cdparanoia will guess wrong. Use +.B \-c +to force cdparanoia to treat the drive as a little endian device. + +.TP +.B \-C --force-cdrom-big-endian +As above but force cdparanoia to treat the drive as a big endian device. + +.TP +.BI "\-n --force-default-sectors " n +Force the interface backend to do atomic reads of +.B n +sectors per read. This number can be misleading; the kernel will often +split read requests into multiple atomic reads (the automated Paranoia +code is aware of this) or allow reads only wihin a restricted size +range. +.B This option should generally not be used. + +.TP +.BI "\-d --force-cdrom-device " device +Force the interface backend to read from +.B device +rather than the first readable CDROM drive it finds. This can be used +to specify devices of any valid interface type (ATAPI, SCSI or +proprietary). + +.TP +.BI "\-k --force-cooked-device " device +This option forces use of the old 'cooked ioctl' kernel +interface with the specified cdrom device. The cooked ioctl interface +is obsolete in Linux 2.6 if it is present at all. +.B \-k +cannot be used +with +.B \-d +or +.BR \-g . + + +.TP +.BI "\-g --force-generic-device " device +This option forces use of the old 'generic scsi' (sg) kernel +interface with the specified generic scsi device. +.B \-g +cannot be used with +.BR \-k . +.B \-g +may be used with +.B \-d +to explicitly set both the SCSI cdrom and +generic (sg) devices seperately. This option is only useful on +obsolete SCSI setups and when using the generic scsi (sg) driver. + +.TP +.BI "\-S --force-read-speed " number +Use this option explicitly to set the read rate of the CD drive (where +supported). This can reduce underruns on machines with slow disks, or +which are low on memory. + +.TP +.BI "\-t --toc-offset " number +Use this option to force the entire disc LBA addressing to shift by +the given amount; the value is added to the beginning offsets in the +TOC. This can be used to shift track boundaries for the whole disc +manually on sector granularity. The next option does something +similar... + +.TP +.BI "\-T --toc-bias " +Some drives (usually random Toshibas) report the actual track +beginning offset values in the TOC, but then treat the beginning of +track 1 index 1 as sector 0 for all read operations. This results in +every track seeming to start too late (losing a bit of the beginning +and catching a bit of the next track). +.B \-T +accounts for this behavior. Note that this option will cause +cdparanoia to attempt to read sectors before or past the known user +data area of the disc, resulting in read errors at disc edges on most +drives and possibly even hard lockups on some buggy hardware. + +.TP +.BI "\-O --sample-offset " number +Use this option to force the entire disc to shift sample position +output by the given amount; This can be used to shift track boundaries +for the whole disc manually on sample granularity. Note that this will +cause cdparanoia to attempt to read partial sectors before or past the +known user data area of the disc, probably causing read errors on most +drives and possibly even hard lockups on some buggy hardware. + + +.TP +.B \-Z --disable-paranoia +Disable +.B all +data verification and correction features. When using -Z, cdparanoia +reads data exactly as would cdda2wav with an overlap setting of zero. +This option implies that +.B \-Y +is active. + +.TP +.B \-z --never-skip[=max_retries] +Do not accept any skips; retry forever if needed. An optional maximum +number of retries can be specified; for comparison, default without -z is +currently 20. + +.TP +.B \-Y --disable-extra-paranoia +Disables intra-read data verification; only overlap checking at read +boundaries is performed. It can wedge if errors occur in the attempted overlap area. Not recommended. + +.TP +.B \-X --abort-on-skip +If the read skips due to imperfect data, a scratch, whatever, abort reading this track. If output is to a file, delete the partially completed file. + +.SH OUTPUT SMILIES +.TP +.B + :-) +Normal operation, low/no jitter +.TP +.B + :-| +Normal operation, considerable jitter +.TP +.B + :-/ +Read drift +.TP +.B + :-P +Unreported loss of streaming in atomic read operation +.TP +.B + 8-| +Finding read problems at same point during reread; hard to correct +.TP +.B + :-0 +SCSI/ATAPI transport error +.TP +.B + :-( +Scratch detected +.TP +.B + ;-( +Gave up trying to perform a correction +.TP +.B + 8-X +Aborted read due to known, uncorrectable error +.TP +.B + :^D +Finished extracting + +.SH PROGRESS BAR SYMBOLS +.TP +.B + +No corrections needed +.TP +.B + - +Jitter correction required +.TP +.B + + +Unreported loss of streaming/other error in read +.TP +.B + ! +Errors found after stage 1 correction; the drive is making the +same error through multiple re-reads, and cdparanoia is having trouble +detecting them. +.TP +.B + e +SCSI/ATAPI transport error (corrected) +.TP +.B + V +Uncorrected error/skip + +.SH SPAN ARGUMENT + +The span argument specifies which track, tracks or subsections of +tracks to read. This argument is required. +.B NOTE: +Unless the span is a simple number, it's generally a good idea to +quote the span argument to protect it from the shell. +.P +The span argument may be a simple track number or an offset/span +specification. The syntax of an offset/span takes the rough form: +.P +1[ww:xx:yy.zz]-2[aa:bb:cc.dd] +.P +Here, 1 and 2 are track numbers; the numbers in brackets provide a +finer grained offset within a particular track. [aa:bb:cc.dd] is in +hours/minutes/seconds/sectors format. Zero fields need not be +specified: [::20], [:20], [20], [20.], etc, would be interpreted as +twenty seconds, [10:] would be ten minutes, [.30] would be thirty +sectors (75 sectors per second). +.P +When only a single offset is supplied, it is interpreted as a starting +offset and ripping will continue to the end of the track. If a single +offset is preceeded or followed by a hyphen, the implicit missing +offset is taken to be the start or end of the disc, respectively. Thus: + +.TP +.B 1:[20.35] +Specifies ripping from track 1, second 20, sector 35 to the end of +track 1. +.TP +.B 1:[20.35]- +Specifies ripping from 1[20.35] to the end of the disc +.TP +.B \-2 +Specifies ripping from the beginning of the disc up to (and including) track 2 +.TP +.B \-2:[30.35] +Specifies ripping from the beginning of the disc up to 2:[30.35] +.TP +.B 2-4 +Specifies ripping from the beginning of track 2 to the end of track 4. +.P +Again, don't forget to protect square brackets and preceeding hyphens from +the shell. + +.SH EXAMPLES + +A few examples, protected from the shell: +.TP +Query only with exhaustive search for a drive and full reporting of autosense: +.P + cdparanoia -vsQ +.TP +Extract an entire disc, putting each track in a seperate file: +.P + cdparanoia -B +.TP +Extract from track 1, time 0:30.12 to 1:10.00: +.P + cdparanoia "1[:30.12]-1[1:10]" +.TP +Extract from the beginning of the disc up to track 3: +.P + cdparanoia -- "-3" +.TP +The "--" above is to distinguish "-3" from an option flag. +.SH OUTPUT + +The output file argument is optional; if it is not specified, +cdparanoia will output samples to one of +.BR cdda.wav ", " cdda.aifc ", or " cdda.raw +depending on whether +.BR \-w ", " \-a ", " \-r " or " \-R " is used (" \-w +is the implicit default). The output file argument of +.B \- +specifies standard output; all data formats may be piped. + +.SH ACKNOWLEDGEMENTS +Cdparanoia sprang from and once drew heavily from the interface of +Heiko Eissfeldt's (heiko@colossus.escape.de) 'cdda2wav' +package. Cdparanoia would not have happened without it. +.P +Joerg Schilling has also contributed SCSI expertise through his +generic SCSI transport library. +.P +.SH AUTHOR +Monty +.P +Cdparanoia's homepage may be found at: +http://www.xiph.org/paranoia/ diff --git a/man/cdrustanoia.1 b/man/cdrustanoia.1 new file mode 100644 index 0000000..c0f8988 --- /dev/null +++ b/man/cdrustanoia.1 @@ -0,0 +1,385 @@ +.TH CDRUSTANOIA 1 "11 Sep 2008" +.SH NAME +cdrustanoia 10.2 (Paranoia release III) \- an audio CD reading utility which includes extra data verification features +.SH SYNOPSIS +.B cdrustanoia +.RB [ options ] +.B span +.RB [ outfile ] +.SH DESCRIPTION +.B cdrustanoia +retrieves audio tracks from CDDA capable CDROM drives. The data can +be saved to a file or directed to standard output in WAV, AIFF, AIFF-C +or raw format. Most ATAPI, SCSI and several proprietary CDROM drive +makes are supported; +.B cdrustanoia +can determine if the target drive is CDDA capable. +.P +In addition to simple reading, +.B cdrustanoia +adds extra-robust data verification, synchronization, error handling +and scratch reconstruction capability. +.SH OPTIONS + +.TP +.B \-A --analyze-drive +Run and log a complete analysis of drive caching, timing and reading behavior; +verifies that cdparanoia is correctly modelling a sprcific drive's cache and +read behavior. Implies -vQL. + +.TP +.B \-v --verbose +Be absurdly verbose about the autosensing and reading process. Good +for setup and debugging. + +.TP +.B \-q --quiet +Do not print any progress or error information during the reading process. + +.TP +.B \-e --stderr-progress +Force output of progress information to stderr (for wrapper scripts). + +.TP +.B \-l --log-summary [file] +Save result summary to file, default filename cdparanoia.log. + +.TP +.B \-L --log-debug [file] +Save detailed device autosense and debugging output to a file, default filename cdparanoia.log. + +.TP +.B \-V --version +Print the program version and quit. + +.TP +.B \-Q --query +Perform CDROM drive autosense, query and print the CDROM table of +contents, then quit. + +.TP +.B \-s --search-for-drive +Forces a complete search for a cdrom drive, even if the /dev/cdrom link exists. + +.TP +.B \-h --help +Print a brief synopsis of +.B cdparanoia +usage and options. + +.TP +.B \-p --output-raw +Output headerless data as raw 16 bit PCM data with interleaved samples in host byte order. To force little or big endian byte order, use +.B \-r +or +.B \-R +as described below. + +.TP +.B \-r --output-raw-little-endian +Output headerless data as raw 16 bit PCM data with interleaved samples in LSB first byte order. + +.TP +.B \-R --output-raw-big-endian +Output headerless data as raw 16 bit PCM data with interleaved samples in MSB first byte order. + +.TP +.B \-w --output-wav +Output data in Micro$oft RIFF WAV format (note that WAV data is always +LSB first byte order). + +.TP +.B \-f --output-aiff +Output data in Apple AIFF format (note that AIFC data is +always in MSB first byte order). + +.TP +.B \-a --output-aifc +Output data in uncompressed Apple AIFF-C format (note that AIFF-C data is +always in MSB first byte order). + +.TP +.BI "\-B --batch " + +Cdda2wav-style batch output flag; cdparanoia will split the output +into multiple files at track boundaries. Output file names are +prepended with 'track#.' + +.TP +.B \-c --force-cdrom-little-endian +Some CDROM drives misreport their endianness (or do not report it at +all); it's possible that cdparanoia will guess wrong. Use +.B \-c +to force cdparanoia to treat the drive as a little endian device. + +.TP +.B \-C --force-cdrom-big-endian +As above but force cdparanoia to treat the drive as a big endian device. + +.TP +.BI "\-n --force-default-sectors " n +Force the interface backend to do atomic reads of +.B n +sectors per read. This number can be misleading; the kernel will often +split read requests into multiple atomic reads (the automated Paranoia +code is aware of this) or allow reads only wihin a restricted size +range. +.B This option should generally not be used. + +.TP +.BI "\-d --force-cdrom-device " device +Force the interface backend to read from +.B device +rather than the first readable CDROM drive it finds. This can be used +to specify devices of any valid interface type (ATAPI, SCSI or +proprietary). + +.TP +.BI "\-k --force-cooked-device " device +This option forces use of the old 'cooked ioctl' kernel +interface with the specified cdrom device. The cooked ioctl interface +is obsolete in Linux 2.6 if it is present at all. +.B \-k +cannot be used +with +.B \-d +or +.BR \-g . + + +.TP +.BI "\-g --force-generic-device " device +This option forces use of the old 'generic scsi' (sg) kernel +interface with the specified generic scsi device. +.B \-g +cannot be used with +.BR \-k . +.B \-g +may be used with +.B \-d +to explicitly set both the SCSI cdrom and +generic (sg) devices seperately. This option is only useful on +obsolete SCSI setups and when using the generic scsi (sg) driver. + +.TP +.BI "\-S --force-read-speed " number +Use this option explicitly to set the read rate of the CD drive (where +supported). This can reduce underruns on machines with slow disks, or +which are low on memory. + +.TP +.BI "\-t --toc-offset " number +Use this option to force the entire disc LBA addressing to shift by +the given amount; the value is added to the beginning offsets in the +TOC. This can be used to shift track boundaries for the whole disc +manually on sector granularity. The next option does something +similar... + +.TP +.BI "\-T --toc-bias " +Some drives (usually random Toshibas) report the actual track +beginning offset values in the TOC, but then treat the beginning of +track 1 index 1 as sector 0 for all read operations. This results in +every track seeming to start too late (losing a bit of the beginning +and catching a bit of the next track). +.B \-T +accounts for this behavior. Note that this option will cause +cdparanoia to attempt to read sectors before or past the known user +data area of the disc, resulting in read errors at disc edges on most +drives and possibly even hard lockups on some buggy hardware. + +.TP +.BI "\-O --sample-offset " number +Use this option to force the entire disc to shift sample position +output by the given amount; This can be used to shift track boundaries +for the whole disc manually on sample granularity. Note that this will +cause cdparanoia to attempt to read partial sectors before or past the +known user data area of the disc, probably causing read errors on most +drives and possibly even hard lockups on some buggy hardware. + + +.TP +.B \-Z --disable-paranoia +Disable +.B all +data verification and correction features. When using -Z, cdparanoia +reads data exactly as would cdda2wav with an overlap setting of zero. +This option implies that +.B \-Y +is active. + +.TP +.B \-z --never-skip[=max_retries] +Do not accept any skips; retry forever if needed. An optional maximum +number of retries can be specified; for comparison, default without -z is +currently 20. + +.TP +.B \-Y --disable-extra-paranoia +Disables intra-read data verification; only overlap checking at read +boundaries is performed. It can wedge if errors occur in the attempted overlap area. Not recommended. + +.TP +.B \-X --abort-on-skip +If the read skips due to imperfect data, a scratch, whatever, abort reading this track. If output is to a file, delete the partially completed file. + +.SH OUTPUT SMILIES +.TP +.B + :-) +Normal operation, low/no jitter +.TP +.B + :-| +Normal operation, considerable jitter +.TP +.B + :-/ +Read drift +.TP +.B + :-P +Unreported loss of streaming in atomic read operation +.TP +.B + 8-| +Finding read problems at same point during reread; hard to correct +.TP +.B + :-0 +SCSI/ATAPI transport error +.TP +.B + :-( +Scratch detected +.TP +.B + ;-( +Gave up trying to perform a correction +.TP +.B + 8-X +Aborted read due to known, uncorrectable error +.TP +.B + :^D +Finished extracting + +.SH PROGRESS BAR SYMBOLS +.TP +.B + +No corrections needed +.TP +.B + - +Jitter correction required +.TP +.B + + +Unreported loss of streaming/other error in read +.TP +.B + ! +Errors found after stage 1 correction; the drive is making the +same error through multiple re-reads, and cdparanoia is having trouble +detecting them. +.TP +.B + e +SCSI/ATAPI transport error (corrected) +.TP +.B + V +Uncorrected error/skip + +.SH SPAN ARGUMENT + +The span argument specifies which track, tracks or subsections of +tracks to read. This argument is required. +.B NOTE: +Unless the span is a simple number, it's generally a good idea to +quote the span argument to protect it from the shell. +.P +The span argument may be a simple track number or an offset/span +specification. The syntax of an offset/span takes the rough form: +.P +1[ww:xx:yy.zz]-2[aa:bb:cc.dd] +.P +Here, 1 and 2 are track numbers; the numbers in brackets provide a +finer grained offset within a particular track. [aa:bb:cc.dd] is in +hours/minutes/seconds/sectors format. Zero fields need not be +specified: [::20], [:20], [20], [20.], etc, would be interpreted as +twenty seconds, [10:] would be ten minutes, [.30] would be thirty +sectors (75 sectors per second). +.P +When only a single offset is supplied, it is interpreted as a starting +offset and ripping will continue to the end of the track. If a single +offset is preceeded or followed by a hyphen, the implicit missing +offset is taken to be the start or end of the disc, respectively. Thus: + +.TP +.B 1:[20.35] +Specifies ripping from track 1, second 20, sector 35 to the end of +track 1. +.TP +.B 1:[20.35]- +Specifies ripping from 1[20.35] to the end of the disc +.TP +.B \-2 +Specifies ripping from the beginning of the disc up to (and including) track 2 +.TP +.B \-2:[30.35] +Specifies ripping from the beginning of the disc up to 2:[30.35] +.TP +.B 2-4 +Specifies ripping from the beginning of track 2 to the end of track 4. +.P +Again, don't forget to protect square brackets and preceeding hyphens from +the shell. + +.SH EXAMPLES + +A few examples, protected from the shell: +.TP +Query only with exhaustive search for a drive and full reporting of autosense: +.P + cdparanoia -vsQ +.TP +Extract an entire disc, putting each track in a seperate file: +.P + cdparanoia -B +.TP +Extract from track 1, time 0:30.12 to 1:10.00: +.P + cdparanoia "1[:30.12]-1[1:10]" +.TP +Extract from the beginning of the disc up to track 3: +.P + cdparanoia -- "-3" +.TP +The "--" above is to distinguish "-3" from an option flag. +.SH OUTPUT + +The output file argument is optional; if it is not specified, +cdparanoia will output samples to one of +.BR cdda.wav ", " cdda.aifc ", or " cdda.raw +depending on whether +.BR \-w ", " \-a ", " \-r " or " \-R " is used (" \-w +is the implicit default). The output file argument of +.B \- +specifies standard output; all data formats may be piped. + +.SH ACKNOWLEDGEMENTS +Cdparanoia sprang from and once drew heavily from the interface of +Heiko Eissfeldt's (heiko@colossus.escape.de) 'cdda2wav' +package. Cdparanoia would not have happened without it. +.P +Joerg Schilling has also contributed SCSI expertise through his +generic SCSI transport library. +.P +.SH AUTHOR +Monty +.P +Cdparanoia's homepage may be found at: +http://www.xiph.org/paranoia/ diff --git a/tests/capi_compile.c b/tests/capi_compile.c new file mode 100644 index 0000000..090f0ad --- /dev/null +++ b/tests/capi_compile.c @@ -0,0 +1,35 @@ +/* Compile-time check that the installed headers expose the cdparanoia 10.2 + * API and the expected 64-bit Linux ABI layout. */ +#include +#include "cdda_interface.h" +#include "cdda_paranoia.h" + +_Static_assert(sizeof(TOC) == 8, "TOC ABI"); +_Static_assert(offsetof(TOC, bFlags) == 0, "TOC flags"); +_Static_assert(offsetof(TOC, bTrack) == 1, "TOC track"); +_Static_assert(offsetof(TOC, dwStartSector) == 4, "TOC sector"); +_Static_assert(sizeof(cdrom_drive) == 1136, "cdrom_drive ABI"); +_Static_assert(offsetof(cdrom_drive, disc_toc) == 64, "TOC array"); +_Static_assert(offsetof(cdrom_drive, private) == 952, "private pointer"); +_Static_assert(offsetof(cdrom_drive, sigset) == 1008, "sigset"); + +static void use_api(cdrom_drive *d, cdrom_paranoia *p) +{ + (void)cdda_version(); + (void)paranoia_version(); + (void)cdda_open(d); + (void)cdda_read(d, 0, 0, 1); + (void)paranoia_init(d); + paranoia_modeset(p, PARANOIA_MODE_FULL); + (void)paranoia_read(p, 0); + paranoia_free(p); + (void)cdda_close(d); +} + +int main(void) +{ + cdrom_drive *d = 0; + cdrom_paranoia *p = 0; + use_api(d, p); + return 0; +}