first draft

This commit is contained in:
tux
2026-11-03 20:19:21 +01:00
commit 158e115242
44 changed files with 10785 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
target/
*.log
cdda.*
track*.cdda.*
cdparanoia-III-10.2
Generated
+150
View File
@@ -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",
]
+23
View File
@@ -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
+343
View File
@@ -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.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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.
<signature of Ty Coon>, 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.
+508
View File
@@ -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.
<one line to give the library's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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.
<signature of Ty Coon>, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!
+57
View File
@@ -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
+141
View File
@@ -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 `<image>.<ext>.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.
+13
View File
@@ -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
+165
View File
@@ -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<unsafe extern "C" fn(*mut CdromDrive, c_int) -> c_int>;
pub type ReadTocFn = Option<unsafe extern "C" fn(*mut CdromDrive) -> c_int>;
pub type ReadAudioFn =
Option<unsafe extern "C" fn(*mut CdromDrive, *mut c_void, c_long, c_long) -> c_long>;
pub type SetSpeedFn = Option<unsafe extern "C" fn(*mut CdromDrive, c_int) -> 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::<Toc>(), 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::<CdromDrive>(), 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);
}
}
+16
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
fn main() {
println!("cargo:rustc-cdylib-link-arg=-Wl,-soname,libcdda_interface.so.0");
}
+571
View File
@@ -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<dyn Drive>,
}
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<dyn Drive>,
) -> *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<usize, ReadError> {
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) {}
+19
View File
@@ -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" }
+3
View File
@@ -0,0 +1,3 @@
fn main() {
println!("cargo:rustc-cdylib-link-arg=-Wl,-soname,libcdda_paranoia.so.0");
}
+314
View File
@@ -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<unsafe extern "C" fn(c_long, c_int)>;
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<usize, ReadError> {
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);
}
}
}
+25
View File
@@ -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"
+295
View File
@@ -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
}
+420
View File
@@ -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<Option<String>>,
pub log_debug: Option<Option<String>>,
pub version: bool,
pub query: bool,
pub batch: bool,
pub search: bool,
pub help: bool,
pub output_format: OutputFormat,
pub force_endian: Option<i32>,
pub force_sectors: Option<i32>,
pub force_overlap: Option<i32>,
pub force_device: Option<String>,
pub force_cooked_device: Option<String>,
pub force_generic_device: Option<String>,
pub force_speed: Option<i32>,
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<String>,
pub span: Option<String>,
pub outfile: Option<String>,
}
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<Short> {
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<String>) -> 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<Args, String> {
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<char> = 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<Args, String> {
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());
}
}
+4
View File
@@ -0,0 +1,4 @@
fn main() {
let argv: Vec<String> = std::env::args().collect();
std::process::exit(cdrustanoia_cli::run(&argv));
}
+806
View File
@@ -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 <monty@xiph.org> and Xiph.Org
Report bugs to paranoia@xiph.org
http://www.xiph.org/paranoia/
USAGE:
cdparanoia [options] <span> [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 [<file>] : save result summary to file, default
filename cdparanoia.log
-L --log-debug [<file>] : 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 <n> : force default number of sectors in read
to n sectors
-o --force-search-overlap <n> : force minimum overlap search during
verification to n sectors
-d --force-cdrom-device <dev> : use specified device; disallow
autosense
-k --force-cooked-device <dev> : 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 <dev> : use specified generic scsi device and
force use of the old SG kernel
interface. -g cannot be used with -k.
-S --force-read-speed <n> : read from device at specified speed; by
default, cdparanoia sets drive to full
speed.
-t --toc-offset <n> : Add <n> 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 <n> : Add <n> 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:
<space> 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<Box<dyn Write>, 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<dyn Write>)
.map_err(|e| format!("Cannot open log file {}: {}", name, e))
}
}
fn setup_logs(args: &Args, argv: &[String]) -> Result<Log, String> {
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<Vec<i16>> {
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<String>,
batch: bool,
track: i32,
format: OutputFormat,
) -> Option<String> {
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<File, String> {
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<Box<dyn Drive>, 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(&sectors) {
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
}
+121
View File
@@ -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<Box<dyn Write>>,
pub report_file: Option<Box<dyn Write>>,
}
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<dyn Write>) {
self.summary_file = Some(writer);
}
pub fn set_report_writer(&mut self, writer: Box<dyn Write>) {
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)*))
};
}
+4
View File
@@ -0,0 +1,4 @@
fn main() {
let argv: Vec<String> = std::env::args().collect();
std::process::exit(cdrustanoia_cli::run(&argv));
}
+226
View File
@@ -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: Write>(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: Write>(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: Write>(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: Write>(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: Write>(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<u8>,
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<usize> {
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");
}
}
+351
View File
@@ -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<u8> =
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);
}
}
+271
View File
@@ -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<String>) -> SpanError {
SpanError(msg.into())
}
#[derive(Default, Debug, Clone, Copy)]
struct TimeParts {
hours: Option<i64>,
minutes: Option<i64>,
seconds: Option<i64>,
sectors: Option<i64>,
}
fn parse_num(s: &str, original: &str) -> Result<i64, SpanError> {
let digits: String = s.chars().take_while(|c| c.is_ascii_digit()).collect();
if digits.is_empty() {
Ok(0)
} else {
digits
.parse::<i64>()
.map_err(|_| err(format!("Error parsing span argument: {original}")))
}
}
fn parse_time(time: &str) -> Result<TimeParts, SpanError> {
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<i64>,
) -> Result<i64, SpanError> {
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::<i64>()
.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());
}
}
+50
View File
@@ -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");
}
+11
View File
@@ -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"
+16
View File
@@ -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;
+138
View File
@@ -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<ImageDrive> {
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<Box<dyn Drive>, 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<Box<dyn Drive>, 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<Box<dyn Drive>, 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<Box<dyn Drive>, String> {
if force_cooked {
identify_cooked(device, verbose)
} else {
identify_device(device, verbose)
}
}
pub fn find_a_cdrom(verbose: bool) -> Option<Box<dyn Drive>> {
if let Some(img) = image_override() {
return Some(Box::new(img));
}
let mut candidates: Vec<String> = 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",
}
}
+323
View File
@@ -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<i16>,
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<u8>) -> 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<u8>, 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<Self> {
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<i16> {
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<i16> {
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<usize, ReadError> {
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<usize, ReadError> {
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
}
}
File diff suppressed because it is too large Load Diff
+422
View File
@@ -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<String>) -> 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<String>) -> 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<usize, ReadError>,
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<usize, ReadError>;
/// 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<usize, ReadError> {
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<i16> {
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<u8> {
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);
}
}
+27
View File
@@ -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
}
+195
View File
@@ -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<i16>,
pub flags: Option<Vec<u8>>,
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<i16>, 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<i16>,
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<CBlock>,
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,
}
File diff suppressed because it is too large Load Diff
+87
View File
@@ -0,0 +1,87 @@
//! Sample-value index, a safe Rust equivalent of isort.c.
use std::collections::HashMap;
pub struct SortIndex {
buckets: HashMap<i16, Vec<u32>>,
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<usize> {
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<usize> {
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<i64> {
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()
}
}
+130
View File
@@ -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<i16> {
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<i16> {
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);
}
+207
View File
@@ -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 <sys/types.h>
#include <signal.h>
#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
+52
View File
@@ -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 <stdio.h>
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
+106
View File
@@ -0,0 +1,106 @@
#include <stdlib.h>
#include <endian.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
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);
}
Executable
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
set -eu
PREFIX="${PREFIX:-/usr/local}"
DESTDIR="${DESTDIR:-}"
exec make -C "$(dirname "$0")" install PREFIX="$PREFIX" DESTDIR="$DESTDIR"
+385
View File
@@ -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
<space>
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 <monty@xiph.org>
.P
Cdparanoia's homepage may be found at:
http://www.xiph.org/paranoia/
+385
View File
@@ -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
<space>
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 <monty@xiph.org>
.P
Cdparanoia's homepage may be found at:
http://www.xiph.org/paranoia/
+35
View File
@@ -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 <stddef.h>
#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;
}