109 Commits
next ... 1.2.0

Author SHA1 Message Date
2b92ee0ce1 finalize 1.2.0 2018-11-25 12:13:30 -05:00
c4acca18d8 review the --help message 2018-11-24 20:02:24 -05:00
b7e133d6ba add exmaple to the man page 2018-11-24 12:05:55 -05:00
5b5b67a0df clean-up the includes 2018-11-24 11:56:43 -05:00
80a4b2ccf6 rewrite ot::read_comments with getline 2018-11-24 11:44:15 -05:00
d1299360de smart ot::file handle 2018-11-24 11:33:04 -05:00
bfa46273b9 fix ot::read_comments when handling empty lines 2018-11-24 11:33:04 -05:00
26411d3843 t: test ot::read_comments 2018-11-24 11:33:04 -05:00
af61b01448 substitute the @-markers in the man page 2018-11-21 21:41:55 -05:00
a043e74e14 review the user doc 2018-11-21 21:40:08 -05:00
20dc8d0fa2 add a changelog 2018-11-21 21:02:42 -05:00
407c12c7ac review the overall code documentation 2018-11-19 19:23:11 -05:00
8949094203 run: return better status 2018-11-18 11:43:45 -05:00
ddb838ac81 process: return better errors 2018-11-18 11:43:29 -05:00
62d56aafff accompany returned status codes with a message 2018-11-18 11:04:11 -05:00
b9a0ece567 include the error message in ot::status 2018-11-18 10:42:27 -05:00
5445c5bc7c t: test the ogg writer 2018-11-18 10:15:49 -05:00
cc83a438ae t: tests for ogg_reader 2018-11-18 09:45:11 -05:00
6ed0326a74 t: copy gobble.opus to the binary directory 2018-11-18 09:34:22 -05:00
0980b35ecd polish the interface of the opus module 2018-11-17 17:34:51 -05:00
2670b661a8 don't create null ogg writers 2018-11-17 17:17:59 -05:00
c604fdb667 encapsulate ogg_writer 2018-11-17 17:07:14 -05:00
8334a5617f polish ogg_reader 2018-11-17 16:10:20 -05:00
cdd591c0c1 really close the files before moving them 2018-11-17 15:40:57 -05:00
e22a1d381a hide the reader's stream in the ogg module 2018-11-16 19:10:14 -05:00
121220ea05 rewrite run with RAII in mind 2018-11-16 18:29:40 -05:00
b6c7a90d92 move run into the cli module
Now the code has been wholly reorganized!
2018-11-14 20:15:30 -05:00
2e88bdc207 t: cli.t -> opustags.t
It reflects the module it tests.
2018-11-14 19:56:51 -05:00
22bfd05b36 move the main loop to ot::process 2018-11-14 19:56:23 -05:00
8a5b80e075 process_tags function in the main module 2018-11-14 18:51:04 -05:00
e41cf918d1 RAII interface for dynamic ogg packets 2018-11-13 20:46:30 -05:00
82ff7f7751 validate_identification_header: take the ogg_packet 2018-11-13 18:51:28 -05:00
351d6149c9 identification header check in opus.cc 2018-11-13 18:45:44 -05:00
9ed2b82b4a error: static assertion of the list of messages 2018-11-13 18:35:34 -05:00
1866dbd1f0 call stderror for ot::status:standard_error 2018-11-13 18:15:43 -05:00
5ff99b620c ot::error_message 2018-11-13 18:04:26 -05:00
b0e8813be6 t: introduce tap.h 2018-11-13 18:04:08 -05:00
c17ad7853c move print_comments in cli, next to read_comments 2018-11-11 12:04:16 -05:00
632caae915 dedicated function for checking if two files are the same 2018-11-11 11:57:25 -05:00
b9dbaf1049 finish moving the argv checks to cli 2018-11-11 11:35:05 -05:00
326ae74afa t: factor opustags_binary and opustags together 2018-11-11 11:19:56 -05:00
497caaa8f3 t: simply the prototype of Perl's opustags 2018-11-11 11:12:26 -05:00
6565cb56b3 t: faithful copy without --overwrite 2018-11-11 11:01:13 -05:00
f664ed94d4 t: generate out.opus in the binary dir 2018-11-11 10:58:33 -05:00
b5dc595855 move the help and some arguments checking in cli.cc 2018-11-11 10:54:54 -05:00
bf386899ae fix a few signedness warnings 2018-11-11 10:30:48 -05:00
51a3eba093 dedicated function for set-all's parsing 2018-11-11 10:24:18 -05:00
fae547c4eb t: rename unit.t to opus.t 2018-11-10 14:07:14 -05:00
3aeb2097de cmake: factor the libogg dependency 2018-11-10 14:00:13 -05:00
132073b842 move argument parsing to cli.cc 2018-11-10 11:52:33 -05:00
2a31c5491b use std::string instead of ot::string_view
String views are cool, but let's play it safe and standard for now. The
impact on performance is insignificant, since most of the job is reading
the ogg file, not actually manipulating tags.
2018-11-10 11:30:30 -05:00
1b9bd83e8f store path_in and path_out as std::string 2018-11-10 11:24:07 -05:00
f02ff44e43 struct for the CLI arguments 2018-11-10 11:04:13 -05:00
b7f85b5fe2 main: store to_add and to_delete in std::vector 2018-11-10 10:54:21 -05:00
c338a04196 string_view: convert from std::string 2018-11-10 10:52:20 -05:00
0426c369be t: test --overwrite 2018-11-10 10:26:12 -05:00
74cc6038b2 t: pass opus data in stdin/stdout 2018-11-10 10:24:14 -05:00
702f86a355 ogg_reader::read_page() 2018-11-09 18:30:11 -05:00
0c4c11032f general status code enum 2018-11-09 18:28:21 -05:00
72a911c11b RAII for the stream and sync states 2018-11-09 18:00:52 -05:00
07af78519b group ogg encoding/decoding variables together 2018-11-09 17:28:03 -05:00
2905b193b1 t: trigger all the possible parse errors 2018-11-07 20:46:07 -05:00
9d3e9c20a3 make parse_tags return a precise error code 2018-11-07 20:31:08 -05:00
0b4e01c3b0 delete add_tags, it had become too simple 2018-11-06 21:14:40 -05:00
cc5896b1a0 move print_tags in the main module
That's where it belongs. The other modules are not supposed to write
anything to the console.
2018-11-06 21:07:43 -05:00
1744cab9ed string_view: expose data and size as functions
Be consistent with C++17.
2018-11-06 21:01:03 -05:00
d9b96d471d store the vendor as a string_view 2018-11-06 20:51:55 -05:00
590a6814dd delete free_tags, now useless thanks to RAII 2018-11-06 20:48:13 -05:00
7ae7a50151 store comments in a std::list 2018-11-06 20:46:34 -05:00
0df7514a83 preserve the extra data after the comments 2018-11-06 18:46:40 -05:00
bd50fb34d9 t: recode a packet with padding (fails) 2018-11-05 19:03:21 -05:00
3e77092f85 t: check that render_tags is faithful 2018-11-05 18:53:01 -05:00
a3a6cb4e36 t: check parse_tags on a simple sample 2018-11-05 18:41:14 -05:00
af988efd8a configure cmake for unit tests 2018-11-04 18:24:29 -05:00
f2a60e4220 overall documentation for opus.cc 2018-11-04 17:47:50 -05:00
002b253c06 t: safer calls to opustags 2018-11-04 14:15:49 -05:00
62ea90e5d5 t: merge tags.t and meta.t in cli.t 2018-11-04 13:15:27 -05:00
098eefe60f explicit use of the ot namespace 2018-11-03 17:25:14 -04:00
3ba7ba8166 create ogg.cc for libogg helpers 2018-11-03 17:22:31 -04:00
3c0aad169b move the opus-related functions in opus.cc 2018-11-03 17:18:15 -04:00
06520bf87e create opustags.h 2018-11-03 16:52:58 -04:00
7fb5b49b81 move the sources in src/ 2018-11-02 16:56:53 -04:00
a2eb11cbe3 make check depends on opustags 2018-10-31 18:24:58 -04:00
dd364c6262 t: check the exit code when called without options 2018-10-31 18:22:48 -04:00
a3e7624866 get the version number from the cmake project 2018-10-31 18:21:47 -04:00
65aad6f62a build the project with cmake 2018-10-30 19:14:34 -04:00
241c9b3071 t: allow running the suite from an arbitrary directory 2018-10-30 19:12:58 -04:00
dd0faa29bc don't print "no tags"
It's undesired, especially if the output is piped somewhere else.

It *could* be printed to stderr alternatively, but better not say
anything when there is nothing to say.
2018-10-30 18:29:08 -04:00
1837f0b0ec build as C++14 2018-10-30 18:28:12 -04:00
1e6698af3e t: check malformed tags 2018-10-29 18:46:37 -04:00
82d0400207 t: complex --set-all 2018-10-29 18:26:21 -04:00
24b6268d7a .gitignore 2018-10-28 20:02:38 -04:00
e91ad48c10 t: delete the temporary opus file at the end 2018-10-28 20:01:57 -04:00
9c50d7d047 t: set all, delete all, and final touches 2018-10-28 20:00:48 -04:00
3624761c7b t: complex tag editing 2018-10-28 19:56:00 -04:00
f56ade7941 delete_tags did not delete multiple tags correctly 2018-10-28 19:56:00 -04:00
15335da1f8 t: check -h too 2018-10-28 19:56:00 -04:00
2006431fa8 t: trivial manipulations 2018-10-28 18:41:26 -04:00
63fce2f555 t: read tags from a file 2018-10-28 18:17:52 -04:00
2181f9f0eb t: use git to detect the version number 2018-10-28 14:12:55 -04:00
69561ae05f update the copyright notice 2018-10-27 20:36:18 -04:00
5dcf9ec543 add make check 2018-10-27 20:33:43 -04:00
7cf478c9cc meta tests 2018-10-27 20:33:43 -04:00
2f98bba07c README: update the status of the project 2018-10-27 20:23:24 -04:00
e44ad86af3 add a contributing guide 2018-10-27 20:23:16 -04:00
7174a1f2f2 bump to 1.1.1 2018-10-24 18:27:57 -04:00
1a8aaff933 Don't croak on overlong opustags 2018-10-02 18:48:09 -04:00
4973a4deab Become macOS compatible 2018-10-02 18:48:09 -04:00
8e9d98ac62 README: show alternatives 2017-10-01 12:26:56 +02:00
69 changed files with 1954 additions and 3072 deletions

2
.gitignore vendored
View File

@ -1,3 +1 @@
/build
tests/catch.h
src/version.h

34
CHANGELOG.md Normal file
View File

@ -0,0 +1,34 @@
opustags changelog
==================
1.2.0 - 2018-11-25
------------------
- Preserve extra data in OpusTags past the comments.
- Improve error reporting.
- Fix various bugs.
This is the biggest release for opustags. The whole code base was reviewed for robustness and
clarity. The program is now built as C++14, and the code refactored without sacrificing the
original simplicity. It is shipped with a new test suite.
1.1.1 - 2018-10-24
------------------
- Mac OS X support.
- Tolerate but truncate the data in the OpusTags packet past the comments.
1.1 - 2013-01-02
----------------
- Add the --in-place option.
- Fix a bug is --set-all where the last unterminated line was ignored.
- Remove broken output files on failure.
1.0 - 2013-01-01
----------------
This is the first release of opustags. It supports all the main feature for basic tag editing.
It was written in a day, and the code is quick and dirty, though the program is simple and
efficient.

View File

@ -1,78 +1,36 @@
cmake_minimum_required (VERSION 2.8.8)
project (opustags)
cmake_minimum_required(VERSION 3.9)
project(
opustags
VERSION 1.2.0
LANGUAGES CXX
)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED on)
set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/modules/")
set(CMAKE_SOURCE_DIR "${CMAKE_BINARY_DIR}/../")
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# ------------
# Dependencies
# ------------
find_package(Ogg REQUIRED)
include_directories(${Ogg_INCLUDE_DIR})
link_directories(${Ogg_LIBRARY_DIRS})
find_package(PkgConfig REQUIRED)
pkg_check_modules(OGG REQUIRED ogg)
add_compile_options(${OGG_CFLAGS})
# --------------------
# Global build options
# --------------------
if(CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wextra")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pedantic")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wold-style-cast")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-parameter")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14") # for MinGW-w64
endif()
configure_file(src/config.h.in config.h @ONLY)
include_directories(BEFORE src "${CMAKE_BINARY_DIR}")
# -------
# Version
# -------
execute_process(COMMAND git describe --tags --abbrev=0 OUTPUT_VARIABLE VERSION_SHORT OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(COMMAND git describe --always --dirty --long --tags OUTPUT_VARIABLE VERSION_LONG OUTPUT_STRIP_TRAILING_WHITESPACE)
if("${VERSION_SHORT}" STREQUAL "")
set(VERSION_SHORT "0.0")
set(VERSION_LONG "?")
endif()
configure_file("${CMAKE_SOURCE_DIR}/src/version.h.in" "${CMAKE_SOURCE_DIR}/src/version.h" @ONLY)
add_library(
libopustags
OBJECT
src/cli.cc
src/ogg.cc
src/opus.cc
)
target_link_libraries(libopustags PUBLIC ${OGG_LIBRARIES})
# ------------
# Source files
# ------------
file(GLOB_RECURSE common_sources "${CMAKE_SOURCE_DIR}/src/*.cc")
file(GLOB_RECURSE common_headers "${CMAKE_SOURCE_DIR}/src/*.h")
file(GLOB_RECURSE test_sources "${CMAKE_SOURCE_DIR}/tests/*.cc")
file(GLOB_RECURSE test_headers "${CMAKE_SOURCE_DIR}/tests/*.h")
list(REMOVE_ITEM common_sources "${CMAKE_SOURCE_DIR}/src/main.cc")
list(REMOVE_ITEM test_sources "${CMAKE_SOURCE_DIR}/tests/main.cc")
add_executable(opustags src/opustags.cc)
target_link_libraries(opustags libopustags)
# -------------------
# 3rd party libraries
# -------------------
# Catch
set(CATCH_PATH "${CMAKE_SOURCE_DIR}/tests/catch.h")
if (NOT EXISTS "${CATCH_PATH}")
message("Downloading Catch...")
file(DOWNLOAD "http://raw.githubusercontent.com/philsquared/Catch/master/single_include/catch.hpp" "${CATCH_PATH}")
endif()
include(GNUInstallDirs)
install(TARGETS opustags DESTINATION "${CMAKE_INSTALL_BINDIR}")
configure_file(opustags.1 . @ONLY)
install(FILES "${CMAKE_BINARY_DIR}/opustags.1" DESTINATION "${CMAKE_INSTALL_MANDIR}/man1")
# ------------
# Installation
# ------------
install(FILES ${CMAKE_SOURCE_DIR}/doc/opustags.1 DESTINATION ${CMAKE_INSTALL_PREFIX}/man/man1)
install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/opustags DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
add_custom_target(uninstall COMMAND
rm -f "${CMAKE_INSTALL_PREFIX}/man/man1/opustags.1" &&
rm -f "${CMAKE_INSTALL_PREFIX}/bin/opustags" )
# -------------------
# Linking definitions
# -------------------
add_library(common OBJECT ${common_sources} ${common_headers})
add_executable(opustags "${CMAKE_SOURCE_DIR}/src/main.cc" $<TARGET_OBJECTS:common>)
add_executable(run_tests "${CMAKE_SOURCE_DIR}/tests/main.cc" $<TARGET_OBJECTS:common> ${test_sources} ${test_headers})
target_link_libraries(opustags ${OGG_LIBRARY})
target_link_libraries(run_tests ${OGG_LIBRARY})
target_include_directories(common BEFORE PUBLIC "${CMAKE_SOURCE_DIR}/src")
target_include_directories(opustags BEFORE PUBLIC "${CMAKE_SOURCE_DIR}/src")
target_include_directories(run_tests BEFORE PUBLIC "${CMAKE_SOURCE_DIR}/src")
target_include_directories(run_tests BEFORE PUBLIC "${CMAKE_SOURCE_DIR}/tests")
add_subdirectory(t)

59
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,59 @@
# Contributing to opustags
opustags is slowing getting more mature, and contributions are welcome.
Before you open a pull request, you might want to talk about the change you'd
like to make to make sure it's relevant. In that case, feel free to open an
issue. You can expect a response within a week.
## Submitting pull requests
opustags has nothing really special, so basic git etiquette is just enough.
Please make focused pull requests, one feature at a time. Don't make huge
commits. Give clear names to your commits and pull requests. Extended
descriptions are welcome.
Stay objective in your changes. Adding a feature or fixing a bug is a clear
improvement, but stylistic changes like renaming a function or moving a few
braces around won't help the project move forward.
You should check that your changes don't break the test suite by running
`make check`
Following these practices is important to keep the history clean, and to allow
for better code reviews.
## History of opustags
opustags is originally a small project made to fill a need to edit tags in Opus
audio files when most taggers didn't support Opus at all. It was written in C
with libogg, and should be very light and fast compared to most alternatives.
However, because it was written on a whim, the code is hardly structured and
might even be fragile, who knows.
An ambitious desire to rewrite it in C++ with bells and whistles gave birth to
the `next` branch, but sadly it wasn't finalized and is currently not usable,
though it contains good pieces of code.
With the growing support of Opus in tag editors, the usefulness of opustags was
questioned, and it was thus abandoned for a few years. Judging by the
inquiries and contributions, albeit few, on GitHub, it looks like it remains
relevant, so let's dust it off a bit.
Today, opustags is written in C++14 and features a unit test suite in C++, and
an integration test suite in Perl. The code was refactored, organized into
modules, and reviewed for safety.
The next release will focus on correctness, with the following technical
objectives:
1. Validate the comments: field name in ASCII and value in UTF-8.
2. Allow selecting the stream to edit, instead of assuming the Ogg contains only
one Opus stream.
3. Provide an --escape option for escaping the newlines inside comment strings.
4. Take into account the system's encoding: the tags must always be stored as
UTF-8, and converted from and to the console encoding when reading input or
printing.
5. Maybe provide a --binary option to dump the raw OpusTags packet, that can be
combined to --set-all to read it back.

View File

@ -1,4 +1,4 @@
Copyright (c) 2013, Frédéric Mangano
Copyright (c) 2013-2018, Frédéric Mangano-Tarumi
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,

View File

@ -3,43 +3,63 @@ opustags
View and edit Opus comments.
The current code quality of this project is getting better, and is suitable for reliably editing any
Opus file provided it does not contain other multiplexed streams. Only UTF-8 is currently supported.
Until opustags becomes top-quality software, if it ever does, you might want to
check out these more mature tag editors:
- [EasyTAG](https://wiki.gnome.org/Apps/EasyTAG)
- [Beets](http://beets.io/)
- [Picard](https://picard.musicbrainz.org/)
- [puddletag](http://docs.puddletag.net/)
- [Quod Libet](https://quodlibet.readthedocs.io/en/latest/)
- [Goggles Music Manager](https://gogglesmm.github.io/)
See also these libraries if you need a lower-level access:
- [TagLib](http://taglib.org/)
- [mutagen](https://mutagen.readthedocs.io/en/latest/)
Requirements
------------
* A POSIX-compliant system,
* `libogg`.
* a C++14 compiler,
* CMake,
* a POSIX-compliant system,
* libogg.
Installing
----------
mkdir build && cd build
cmake ..
opustags is a commonplace CMake project.
Here's how to install it in your `.local`, under your home:
mkdir build
cd build
cmake -DCMAKE_INSTALL_PREFIX=~/.local ..
make
make install
Note that you don't need to install opustags in order to run it, as the executable is standalone.
Documentation
-------------
Usage: opustags --help
opustags [OPTIONS] INPUT
opustags [OPTIONS] -o OUTPUT INPUT
opustags [OPTIONS] FILE
opustags OPTIONS FILE -o FILE
Options:
-h, --help print this help
-V, --version print version
-o, --output FILE write the modified tags to this file
-i, --in-place [SUFFIX] use a temporary file then replace the original file
-o, --output FILE set the output file
-i, --in-place overwrite the input file instead of writing a different output file
-y, --overwrite overwrite the output file if it already exists
--stream ID select stream for the next operations
-l, --list display a pretty listing of all tags
--no-color disable colors in --list output
-d, --delete FIELD delete all the fields of a specified type
-a, --add FIELD=VALUE add a field
-s, --set FIELD=VALUE delete then add a field
-D, --delete-all delete all the fields!
--full enable full file scan
--export dump the tags to standard output for --import
--import set the tags from scratch basing on stanard input
-e, --edit spawn the $EDITOR and apply --import on the result
-a, --add FIELD=VALUE add a comment
-d, --delete FIELD delete all previously existing comments of a specific type
-D, --delete-all delete all the previously existing comments
-s, --set FIELD=VALUE replace a comment (shorthand for --delete FIELD --add FIELD=VALUE)
-S, --set-all replace all the comments with the ones read from standard input
See the man page, `opustags.1`, for extensive documentation.

View File

@ -1,209 +0,0 @@
.TH opustags 1 "2016"
.SH NAME
opustags \- Opus comment editor
.SH SYNOPSIS
.B opustags \-\-help
.br
.B opustags
.RI [ OPTIONS ]
.I INPUT
.br
.B opustags
.RI [ OPTIONS ]
.B \-o
.I OUTPUT INPUT
.SH DESCRIPTION
.PP
\fBopustags\fP can read and edit the comment header of an Opus file.
It has two modes of operation: read\-only for tag listing, and read\-write for
tag edition.
.PP
Edition mode is triggered by the \fB\-\-ouput\fP and \fB\-\-in\-place\fP
options. Otherwise, the default mode is listing.
.PP
Edition options are incompatible in listing mode, the same way as listing
options are incompatible in edition mode.
.PP
\fIINPUT\fP can either be the name of a file or \fB\-\fP to read from standard input.
.SS Listing
.PP
In listing mode, the tags are printed on standard output. Two listing formats
are available: pretty and raw. By default, pretty listing is implied if the
output is a terminal, and raw listing if it isn't.
.PP
Pretty listing is enabled with the option \fB\-\-list\fP. Colors are enabled
unless you use the \fB\-\-no\-color\fP option.
.PP
Dumping with \fB\-\-export\fP outputs the raw tags in UTF-8, for automatic
processing. You can restore exported tags with the \fB\-\-import\fP option.
.SS Editing
.PP
As for the edition mode, you need to specify an output file (or \fB\-\fP for
standard output). It must be different from the input file. You may want to
use \fB\-\-overwrite\fP if you know what you're doing. To overwrite the input
file, use \fB\-\-in\-place\fP.
.PP
Tag edition is done with the \fB\-\-add\fP, \fB\-\-delete\fP and \fB\-\-set\fP
options. You can use these options as many times as you wish.
.PP
You can delete all the tags with \fB\-\-delete\-all\fP. This operation can be
combined with \fB\-\-add\fP to set new tags without being bothered by the old
ones.
.PP
If you want to process tags yourself, you can use the \fB\-\-import\fP option
which will cause \fBopustags\fP to read tags from standard input. The format
is the same as the one used for output with \fB\-\-export\fP.
Note that this option implies \fB\-\-delete\-all\fP.
Also, input is read as UTF-8.
.PP
You can use \fB\-\-edit\fP to spawn your \fBEDITOR\fP and edit the tags
interactively. In read\-write mode, if no other option is specified, this one
is implied.
.SS Stream Selection
.PP
In case an Ogg file contains multiple streams, the \fB\-\-stream\fP option lets
you specify the particular streams to modify. You can modify several streams in
different ways by using this option more than once. Only operations specified
after this option and relevant, and until another \fB\-\-stream\fP option is
found.
.PP
opustags \-\-stream 1 \-\-stream 2 \-\-list # BAD
.PP
The above command would only show the second stream. To display both streams,
use instead:
.PP
opustags \-\-stream 1,2 \-\-list
.PP
See also the examples at the end of this page, and the documentation of the
\fB\-\-stream\fP option.
.PP
You can easily find the id's of the available streams by calling opustags with
no options to list the tags. Usually, you'll only have one stream, id 1.
.SH OPTIONS
.SS General
.TP
.B \-h, \-\-help
Display a brief description of the options.
.TP
.B \-V, \-\-version
Display the version of opustags.
.TP
.B \-\-stream \fIID\fP
Specify the stream that will be affected by the options following it. The
special id \fBall\fP selects all the Opus streams found, and is the default
value.
.sp
The identifer of a stream is determined from its position in the file. The
first stream found will have id 1, the second 2, and so on.
.sp
You can select more than one stream with this option by separated id's with commas. For example: \-\-stream 1,3. Ranges are not supported.
.sp
In edition mode, this option should be set if the input file contains more than
one Opus stream, otherwise a warning is generated. If in\-place modification is
selected, this warning becomes a fatal error.
.SS Listing
.TP
.B \-l, \-\-list
Display a pretty listing of all the tags found. This is the default option in
read\-only mode if the standard output is a terminal.
.TP
.B \-\-no\-color
By default, when listing tags with \fB\-\-list\fP, colors are used. Use this
option to disable it.
.TP
.B \-\-export
Dump the tags on standard output in a format compatible with \fB\-\-import\fP.
If only one stream is specified, the output format is compatible with other
tools such as vorbiscomment.
.sp
When exporting, output is encoded in UTF\-8 in
order not to lose information.
.TP
.B \-\-full
For performance reasons, only the beginning of the input is read, because
that's where tags are expected. If, however, you think you have an
unconventional file and you suspect that opustags is missing some streams, you
can use this option to force it to read the whole file.
.sp
This option shouldn't be needed, but if you do find files that require this
option, please submit a bug report (see the bottom of the page).
.SS Edition
.TP
.B \-o, \-\-output \fIFILE\fI
Edition mode. The input file will be read, its tags edited, then written to the
specified output file. If \fIFILE\fP is \fB\-\fP then the resulting Opus file
will be written to standard output. The output file can't be the same as the
input file, use \fB\-\-in\-place\fP instead. This option may be specified at
most once.
.TP
.B \-i, \-\-in\-place \fR[\fP\fISUFFIX\fP\fR]\fP
Use this when you want to modify the input file in\-place. This creates a
temporary file with the specified suffix (\fI.otmp\fP by default). This implies
\fB\-\-overwrite\fP in that if a file with the same temporary name already
exists, it will be overwritten without warning. Of course, this overwrites the
input file too. You cannot use this option when the input file is the standard
input.
.TP
.B \-y, \-\-overwrite
By default, opustags refuses to overwrite an already existent file. Use
this option to allow that.
.TP
.B \-d, \-\-delete \fIFIELD\fP
Delete all the tags whose field name is \fIFIELD\fP. Note that one tag key,
like \fIARTIST\fP, may appear more than once, in which case all of those are
deleted.
.TP
.B \-a, \-\-add \fIFIELD=VALUE\fP
Add a tag. It doesn't matter if a tag of the same type already exist (think
about the case where there are several artists).
.TP
.B \-s, \-\-set \fIFIELD=VALUE\fP
This option is provided for convenience. It deletes all the fields of the same
type that may already exist, then adds it with the wanted value. This is
strictly equivalent to \fB\-\-delete\fP \fIFIELD\fP \fB\-\-add\fP
\fIFIELD=VALUE\fP.
You can combine it with \fB\-\-add\fP to add more tags with that same \fIFIELD\fP.
.TP
.B \-D, \-\-delete\-all
Delete all the tags before adding any.
.TP
.B \-\-import
Set the tags from scratch. All the original tags are deleted and new ones are
read from standard input.
.sp
Each line must specify a \fIFIELD=VALUE\fP pair and be LF\-terminated (except
for the last line). Invalid lines are skipped and issue a warning. Blank lines
are ignored. Lines whose first non-blank character is \fB#\fP are ignored.
Blank characters at the beginning of a line are also skipped.
.sp
Input is read as UTF\-8, disregarding the current locale of your system.
.TP
.B \-e, \-\-edit
Spawn the program specified in the environment variable \fBEDITOR\fP to edit
tags interactively. If this variable can't be read or is empty, an error
message is displayed.
.sp
The expected format is the same as the one \fB\-\-import\fP expects.
.SH EXAMPLES
Here's how you would list all tags in a stream:
.PP
opustags in.ogg
.PP
Here's how you would edit two streams at once, setting the title and artist of
the first, and only the title of the second:
.PP
opustags \-\-stream 1 \-\-set TITLE=X \-\-set ARTIST=Y \-\-stream 2 \-\-set TITLE=Y in.ogg \-o out.ogg
.PP
Here's how you would set two artists:
.PP
opustags \-\-delete ARTIST \-\-add ARTIST=A \-\-add ARTIST=B in.ogg \-o out.ogg
.PP
Hoping that helped!
.SH SEE ALSO
.BR vorbiscomment (1),
.BR sed (1)
.SH AUTHORS
Frédéric Mangano <fmang+opustags@mg0.fr>,
rr\- <https://github.com/rr\->.
.PP
Please report issues on GitHub at <https://github.com/fmang/opustags/issues>.

View File

@ -1,23 +0,0 @@
# Base Io build system
# Written by Jeremy Tregunna <jeremy.tregunna@me.com>
#
# Find libogg.
FIND_PATH(OGG_INCLUDE_DIR ogg/ogg.h)
SET(OGG_NAMES ${OGG_NAMES} ogg libogg)
FIND_LIBRARY(OGG_LIBRARY NAMES ${OGG_NAMES} PATH)
IF(OGG_INCLUDE_DIR AND OGG_LIBRARY)
SET(OGG_FOUND TRUE)
ENDIF(OGG_INCLUDE_DIR AND OGG_LIBRARY)
IF(OGG_FOUND)
IF(NOT Ogg_FIND_QUIETLY)
MESSAGE(STATUS "Found Ogg: ${OGG_LIBRARY}")
ENDIF (NOT Ogg_FIND_QUIETLY)
ELSE(OGG_FOUND)
IF(Ogg_FIND_REQUIRED)
MESSAGE(FATAL_ERROR "Could not find ogg")
ENDIF(Ogg_FIND_REQUIRED)
ENDIF (OGG_FOUND)

116
opustags.1 Normal file
View File

@ -0,0 +1,116 @@
.TH opustags 1 "November 2018" "@PROJECT_NAME@ @PROJECT_VERSION@"
.SH NAME
opustags \- Opus comment editor
.SH SYNOPSIS
.B opustags --help
.br
.B opustags
.RI [ OPTIONS ]
.I INPUT
.br
.B opustags
.I OPTIONS
.B -o
.I OUTPUT INPUT
.SH DESCRIPTION
.PP
\fBopustags\fP can read and edit the comment header of an Opus file.
It basically has two modes: read-only, and read-write for tag edition.
.PP
In read-only mode, only the beginning of \fIINPUT\fP is read, and the tags are
printed on standard output.
\fIINPUT\fP can either be the name of a file or \fB-\fP to read from standard input.
You can use the options below to edit the tags before printing them.
This could be useful to preview some changes before writing them.
.PP
In edition mode, you need to specify an output file (or \fB-\fP for standard output). It must be
different from the input file. To overwrite the input file, use \fB--in-place\fP.
.PP
Tag edition can be performed with the \fB--add\fP, \fB--delete\fP and \fB--set\fP
options. Options can be specified in any order and dont conflict with each other.
First the specified tags are deleted, then the new tags are added.
.PP
You can delete all the tags with \fB--delete-all\fP. This operation can be combined with \fB--add\fP
to set new tags without being bothered by the old ones.
.PP
If you want to replace all the tags, you can use the \fB--set-all\fP option which will cause
\fBopustags\fP to read tags from standard input.
The format is the same as the one used for output: newline-separated \fIFIELD=Value\fP assignment.
All the previously existing tags as deleted.
.PP
\fBWarning:\fP the Opus format specifications requires tags to be encoded in
\fBUTF-8\fP. This tool ignores the system locale, assuming the encoding is
set to UTF-8, and assume that tags are already encoded in UTF-8.
.SH OPTIONS
.TP
.B \-h, \-\-help
Display a brief description of the options.
.TP
.B \-o, \-\-output \fIFILE\fI
Specify the output file.
The input file will be read, its tags edited, then written to the specified output file. If
\fIFILE\fP is \fB-\fP then the resulting Opus file will be written to standard output.
The output file cant be the same as the input file.
.TP
.B \-i, \-\-in-place\fR[=\fP\fISUFFIX\fP\fR]\fP
Use this when you want to modify the input file in-place. opustags will create a temporary output
file with the specified suffix (.otmp by default), and move it to the location of the input file on
success. If a file with the same name as the temporary file already exists, it will be overwritten
without warning.
This option conflicts with \fB--output\fP.
.TP
.B \-y, \-\-overwrite
By default, \fBopustags\fP refuses to overwrite an already existent file. Use
this option to allow that.
.TP
.B \-d, \-\-delete \fIFIELD\fP
Delete all the tags whose field name is \fIFIELD\fP. They may be several one of them, though usually
there is only one of each type.
.TP
.B \-a, \-\-add \fIFIELD=VALUE\fP
Add a tag. Note that multiple tags with the same field name are perfectly acceptable, so you can add
multiple fields with the same name, and previously existing tags will also be preserved.
When the \fB--delete\fP is used with the same \fIFIELD\fP, only the older tags are deleted.
.TP
.B \-s, \-\-set \fIFIELD=VALUE\fP
This option is provided for convenience. It delete all the fields of the same
type that may already exist, then adds it with the wanted value.
This is strictly equivalent to \fB--delete\fP \fIFIELD\fP \fB--add\fP
\fIFIELD=VALUE\fP. You can combine it with \fB--add\fP to add tags of the same
type. As deletion occurs before adding, \fB--set\fP wont erase the tags
added with \fB--add\fP.
.TP
.B \-D, \-\-delete-all
Delete all the previously existing tags.
.TP
.B \-S, \-\-set-all
Sets the tags from scratch.
All the original tags are deleted and new ones are read from standard input.
Each line must specify a \fIFIELD=VALUE\fP pair and be separated with line feeds.
Invalid lines are skipped and cause a warning to be issued. Blank lines are ignored.
This mode could be useful for batch processing tags through an utility like \fBsed\fP.
.SH EXAMPLES
.PP
List all the tags in file foo.opus:
.PP
opustags foo.opus
.PP
Copy in.opus to out.opus, with the TITLE tag added:
.PP
opustags in.opus --output out.opus --add "TITLE=Hello world!"
.PP
Replace all the tags in dest.opus with the ones from src.opus:
.PP
opustags src.opus | opustags --in-place dest.opus --set-all
.PP
Remove the previously existing ARTIST tags and add the two X and Y ARTIST tags, then display the new
tags without writing them to the Opus file:
.PP
opustags in.opus --add ARTIST=X --add ARTIST=Y --delete ARTIST
.SH SEE ALSO
.BR vorbiscomment (1),
.BR sed (1)
.SH AUTHOR
Frédéric Mangano-Tarumi <fmang@mg0.fr>
.PP
Report bugs at <https://github.com/fmang/opustags/issues>

View File

@ -1,83 +0,0 @@
#include "actions.h"
using namespace opustags;
void opustags::list_tags(ogg::Decoder &dec, ITagsHandler &handler, bool full)
{
std::map<long, int> sequence_numbers;
int stream_count = 0;
int remaining_streams = 0;
std::shared_ptr<ogg::Stream> s;
while (!handler.done()) {
s = dec.read_page();
if (s == nullptr)
break; // end of stream
switch (s->state) {
case ogg::HEADER_READY:
stream_count++;
sequence_numbers[s->stream.serialno] = stream_count;
handler.start_of_stream(stream_count, s->type);
if (!handler.relevant(stream_count))
s->downgrade();
remaining_streams++;
break;
case ogg::TAGS_READY:
handler.list(sequence_numbers[s->stream.serialno], s->tags);
s->downgrade(); // no more use for it
default:
remaining_streams--;
}
if (!full && remaining_streams <= 0) {
break;
// premature exit, but calls end_of_stream anyway
// we want our optimization to be transparent to the TagsHandler
}
}
handler.end_of_file();
}
void opustags::edit_tags(
ogg::Decoder &in, ogg::Encoder &out, ITagsHandler &handler)
{
std::map<long, int> sequence_numbers;
int stream_count = 0;
std::shared_ptr<ogg::Stream> s;
for (;;) {
s = in.read_page();
if (s == nullptr)
break; // end of stream
switch (s->state) {
case ogg::HEADER_READY:
stream_count++;
sequence_numbers[s->stream.serialno] = stream_count;
handler.start_of_stream(stream_count, s->type);
if (!handler.relevant(stream_count))
s->downgrade(); // makes it UNKNOWN
if (s->type == ogg::UNKNOWN_STREAM) {
out.write_raw_page(in.current_page);
} else {
out.forward(*s);
}
break;
case ogg::TAGS_READY:
handler.edit(sequence_numbers[s->stream.serialno], s->tags);
handler.list(sequence_numbers[s->stream.serialno], s->tags);
out.write_tags(s->stream.serialno, s->tags);
break;
case ogg::DATA_READY:
out.forward(*s);
break;
case ogg::RAW_READY:
out.write_raw_page(in.current_page);
break;
default:
;
}
}
handler.end_of_file();
}

View File

@ -1,33 +0,0 @@
#pragma once
#include "ogg.h"
#include "tags_handler.h"
namespace opustags {
// Decode a file and call the handler's list method every time a tags
// header is read. Set full to true if you want to make sure every single
// page of the stream is read.
//
// Use:
// std::ifstream in("in.ogg");
// ogg::Decoder dec(in);
// TagsLister lister(options);
// list_tags(dec, lister);
//
void list_tags(ogg::Decoder&, ITagsHandler &, bool full = false);
// Forward the input data to the output stream, transforming tags on-the-go
// with the handler's edit method.
//
// Use:
// std::ifstream in("in.ogg");
// ogg::Decoder dec(in);
// std::ofstream out("out.ogg");
// std::Encoder enc(out);
// TagsEditor editor(options);
// edit_tags(dec, enc, editor);
//
void edit_tags(ogg::Decoder &in, ogg::Encoder &out, ITagsHandler &);
}

331
src/cli.cc Normal file
View File

@ -0,0 +1,331 @@
/**
* \file src/cli.cc
* \ingroup cli
*
* Provide all the features of the opustags executable from a C++ API. The main point of separating
* this module from the main one is to allow easy testing.
*
* \todo Use a safer temporary file name for in-place editing, like tmpnam.
* \todo Abort editing with --set-all if one comment is invalid?
*/
#include <config.h>
#include <opustags.h>
#include <getopt.h>
#include <limits.h>
#include <string.h>
#include <unistd.h>
static const char* version = PROJECT_NAME " version " PROJECT_VERSION "\n";
static const char* usage = 1 + R"raw(
Usage: opustags --help
opustags [OPTIONS] FILE
opustags OPTIONS FILE -o FILE
)raw";
static const char* help = 1 + R"raw(
Options:
-h, --help print this help
-o, --output FILE set the output file
-i, --in-place overwrite the input file instead of writing a different output file
-y, --overwrite overwrite the output file if it already exists
-a, --add FIELD=VALUE add a comment
-d, --delete FIELD delete all previously existing comments of a specific type
-D, --delete-all delete all the previously existing comments
-s, --set FIELD=VALUE replace a comment (shorthand for --delete FIELD --add FIELD=VALUE)
-S, --set-all replace all the comments with the ones read from standard input
)raw";
static struct option getopt_options[] = {
{"help", no_argument, 0, 'h'},
{"output", required_argument, 0, 'o'},
{"in-place", optional_argument, 0, 'i'},
{"overwrite", no_argument, 0, 'y'},
{"delete", required_argument, 0, 'd'},
{"add", required_argument, 0, 'a'},
{"set", required_argument, 0, 's'},
{"delete-all", no_argument, 0, 'D'},
{"set-all", no_argument, 0, 'S'},
{NULL, 0, 0, 0}
};
ot::status ot::process_options(int argc, char** argv, ot::options& opt)
{
if (argc == 1) {
fputs(version, stdout);
fputs(usage, stdout);
return st::exit_now;
}
int c;
while ((c = getopt_long(argc, argv, "ho:i::yd:a:s:DS", getopt_options, NULL)) != -1) {
switch (c) {
case 'h':
opt.print_help = true;
break;
case 'o':
opt.path_out = optarg;
if (opt.path_out.empty()) {
fputs("output's file path cannot be empty\n", stderr);
return st::bad_arguments;
}
break;
case 'i':
opt.inplace = optarg == nullptr ? ".otmp" : optarg;
if (strcmp(opt.inplace, "") == 0) {
fputs("the in-place suffix cannot be empty\n", stderr);
return st::bad_arguments;
}
break;
case 'y':
opt.overwrite = true;
break;
case 'd':
if (strchr(optarg, '=') != nullptr) {
fprintf(stderr, "invalid field name: '%s'\n", optarg);
return st::bad_arguments;
}
opt.to_delete.emplace_back(optarg);
break;
case 'a':
case 's':
if (strchr(optarg, '=') == NULL) {
fprintf(stderr, "invalid comment: '%s'\n", optarg);
return st::bad_arguments;
}
opt.to_add.emplace_back(optarg);
if (c == 's')
opt.to_delete.emplace_back(optarg);
break;
case 'S':
opt.set_all = true;
/* fall through */
case 'D':
opt.delete_all = true;
break;
default:
/* getopt printed a message */
return st::bad_arguments;
}
}
if (opt.print_help) {
puts(version);
puts(usage);
puts(help);
puts("See the man page for extensive documentation.");
return st::exit_now;
}
if (optind != argc - 1) {
fputs("exactly one input file must be specified\n", stderr);
return st::bad_arguments;
}
opt.path_in = argv[optind];
if (opt.path_in.empty()) {
fputs("input's file path cannot be empty\n", stderr);
return st::bad_arguments;
}
if (opt.inplace != nullptr) {
if (!opt.path_out.empty()) {
fputs("cannot combine --in-place and --output\n", stderr);
return st::bad_arguments;
}
opt.path_out = opt.path_in + opt.inplace;
}
if (opt.path_in == "-" && opt.set_all) {
fputs("can't open standard input for input when --set-all is specified\n", stderr);
return st::bad_arguments;
}
if (opt.path_in == "-" && opt.inplace) {
fputs("cannot modify standard input in-place\n", stderr);
return st::bad_arguments;
}
return st::ok;
}
/**
* \todo Escape new lines.
*/
void ot::print_comments(const std::list<std::string>& comments, FILE* output)
{
for (const std::string& comment : comments) {
fwrite(comment.data(), 1, comment.size(), output);
puts("");
}
}
std::list<std::string> ot::read_comments(FILE* input)
{
std::list<std::string> comments;
char* line = nullptr;
size_t buflen = 0;
ssize_t nread;
while ((nread = getline(&line, &buflen, input)) != -1) {
if (nread > 0 && line[nread - 1] == '\n')
--nread;
if (nread == 0)
continue;
if (memchr(line, '=', nread) == nullptr) {
fputs("warning: skipping malformed tag\n", stderr);
continue;
}
comments.emplace_back(line, nread);
}
free(line);
return comments;
}
/**
* Parse the packet as an OpusTags comment header, apply the user's modifications, and write the new
* packet to the writer.
*/
static ot::status process_tags(const ogg_packet& packet, const ot::options& opt, ot::ogg_writer* writer)
{
ot::opus_tags tags;
ot::status rc = ot::parse_tags(packet, tags);
if (rc != ot::st::ok)
return rc;
if (opt.delete_all) {
tags.comments.clear();
} else {
for (const std::string& name : opt.to_delete)
ot::delete_comments(tags, name.c_str());
}
if (opt.set_all)
tags.comments = ot::read_comments(stdin);
for (const std::string& comment : opt.to_add)
tags.comments.emplace_back(comment);
if (writer) {
auto packet = ot::render_tags(tags);
return writer->write_packet(packet);
} else {
ot::print_comments(tags.comments, stdout);
return ot::st::ok;
}
}
ot::status ot::process(ogg_reader& reader, ogg_writer* writer, const ot::options &opt)
{
int packet_count = 0;
for (;;) {
// Read the next page.
ot::status rc = reader.read_page();
if (rc == ot::st::end_of_stream)
break;
else if (rc != ot::st::ok)
return rc;
// Short-circuit when the relevant packets have been read.
if (packet_count >= 2 && writer) {
if ((rc = writer->write_page(reader.page)) != ot::st::ok)
return rc;
continue;
}
auto serialno = ogg_page_serialno(&reader.page);
if (writer && (rc = writer->prepare_stream(serialno)) != ot::st::ok)
return rc;
// Read all the packets.
for (;;) {
rc = reader.read_packet();
if (rc == ot::st::end_of_page)
break;
else if (rc != ot::st::ok)
return rc;
packet_count++;
if (packet_count == 1) { // Identification header
rc = ot::validate_identification_header(reader.packet);
if (rc != ot::st::ok)
return rc;
} else if (packet_count == 2) { // Comment header
rc = process_tags(reader.packet, opt, writer);
if (rc != ot::st::ok)
return rc;
if (!writer)
return ot::st::ok; /* nothing else to do */
else
continue; /* process_tags wrote the new packet */
}
if (writer && (rc = writer->write_packet(reader.packet)) != ot::st::ok)
return rc;
}
// Write the assembled page.
if (writer && (rc = writer->flush_page()) != ot::st::ok)
return rc;
}
if (packet_count < 2)
return {ot::st::fatal_error, "Expected at least 2 Ogg packets"};
return ot::st::ok;
}
/**
* Check if two filepaths point to the same file, after path canonicalization.
* The path "-" is treated specially, meaning stdin for path_in and stdout for path_out.
*/
static bool same_file(const std::string& path_in, const std::string& path_out)
{
if (path_in == "-" || path_out == "-")
return false;
char canon_in[PATH_MAX+1], canon_out[PATH_MAX+1];
if (realpath(path_in.c_str(), canon_in) && realpath(path_out.c_str(), canon_out)) {
return (strcmp(canon_in, canon_out) == 0);
}
return false;
}
ot::status ot::run(ot::options& opt)
{
if (!opt.path_out.empty() && same_file(opt.path_in, opt.path_out))
return {ot::st::fatal_error, "Input and output files are the same"};
ot::file input;
if (opt.path_in == "-") {
input = stdin;
} else {
input = fopen(opt.path_in.c_str(), "r");
if (input == nullptr)
return {ot::st::standard_error,
"Could not open '" + opt.path_in + "' for reading: " + strerror(errno)};
}
ot::file output;
if (opt.path_out == "-") {
output.reset(stdout);
} else if (!opt.path_out.empty()) {
if (!opt.overwrite && access(opt.path_out.c_str(), F_OK) == 0)
return {ot::st::fatal_error,
"'" + opt.path_out + "' already exists (use -y to overwrite)"};
output = fopen(opt.path_out.c_str(), "w");
if (output == nullptr)
return {ot::st::standard_error,
"Could not open '" + opt.path_out + "' for writing: " + strerror(errno)};
}
ot::status rc;
{
ot::ogg_reader reader(input.get());
std::unique_ptr<ot::ogg_writer> writer;
if (output != nullptr)
writer = std::make_unique<ot::ogg_writer>(output.get());
rc = process(reader, writer.get(), opt);
/* delete reader and writer before closing the files */
}
input.reset();
output.reset();
if (rc != ot::st::ok) {
if (!opt.path_out.empty() && opt.path_out != "-")
remove(opt.path_out.c_str());
return rc;
}
if (opt.inplace) {
if (rename(opt.path_out.c_str(), opt.path_in.c_str()) == -1)
return {ot::st::fatal_error,
"Could not move the result to '" + opt.path_in + "': " + strerror(errno)};
}
return ot::st::ok;
}

2
src/config.h.in Normal file
View File

@ -0,0 +1,2 @@
#cmakedefine PROJECT_NAME "@PROJECT_NAME@"
#cmakedefine PROJECT_VERSION "@PROJECT_VERSION@"

View File

@ -1,29 +0,0 @@
#pragma once
namespace opustags {
enum class LogLevel {
LOG_NORMAL = 0,
LOG_VERBOSE = 1,
LOG_DEBUG = 2,
LOG_DEBUG_EXTRA = 3,
};
class Log
{
public:
Log(std::ostream &out);
LogLevel level;
Log& operator<<(LogLevel lvl);
template<class T> Log& operator<<(const T&);
private:
std::ostream &out;
};
extern Log log;
}

View File

@ -1,88 +0,0 @@
#include "actions.h"
#include "options.h"
#include "version.h"
#include <iostream>
#include <fstream>
#include <cstring>
static void show_usage(const bool include_help)
{
static const auto usage =
"Usage: opustags --help\n"
" opustags [OPTIONS] INPUT\n"
" opustags [OPTIONS] -o OUTPUT INPUT\n";
static const auto help =
"Options:\n"
" -h, --help print this help\n"
" -V, --version print version\n"
" -o, --output FILE write the modified tags to this file\n"
" -i, --in-place [SUFFIX] use a temporary file then replace the original file\n"
" -y, --overwrite overwrite the output file if it already exists\n"
" --stream ID select stream for the next operations\n"
" -l, --list display a pretty listing of all tags\n"
" --no-color disable colors in --list output\n"
" -d, --delete FIELD delete all the fields of a specified type\n"
" -a, --add FIELD=VALUE add a field\n"
" -s, --set FIELD=VALUE delete then add a field\n"
" -D, --delete-all delete all the fields!\n"
" --full enable full file scan\n"
" --export dump the tags to standard output for --import\n"
" --import set the tags from scratch basing on stanard input\n"
" -e, --edit spawn the $EDITOR and apply --import on the result\n";
std::cout << "opustags v" << opustags::version_short << "\n";
std::cout << usage;
if (include_help) {
std::cout << "\n";
std::cout << help;
}
}
static void show_version()
{
std::cout << "opustags v" << opustags::version_long << "\n";
}
int main(int argc, char **argv)
{
if (argc == 1) {
show_usage(false);
return EXIT_SUCCESS;
}
try {
auto options = opustags::parse_args(argc, argv);
if (options.show_help) {
show_usage(true);
return EXIT_SUCCESS;
}
if (options.show_version) {
show_version();
return EXIT_SUCCESS;
}
if (options.path_out.empty()) {
std::ifstream in(options.path_in);
opustags::ogg::Decoder dec(in);
list_tags(dec, options.tags_handler, options.full);
// TODO: report errors if user tries to edit the stream
} else {
std::ifstream in(options.path_in);
std::ofstream out(options.path_out);
opustags::ogg::Decoder dec(in);
opustags::ogg::Encoder enc(out);
edit_tags(dec, enc, options.tags_handler);
}
} catch (const std::exception &e) {
if (errno)
std::cerr << "fatal error: " << e.what() << " (" << strerror(errno) << ")" << std::endl;
else
std::cerr << "fatal error: " << e.what() << std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}

View File

@ -1,308 +1,124 @@
#include "ogg.h"
/**
* \file src/ogg.c
* \ingroup ogg
*
* High-level interface for libogg.
*
* This module is not meant to be a complete libogg wrapper, but rather a convenient and highly
* specialized layer above libogg and stdio.
*/
#include <stdexcept>
#include <fstream>
#include <sstream>
#include <cstring>
#include <endian.h>
#include <opustags.h>
using namespace opustags;
#include <string.h>
////////////////////////////////////////////////////////////////////////////////
// ogg::Stream
using namespace std::literals::string_literals;
ogg::Stream::Stream(int streamno)
ot::ogg_reader::ogg_reader(FILE* input)
: file(input)
{
state = ogg::BEGIN_OF_STREAM;
type = ogg::UNKNOWN_STREAM;
if (ogg_stream_init(&stream, streamno) != 0)
throw std::runtime_error("ogg_stream_init failed");
ogg_sync_init(&sync);
}
ogg::Stream::~Stream()
ot::ogg_reader::~ogg_reader()
{
ogg_stream_clear(&stream);
if (stream_ready)
ogg_stream_clear(&stream);
ogg_sync_clear(&sync);
}
void ogg::Stream::flush_packets()
ot::status ot::ogg_reader::read_page()
{
ogg_packet op;
while (ogg_stream_packetout(&stream, &op) > 0);
while (ogg_sync_pageout(&sync, &page) != 1) {
if (feof(file))
return {st::end_of_stream, "End of stream was reached"};
char* buf = ogg_sync_buffer(&sync, 65536);
if (buf == nullptr)
return {st::libogg_error, "ogg_sync_buffer failed"};
size_t len = fread(buf, 1, 65536, file);
if (ferror(file))
return {st::standard_error, "fread error: "s + strerror(errno)};
if (ogg_sync_wrote(&sync, len) != 0)
return {st::libogg_error, "ogg_sync_wrote failed"};
if (ogg_sync_check(&sync) != 0)
return {st::libogg_error, "ogg_sync_check failed"};
}
/* at this point, we've got a good page */
if (!stream_ready) {
if (ogg_stream_init(&stream, ogg_page_serialno(&page)) != 0)
return {st::libogg_error, "ogg_stream_init failed"};
stream_ready = true;
}
stream_in_sync = false;
return st::ok;
}
bool ogg::Stream::page_in(ogg_page &og)
ot::status ot::ogg_reader::read_packet()
{
if (state != ogg::BEGIN_OF_STREAM && type == ogg::UNKNOWN_STREAM) {
state = ogg::RAW_READY;
return true;
}
flush_packets(); // otherwise packet_out keeps returning the same packet
if (ogg_stream_pagein(&stream, &og) != 0)
throw std::runtime_error("ogg_stream_pagein failed");
if (state == ogg::BEGIN_OF_STREAM || state == ogg::HEADER_READY) {
// We're expecting a header, so we parse it.
return handle_page();
} else {
// We're past the first two headers.
state = ogg::DATA_READY;
return true;
}
if (!stream_ready)
return {st::stream_not_ready, "Stream was not initialized"};
if (!stream_in_sync) {
if (ogg_stream_pagein(&stream, &page) != 0)
return {st::libogg_error, "ogg_stream_pagein failed"};
stream_in_sync = true;
}
int rc = ogg_stream_packetout(&stream, &packet);
if (rc == 1)
return st::ok;
else if (rc == 0 && ogg_stream_check(&stream) == 0)
return {st::end_of_page, "End of page was reached"};
else
return {st::libogg_error, "ogg_stream_packetout failed"};
}
// Read the first packet of the page and parses it.
bool ogg::Stream::handle_page()
ot::ogg_writer::ogg_writer(FILE* output)
: file(output)
{
ogg_packet op;
int rc = ogg_stream_packetpeek(&stream, &op);
if (rc < 0)
throw std::runtime_error("ogg_stream_packetout failed");
else if (rc == 0) // insufficient data
return false; // asking for a new page
// We've read the first packet successfully.
// The headers are supposed to contain only one packet, so this is enough
// for us. Still, we could ensure there are no other packets.
handle_packet(op);
return true;
if (ogg_stream_init(&stream, 0) != 0)
throw std::bad_alloc();
}
void ogg::Stream::handle_packet(const ogg_packet &op)
ot::ogg_writer::~ogg_writer()
{
if (state == ogg::BEGIN_OF_STREAM)
parse_header(op);
else if (state == ogg::HEADER_READY)
parse_opustags(op);
// else shrug
ogg_stream_clear(&stream);
}
void ogg::Stream::parse_header(const ogg_packet &op)
ot::status ot::ogg_writer::write_page(const ogg_page& page)
{
if (op.bytes >= 8 && memcmp(op.packet, "OpusHead", 8) == 0)
type = OPUS_STREAM;
else
type = UNKNOWN_STREAM;
state = HEADER_READY;
if (page.header_len < 0 || page.body_len < 0)
return {st::int_overflow, "Overflowing page length"};
auto header_len = static_cast<size_t>(page.header_len);
auto body_len = static_cast<size_t>(page.body_len);
if (fwrite(page.header, 1, header_len, file) < header_len)
return {st::standard_error, "fwrite error: "s + strerror(errno)};
if (fwrite(page.body, 1, body_len, file) < body_len)
return {st::standard_error, "fwrite error: "s + strerror(errno)};
return st::ok;
}
// For reference:
// https://tools.ietf.org/html/draft-ietf-codec-oggopus-14#section-5.2
void ogg::Stream::parse_opustags(const ogg_packet &op)
ot::status ot::ogg_writer::prepare_stream(long serialno)
{
// This part is gonna be C-ish because I don't see how I'd do this in C++
// without being inefficient, both in volume of code and performance.
char *data = reinterpret_cast<char*>(op.packet);
long remaining = op.bytes;
if (remaining < 8 || memcmp(data, "OpusTags", 8) != 0)
throw std::runtime_error("expected OpusTags header");
data += 8;
remaining -= 8;
// Vendor string
if (remaining < 4)
throw std::runtime_error("no space for vendor string length");
uint32_t vendor_length = le32toh(*reinterpret_cast<uint32_t*>(data));
if (remaining - 4 < vendor_length)
throw std::runtime_error("invalid vendor string length");
tags.vendor = std::string(data + 4, vendor_length);
data += 4 + vendor_length;
remaining -= 4 + vendor_length;
// User comments count
if (remaining < 4)
throw std::runtime_error("no space for user comment list length");
long comment_count = le32toh(*reinterpret_cast<uint32_t*>(data));
data += 4;
remaining -= 4;
// Actual comments
// We iterate on a long type to prevent infinite looping when comment_count == UINT32_MAX.
for (long i = 0; i < comment_count; i++) {
if (remaining < 4)
throw std::runtime_error("no space for user comment length");
uint32_t comment_length = le32toh(*reinterpret_cast<uint32_t*>(data));
if (remaining - 4 < comment_length)
throw std::runtime_error("no space for comment contents");
tags.add(parse_tag(std::string(data + 4, comment_length)));
data += 4 + comment_length;
remaining -= 4 + comment_length;
}
// Extra data to keep if the least significant bit of the first byte is 1
if (remaining > 0 && (*data & 1) == 1 )
tags.extra = std::string(data, remaining);
state = TAGS_READY;
if (stream.serialno != serialno) {
if (ogg_stream_reset_serialno(&stream, serialno) != 0)
return {st::libogg_error, "ogg_stream_reset_serialno failed"};
}
return st::ok;
}
void ogg::Stream::downgrade()
ot::status ot::ogg_writer::write_packet(const ogg_packet& packet)
{
type = ogg::UNKNOWN_STREAM;
if (state != ogg::BEGIN_OF_STREAM && state != ogg::END_OF_STREAM)
state = RAW_READY;
if (ogg_stream_packetin(&stream, const_cast<ogg_packet*>(&packet)) != 0)
return {st::libogg_error, "ogg_stream_packetin failed"};
else
return st::ok;
}
////////////////////////////////////////////////////////////////////////////////
// ogg::Decoder
ogg::Decoder::Decoder(std::istream &in)
: input(in)
ot::status ot::ogg_writer::flush_page()
{
if (!in)
throw std::runtime_error("invalid stream to decode");
input.exceptions(std::ifstream::badbit);
ogg_sync_init(&sync);
}
ogg::Decoder::~Decoder()
{
ogg_sync_clear(&sync);
}
std::shared_ptr<ogg::Stream> ogg::Decoder::read_page()
{
while (page_out()) {
int streamno = ogg_page_serialno(&current_page);
auto i = streams.find(streamno);
if (i == streams.end()) {
// we could check the page number to detect new streams (pageno = 0)
auto s = std::make_shared<Stream>(streamno);
i = streams.emplace(streamno, s).first;
}
if (i->second->page_in(current_page))
return i->second;
}
return nullptr; // end of stream
}
// Read the next page and return true on success, false on end of stream.
bool ogg::Decoder::page_out()
{
int rc;
for (;;) {
rc = ogg_sync_pageout(&sync, &current_page);
if (rc < 0) {
throw std::runtime_error("ogg_sync_pageout failed");
} else if (rc == 1) {
break; // page complete
} else if (!buff()) {
// more data required but end of file reached
// TODO check sync.unsynced flag in case we've got an incomplete page
return false;
}
}
return true;
}
// Read data from the stream into the sync's buffer.
bool ogg::Decoder::buff()
{
if (input.eof())
return false;
char *buf = ogg_sync_buffer(&sync, 65536);
if (buf == nullptr)
throw std::runtime_error("ogg_sync_buffer failed");
input.read(buf, 65536);
ogg_sync_wrote(&sync, input.gcount());
return true;
}
////////////////////////////////////////////////////////////////////////////////
// ogg::Encoder
ogg::Encoder::Encoder(std::ostream &out)
: output(out)
{
if (!output)
throw std::runtime_error("invalid stream to decode");
output.exceptions(std::ifstream::badbit);
}
ogg::Stream& ogg::Encoder::get_stream(int streamno)
{
auto i = streams.find(streamno);
if (i == streams.end()) {
auto s = std::make_shared<Stream>(streamno);
i = streams.emplace(streamno, s).first;
}
return *(i->second);
}
void ogg::Encoder::forward(ogg::Stream &in)
{
ogg::Stream *out = &get_stream(in.stream.serialno);
forward_stream(in, *out);
flush_stream(*out);
}
void ogg::Encoder::forward_stream(ogg::Stream &in, ogg::Stream &out)
{
int rc;
ogg_packet op;
for (;;) {
rc = ogg_stream_packetout(&in.stream, &op);
if (rc < 0) {
throw std::runtime_error("ogg_stream_packetout failed");
} else if (rc == 0) {
break;
} else {
if (ogg_stream_packetin(&out.stream, &op) != 0)
throw std::runtime_error("ogg_stream_packetin failed");
}
}
}
void ogg::Encoder::flush_stream(ogg::Stream &out)
{
ogg_page og;
if (ogg_stream_flush(&out.stream, &og))
write_raw_page(og);
}
void ogg::Encoder::write_raw_page(const ogg_page &og)
{
output.write(reinterpret_cast<const char*>(og.header), og.header_len);
output.write(reinterpret_cast<const char*>(og.body), og.body_len);
}
void ogg::Encoder::write_tags(int streamno, const Tags &tags)
{
ogg_packet op;
op.b_o_s = 0;
op.e_o_s = 0;
op.granulepos = 0;
op.packetno = 1; // checked on a file from ffmpeg
std::string data = render_opustags(tags);
op.bytes = data.size();
op.packet = reinterpret_cast<unsigned char*>(const_cast<char*>(data.data()));
std::shared_ptr<ogg::Stream> s = streams.at(streamno); // assume it exists
if (ogg_stream_packetin(&s->stream, &op) != 0)
throw std::runtime_error("ogg_stream_packetin failed");
flush_stream(*s);
}
std::string ogg::Encoder::render_opustags(const Tags &tags)
{
std::stringbuf s;
uint32_t length;
s.sputn("OpusTags", 8);
length = htole32(tags.vendor.size());
s.sputn(reinterpret_cast<char*>(&length), 4);
s.sputn(tags.vendor.data(), tags.vendor.size());
auto assocs = tags.get_all();
length = htole32(assocs.size());
s.sputn(reinterpret_cast<char*>(&length), 4);
for (const auto assoc : assocs) {
length = htole32(assoc.key.size() + 1 + assoc.value.size());
s.sputn(reinterpret_cast<char*>(&length), 4);
s.sputn(assoc.key.data(), assoc.key.size());
s.sputc('=');
s.sputn(assoc.value.data(), assoc.value.size());
}
s.sputn(tags.extra.data(), tags.extra.size());
return s.str();
ogg_page page;
if (ogg_stream_flush(&stream, &page) != 0)
return write_page(page);
if (ogg_stream_check(&stream) != 0)
return {st::libogg_error, "ogg_stream_check failed"};
return st::ok; /* nothing was done */
}

129
src/ogg.h
View File

@ -1,129 +0,0 @@
#pragma once
#include "tags.h"
#include <iostream>
#include <map>
#include <memory>
#include <ogg/ogg.h>
namespace opustags {
namespace ogg
{
enum StreamState {
BEGIN_OF_STREAM,
HEADER_READY,
TAGS_READY,
DATA_READY,
RAW_READY,
END_OF_STREAM,
// Meaning of these states below, in Stream.
};
enum StreamType {
UNKNOWN_STREAM = 0,
OPUS_STREAM,
};
// An Ogg file may contain many logical bitstreams, and among them, many
// Opus streams. This class represents one stream, whether it is Opus or
// not.
struct Stream
{
Stream(int streamno);
Stream(const Stream&) = delete;
~Stream();
// Called by Decoder once a page was read.
// Returns true if it's ready, false if it expects more data.
// In the latter case, Decoder::read_page will keep reading.
bool page_in(ogg_page&);
// Make the stream behave as if it were unknown.
// As a consequence, no more effort would be made in extracting data.
void downgrade();
StreamState state;
StreamType type;
Tags tags;
// Here are the meanings of the state variable:
// BEGIN_OF_STREAM: the stream was just initialized,
// HEADER_READY: the header (first packet) was found,
// TAGS_READY: the tags are parsed and complete,
// DATA_READY: we've read a data page whose meaning is no concern to us,
// RAW_READY: we don't even know what kind of stream that is, so don't alter it,
// END_OF_STREAM: no more thing to read, not even the current page.
// From the state, we decide what to do with the Decoder's current_page.
// The difference between DATA_READY and RAW_DATA is that the former
// might require a reencoding of the current page. For example, if the
// tags grow and span over two pages, all the following pages are gonna
// need a new sequence number.
// For an Opus stream, the sequence will be:
// BEGIN_OF_STREAM → HEADER_READY → TAGS_READY → DATA_READY* → END_OF_STREAM
// For an unknown stream:
// BEGIN_OF_STREAM → HEADER_READY → RAW_READY* → END_OF_STREAM
ogg_stream_state stream;
private:
void flush_packets();
bool handle_page();
void handle_packet(const ogg_packet&);
void parse_header(const ogg_packet&);
void parse_opustags(const ogg_packet&);
};
struct Decoder
{
Decoder(std::istream&);
Decoder(const Decoder&) = delete;
~Decoder();
// Read a page, dispatch it, and return the stream it belongs to.
// The read page is given to Stream::page_in before this function
// returns.
// After the end of the file is reached, it returns NULL.
std::shared_ptr<Stream> read_page();
std::istream &input;
ogg_sync_state sync;
ogg_page current_page;
std::map<int, std::shared_ptr<Stream>> streams;
private:
bool page_out();
bool buff();
};
struct Encoder
{
Encoder(std::ostream&);
// Copy the input stream's current page.
void forward(Stream &in);
// Write the page without even ensuring its page number is correct.
// It would be an efficient way to copy a stream identically, and also
// needed for write_page.
void write_raw_page(const ogg_page&);
void write_tags(int streamno, const Tags&);
std::ostream &output;
// We're gonna need some ogg_stream_state for adjusting the page
// numbers and splitting large packets as it's gotta be done.
std::map<int, std::shared_ptr<Stream>> streams;
private:
Stream& get_stream(int streamno);
void forward_stream(Stream &in, Stream &out);
void flush_stream(Stream &out);
std::string render_opustags(const Tags &tags);
};
}
}

View File

@ -1,199 +0,0 @@
#include "options.h"
#include "tags_handlers/insertion_tags_handler.h"
#include "tags_handlers/modification_tags_handler.h"
#include "tags_handlers/external_edit_tags_handler.h"
#include "tags_handlers/export_tags_handler.h"
#include "tags_handlers/import_tags_handler.h"
#include "tags_handlers/listing_tags_handler.h"
#include "tags_handlers/removal_tags_handler.h"
#include <getopt.h>
#include <regex>
#include <sstream>
using namespace opustags;
ArgumentError::ArgumentError(const std::string &message)
: std::runtime_error(message.c_str())
{
}
Options::Options() :
show_help(false),
show_version(false),
overwrite(false),
full(false),
in_place(false)
{
}
Options opustags::parse_args(const int argc, char **argv)
{
static const auto short_def = "hVeo:i::yd:a:s:Dl";
static const option long_def[] = {
{"help", no_argument, 0, 'h'},
{"version", no_argument, 0, 'V'},
{"full", no_argument, 0, 0},
{"output", required_argument, 0, 'o'},
{"in-place", optional_argument, 0, 'i'},
{"overwrite", no_argument, 0, 'y'},
{"delete", required_argument, 0, 'd'},
{"add", required_argument, 0, 'a'},
{"stream", required_argument, 0, 0},
{"set", required_argument, 0, 's'},
{"list", no_argument, 0, 'l'},
{"delete-all", no_argument, 0, 'D'},
{"edit", no_argument, 0, 'e'},
{"import", no_argument, 0, 0},
{"export", no_argument, 0, 0},
// TODO: parse no-colors
{nullptr, 0, 0, 0}
};
// TODO: use --list as default switch
Options options;
std::vector<int> current_streamnos {StreamTagsHandler::ALL_STREAMS};
int option_index;
char c;
optind = 0;
while ((c = getopt_long(
argc, argv, short_def, long_def, &option_index)) != -1) {
const std::string arg(optarg == nullptr ? "" : optarg);
switch (c) {
case 'h':
options.show_help = true;
break;
case 'V':
options.show_version = true;
break;
case 'o':
if (arg.empty())
throw ArgumentError("Output path cannot be empty");
options.path_out = arg;
break;
case 'i':
// TODO: shouldn't we generate random file name here to which
// we apply the arg, rather than use the arg as a whole?
options.path_out = arg.empty() ? ".otmp" : arg;
options.in_place = true;
break;
case 'y':
options.overwrite = true;
break;
case 'd':
if (arg.find('=') != std::string::npos)
throw ArgumentError("Invalid field: '" + arg + "'");
for (const auto streamno : current_streamnos) {
options.tags_handler.add_handler(
std::make_shared<RemovalTagsHandler>(streamno, arg));
}
break;
case 'a':
case 's':
{
std::smatch match;
std::regex regex("^(\\w+)=(.*)$");
if (!std::regex_match(arg, match, regex))
throw ArgumentError("Invalid field: '" + arg + "'");
for (const auto streamno : current_streamnos) {
if (c == 's') {
options.tags_handler.add_handler(
std::make_shared<ModificationTagsHandler>(
streamno, match[1], match[2]));
} else {
options.tags_handler.add_handler(
std::make_shared<InsertionTagsHandler>(
streamno, match[1], match[2]));
}
}
break;
}
case 'l':
for (const auto streamno : current_streamnos) {
options.tags_handler.add_handler(
std::make_shared<ListingTagsHandler>(
streamno, std::cout));
}
break;
case 'e':
options.tags_handler.add_handler(
std::make_shared<ExternalEditTagsHandler>());
break;
case 'D':
for (const auto streamno : current_streamnos) {
options.tags_handler.add_handler(
std::make_shared<RemovalTagsHandler>(streamno));
}
break;
case 0:
{
std::string long_arg = long_def[option_index].name;
if (long_arg == "stream") {
int i;
current_streamnos.clear();
std::stringstream ss(optarg);
while (ss >> i) {
current_streamnos.push_back(i);
if (ss.peek() == ',')
ss.ignore();
}
}
else if (long_arg == "full")
options.full = true;
else if (long_arg == "export")
options.tags_handler.add_handler(
std::make_shared<ExportTagsHandler>(std::cout));
else if (long_arg == "import")
options.tags_handler.add_handler(
std::make_shared<ImportTagsHandler>(std::cin));
break;
}
default:
throw ArgumentError("Invalid flag");
}
}
std::vector<std::string> stray;
while (optind < argc)
stray.push_back(argv[optind++]);
if (!options.show_help && !options.show_version) {
if (stray.empty())
throw ArgumentError("Missing input path");
options.path_in = stray.at(0);
if (options.path_in.empty())
throw ArgumentError("Input path cannot be empty");
if (stray.size() > 1)
throw ArgumentError("Extra argument: " + stray.at(1));
if (options.path_out.empty()) {
options.tags_handler.add_handler(
std::make_shared<ListingTagsHandler>(
StreamTagsHandler::ALL_STREAMS, std::cout));
}
}
return options;
}

View File

@ -1,33 +0,0 @@
#pragma once
#include "tags_handlers/composite_tags_handler.h"
#include <stdexcept>
#include <map>
#include <vector>
namespace opustags
{
struct Options final
{
Options();
bool show_help;
bool show_version;
bool overwrite;
bool full;
bool in_place;
std::string path_in;
std::string path_out;
CompositeTagsHandler tags_handler;
};
struct ArgumentError : std::runtime_error
{
ArgumentError(const std::string &message);
};
Options parse_args(const int argc, char **argv);
}

163
src/opus.cc Normal file
View File

@ -0,0 +1,163 @@
/**
* \file src/opus.cc
* \ingroup opus
*
* The way Opus is encapsulated into an Ogg stream, and the content of the packets we're dealing
* with here is defined by [RFC 7584](https://tools.ietf.org/html/rfc7845.html).
*
* Section 3 "Packet Organization" is critical for us:
*
* - The first page contains exactly 1 packet, the OpusHead, and it contains it entirely.
* - The second page begins the OpusTags packet, which may span several pages.
* - The OpusTags packet must finish the page on which it completes.
*
* The structure of the OpusTags packet is defined in section 5.2 "Comment Header" of the RFC.
*
* OpusTags is similar to [Vorbis Comment](https://www.xiph.org/vorbis/doc/v-comment.html), which
* gives us some context, but let's stick to the RFC for the technical details.
*
* \todo Validate that the vendor string and comments are valid UTF-8.
* \todo Validate that field names are ASCII: 0x20 through 0x7D, 0x3D ('=') excluded.
* \todo Field names are case insensitive, respect that.
*
*/
#include <opustags.h>
#include <string.h>
#ifdef __APPLE__
#include <libkern/OSByteOrder.h>
#define htole32(x) OSSwapHostToLittleInt32(x)
#define le32toh(x) OSSwapLittleToHostInt32(x)
#endif
/**
* \todo Validate more properties of the packet, like the sequence number.
*/
ot::status ot::validate_identification_header(const ogg_packet& packet)
{
if (packet.bytes < 8)
return {ot::st::cut_magic_number,
"Identification header too short for the magic number"};
if (memcmp(packet.packet, "OpusHead", 8) != 0)
return {ot::st::bad_magic_number,
"Identification header did not start with OpusHead"};
return ot::st::ok;
}
/**
* \todo See if the packet's data could be casted more nicely into a string.
*/
ot::status ot::parse_tags(const ogg_packet& packet, opus_tags& tags)
{
if (packet.bytes < 0)
return {st::int_overflow, "Overflowing comment header length"};
size_t size = static_cast<size_t>(packet.bytes);
const char* data = reinterpret_cast<char*>(packet.packet);
size_t pos = 0;
opus_tags my_tags;
// Magic number
if (8 > size)
return {st::cut_magic_number, "Comment header too short for the magic number"};
if (memcmp(data, "OpusTags", 8) != 0)
return {st::bad_magic_number, "Comment header did not start with OpusTags"};
// Vendor
pos = 8;
if (pos + 4 > size)
return {st::cut_vendor_length,
"Vendor string length did not fit the comment header"};
size_t vendor_length = le32toh(*((uint32_t*) (data + pos)));
if (pos + 4 + vendor_length > size)
return {st::cut_vendor_data, "Vendor string did not fit the comment header"};
my_tags.vendor = std::string(data + pos + 4, vendor_length);
pos += 4 + my_tags.vendor.size();
// Comment count
if (pos + 4 > size)
return {st::cut_comment_count, "Comment count did not fit the comment header"};
uint32_t count = le32toh(*((uint32_t*) (data + pos)));
pos += 4;
// Comments' data
for (uint32_t i = 0; i < count; ++i) {
if (pos + 4 > size)
return {st::cut_comment_length,
"Comment length did not fit the comment header"};
uint32_t comment_length = le32toh(*((uint32_t*) (data + pos)));
if (pos + 4 + comment_length > size)
return {st::cut_comment_data,
"Comment string did not fit the comment header"};
const char *comment_value = data + pos + 4;
my_tags.comments.emplace_back(comment_value, comment_length);
pos += 4 + comment_length;
}
// Extra data
my_tags.extra_data = std::string(data + pos, size - pos);
tags = std::move(my_tags);
return st::ok;
}
ot::dynamic_ogg_packet ot::render_tags(const opus_tags& tags)
{
size_t size = 8 + 4 + tags.vendor.size() + 4;
for (const std::string& comment : tags.comments)
size += 4 + comment.size();
size += tags.extra_data.size();
dynamic_ogg_packet op(size);
op.b_o_s = 0;
op.e_o_s = 0;
op.granulepos = 0;
op.packetno = 1;
unsigned char* data = op.packet;
uint32_t n;
memcpy(data, "OpusTags", 8);
n = htole32(tags.vendor.size());
memcpy(data+8, &n, 4);
memcpy(data+12, tags.vendor.data(), tags.vendor.size());
data += 12 + tags.vendor.size();
n = htole32(tags.comments.size());
memcpy(data, &n, 4);
data += 4;
for (const std::string& comment : tags.comments) {
n = htole32(comment.size());
memcpy(data, &n, 4);
memcpy(data+4, comment.data(), comment.size());
data += 4 + comment.size();
}
memcpy(data, tags.extra_data.data(), tags.extra_data.size());
return op;
}
/**
* \todo Make the field name case-insensitive?
*/
static int match_field(const char *comment, uint32_t len, const char *field)
{
size_t field_len;
for (field_len = 0; field[field_len] != '\0' && field[field_len] != '='; field_len++);
if (len <= field_len)
return 0;
if (comment[field_len] != '=')
return 0;
if (strncmp(comment, field, field_len) != 0)
return 0;
return 1;
}
void ot::delete_comments(opus_tags& tags, const char* field_name)
{
auto it = tags.comments.begin(), end = tags.comments.end();
while (it != end) {
auto current = it++;
if (match_field(current->data(), current->size(), field_name))
tags.comments.erase(current);
}
}

35
src/opustags.cc Normal file
View File

@ -0,0 +1,35 @@
/**
* \file src/opustags.cc
* \brief Main function for opustags.
*
* See opustags.h for the program's documentation.
*/
#include <opustags.h>
/**
* Main entry point to the opustags binary.
*
* Does practically nothing but call the cli module.
*/
int main(int argc, char** argv) {
ot::status rc;
ot::options opt;
rc = process_options(argc, argv, opt);
if (rc == ot::st::exit_now) {
return EXIT_SUCCESS;
} else if (rc != ot::st::ok) {
if (!rc.message.empty())
fprintf(stderr, "error: %s\n", rc.message.c_str());
return EXIT_FAILURE;
}
rc = run(opt);
if (rc != ot::st::ok && rc != ot::st::exit_now) {
if (!rc.message.empty())
fprintf(stderr, "error: %s\n", rc.message.c_str());
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}

475
src/opustags.h Normal file
View File

@ -0,0 +1,475 @@
/**
* \file src/opustags.h
*
* Welcome to opustags!
*
* Let's have a quick tour around. The project is split into the following modules:
*
* - The ogg module reads and writes Ogg files, letting you manipulate Ogg pages and packets.
* - The opus module parses the contents of Ogg packets according to the Opus specifications.
* - The cli module implements the main logic of the program.
* - The opustags module contains the main function, which is a simple wrapper around cli.
*
* Each module is implemented in its eponymous .cc file. Their interfaces are all defined and
* documented together in this header file. Look into the .cc files for implementation-specific
* details.
*
* To understand how this program works, you need to know what an Ogg files is made of, in
* particular the streams, pages, and packets. You hardly need any knowledge of the actual Opus
* audio codec, but need the RFC 7845 "Ogg Encapsulation for the Opus Audio Codec" that defines the
* format of the header packets that are essential to opustags.
*
*/
#pragma once
#include <ogg/ogg.h>
#include <stdio.h>
#include <list>
#include <memory>
#include <string>
#include <vector>
namespace ot {
/**
* Possible return status code, ranging from errors to special statuses. They are usually
* accompanied with a message with the #status structure.
*
* Functions that return non-ok status codes to signal special conditions like #end_of_stream should
* have it explictly mentionned in their documentation. By default, a non-ok status should be
* handled like an error.
*
* The cut error family means that the end of packet was reached when attempting to read the
* overflowing value. For example, cut_comment_count means that after reading the vendor string,
* less than 4 bytes were left in the packet.
*/
enum class st {
/* Generic */
ok,
int_overflow,
standard_error,
/* Ogg */
end_of_stream,
end_of_page,
stream_not_ready,
libogg_error,
/* Opus */
bad_magic_number,
cut_magic_number,
cut_vendor_length,
cut_vendor_data,
cut_comment_count,
cut_comment_length,
cut_comment_data,
/* CLI */
bad_arguments,
exit_now, /**< The program should terminate successfully. */
fatal_error,
};
/**
* Wraps a status code with an optional message. It is implictly converted to and from a
* #status_code.
*
* All the statuses except #st::ok should be accompanied with a relevant error message, in case it
* propagates back to the main function and is shown to the user.
*/
struct status {
status(st code = st::ok) : code(code) {}
template<class T> status(st code, T&& message) : code(code), message(message) {}
operator st() { return code; }
st code;
std::string message;
};
/**
* Smart auto-closing FILE* handle.
*
* It implictly converts from an already opened FILE*.
*/
struct file : std::unique_ptr<FILE, decltype(&fclose)> {
file(FILE* f = nullptr) : std::unique_ptr<FILE, decltype(&fclose)>(f, &fclose) {}
};
/***********************************************************************************************//**
* \defgroup ogg Ogg
* \{
*/
/**
* Ogg reader, combining a FILE input, an ogg_sync_state reading the pages, and an ogg_stream_state
* extracting the packets from the page.
*
* Call #read_page repeatedly until #status::end_of_stream to consume the stream, and use #page to
* check its content. To extract its packets, call #read_packet until #status::end_of_packet.
*/
class ogg_reader {
public:
/**
* Initialize the reader with the given input file handle. The caller is responsible for
* keeping the file handle alive, and to close it.
*/
ogg_reader(FILE* input);
/**
* Clear all the internal memory allocated by libogg for the sync and stream state. The
* page and the packet are owned by these states, so nothing to do with them.
*
* The input file is not closed.
*/
~ogg_reader();
/**
* Read the next page from the input file. The result, provided the status is #status::ok,
* is made available in the #page field, is owned by the Ogg reader, and is valid until the
* next call to #read_page.
*
* After the last page was read, return #status::end_of_stream.
*/
status read_page();
/**
* Read the next available packet from the current #page. The packet is made available in
* the #packet field.
*
* No packet can be read until a page has been loaded with #read_page. If that happens,
* return #status::stream_not_ready.
*
* After the last packet was read, return #status::end_of_page.
*/
status read_packet();
/**
* Current page from the sync state.
*
* Its memory is managed by libogg, inside the sync state, and is valid until the next call
* to ogg_sync_pageout, wrapped by #read_page.
*/
ogg_page page;
/**
* Current packet from the stream state.
*
* Its memory is managed by libogg, inside the stream state, and is valid until the next
* call to ogg_stream_packetout, wrapped by #read_packet.
*/
ogg_packet packet;
private:
/**
* The file is our source of binary data. It is not integrated to libogg, so we need to
* handle it ourselves.
*
* The file is not owned by the ogg_reader instance.
*/
FILE* file;
/**
* The sync layer gets binary data and yields a sequence of pages.
*
* A page contains packets that we can extract using the #stream state, but we only do that
* for the headers. Once we got the OpusHead and OpusTags packets, all the following pages
* are simply forwarded to the Ogg writer.
*/
ogg_sync_state sync;
/**
* Indicates whether the stream has been initialized or not.
*
* To initialize it properly, we need the serialno of the stream, which is available only
* after the first page was read.
*/
bool stream_ready = false;
/**
* Indicates if the stream's last fed page is the current one.
*
* Its state is irrelevant if the stream is not ready.
*/
bool stream_in_sync;
/**
* The stream layer receives pages and yields a sequence of packets.
*
* A single page may contain several packets, and a single packet may span on multiple
* pages. The 2 packets we're interested in occupy whole pages though, in theory, but we'd
* better ensure there are no extra packets anyway.
*
* After we've read OpusHead and OpusTags, we don't need the stream layer anymore.
*/
ogg_stream_state stream;
};
/**
* An Ogg writer lets you write ogg_page objets to an output file, and assemble packets into pages.
*
* It has two modes of operations :
* 1. call #write_page, or
* 2. call #prepare_stream, then #write_packet one or more times, followed by #flush_page.
*
* You can switch between the two modes, but must not start writing packets and then pages without
* flushing.
*/
class ogg_writer {
public:
/**
* Initialize the writer with the given output file handle. The caller is responsible for
* keeping the file handle alive, and to close it.
*/
ogg_writer(FILE* output);
/**
* Clears the stream state and any internal memory. Does not close the output file.
*/
~ogg_writer();
/**
* Write a whole Ogg page into the output stream.
*
* This is a basic I/O operation and does not even require libogg, or the stream.
*/
status write_page(const ogg_page& page);
/**
* Prepare the stream with the given Ogg serial number.
*
* If the stream is already configured with the right serial number, it doesn't do anything
* and is cheap to call.
*
* If the stream contains unflushed packets, they will be lost.
*/
status prepare_stream(long serialno);
/**
* Add a packet to the current page under assembly.
*
* If the packet is coming from a different page, make sure the serial number fits by
* calling #prepare_stream.
*
* When the page is complete, you should call #flush_page to finalize the page.
*
* You must not call #write_page after it, until you call #flush_page.
*/
status write_packet(const ogg_packet& packet);
/**
* Write the page under assembly. Future calls to #write_packet will be written in a new
* page.
*/
status flush_page();
private:
/**
* The stream state receives packets and generates pages.
*
* In our specific use case, we only need it to put the OpusHead and OpusTags packets into
* their own pages. The other pages are naively written to the output stream.
*/
ogg_stream_state stream;
/**
* Output file. It should be opened in binary mode. We use it to write whole pages,
* represented as a block of data and a length.
*/
FILE* file;
};
/**
* Ogg packet with dynamically allocated data.
*
* Provides a wrapper around libogg's ogg_packet with RAII.
*/
struct dynamic_ogg_packet : ogg_packet {
/** Construct an ogg_packet of the given size. */
explicit dynamic_ogg_packet(size_t size) {
bytes = size;
data = std::make_unique<unsigned char[]>(size);
packet = data.get();
}
private:
/** Owning reference to the data. Use the packet field from ogg_packet instead. */
std::unique_ptr<unsigned char[]> data;
};
/** \} */
/***********************************************************************************************//**
* \defgroup opus Opus
* \{
*/
/**
* Faithfully represent *all* the data in an OpusTags packet, exactly as they will be written in the
* final stream, disregarding the current system locale or anything else.
*
* The vendor and comment strings are expected to contain valid UTF-8, but we should keep their
* values intact even if the string is not UTF-8 clean, or encoded in any other way.
*/
struct opus_tags {
/**
* OpusTags packets begin with a vendor string, meant to identify the implementation of the
* encoder. It should be an arbitrary UTF-8 string.
*/
std::string vendor;
/**
* Comments. These are a list of string following the NAME=Value format. A comment may also
* be called a field, or a tag.
*
* The field name in vorbis comment is case-insensitive and ASCII, while the value can be
* any valid UTF-8 string. The specification is not too clear for Opus, but let's assume
* it's the same.
*/
std::list<std::string> comments;
/**
* According to RFC 7845:
* > Immediately following the user comment list, the comment header MAY contain
* > zero-padding or other binary data that is not specified here.
*
* The first byte is supposed to indicate whether this data should be kept or not, but let's
* assume it's here for a reason and always keep it. Better safe than sorry.
*
* In the future, we could add options to manipulate this data: view it, edit it, truncate
* it if it's marked as padding, truncate it unconditionally.
*/
std::string extra_data;
};
/**
* Validate the content of the first packet of an Ogg stream to ensure it's a valid OpusHead.
*
* Returns #ot::status::ok on success, #ot::status::bad_identification_header on error.
*/
status validate_identification_header(const ogg_packet& packet);
/**
* Read the given OpusTags packet and extract its content into an opus_tags object.
*
* On error, the tags object is left unchanged.
*/
status parse_tags(const ogg_packet& packet, opus_tags& tags);
/**
* Serialize an #opus_tags object into an OpusTags Ogg packet.
*/
dynamic_ogg_packet render_tags(const opus_tags& tags);
/**
* Remove all the comments whose field name is equal to the special one, case-sensitive.
*/
void delete_comments(opus_tags& tags, const char* field_name);
/** \} */
/***********************************************************************************************//**
* \defgroup cli Command-Line Interface
* \{
*/
/**
* Structured representation of the arguments to opustags.
*/
struct options {
/**
* Path to the input file. It cannot be empty. The special "-" string means stdin.
*
* This is the mandatory non-flagged parameter.
*/
std::string path_in;
/**
* Path to the optional file. The special "-" string means stdout. When empty, opustags runs
* in read-only mode.
*
* Option: --output
*/
std::string path_out;
/**
* If null, in-place editing is disabled. Otherwise, it points to the suffix to add to the
* file name.
*
* Option: --in-place
*/
const char* inplace = nullptr;
/**
* List of field names to delete. `{"ARTIST"}` will delete *all* the comments `ARTIST=*`. It
* is currently case-sensitive. When #delete_all is true, it becomes meaningless.
*
* #to_add takes precedence over #to_delete, so if the same comment appears in both lists,
* the one in #to_delete applies only to the previously existing tags.
*
* \todo Consider making it case-insensitive.
* \todo Allow values like `ARTIST=x` to delete only the ARTIST comment whose value is x.
*
* Option: --delete, --set
*/
std::vector<std::string> to_delete;
/**
* List of comments to add, in the current system encoding. For exemple `TITLE=a b c`. They
* must be valid.
*
* Options: --add, --set, --set-all
*/
std::vector<std::string> to_add;
/**
* Delete all the existing comments.
*
* Option: --delete-all
*/
bool delete_all = false;
/**
* Replace the previous comments by the ones supplied by the user.
*
* Read a list of comments from stdin and populate #to_add. Implies #delete_all. Further
* comments may be added with the --add option.
*
* Option: --set-all
*/
bool set_all = false;
/**
* By default, opustags won't overwrite the output file if it already exists.
*
* Option: --overwrite
*/
bool overwrite = false;
/**
* When true, opustags prints a detailed help and exits. All the other options are ignored.
*
* Option: --help
*/
bool print_help = false;
};
/**
* Process the command-line arguments.
*
* This function does not perform I/O related validations, but checks the consistency of its
* arguments.
*
* It returns one of :
* - #ot::st::ok, meaning the process may continue normally.
* - #ot::st::exit_now, meaning there is nothing to do and process should exit successfully.
* This happens when all the user wants is see the help or usage.
* - #ot::st::bad_arguments, meaning the arguments were invalid and the process should exit with
* an error.
*
* Help messages are written on standard output, and error messages on standard error.
*/
status process_options(int argc, char** argv, options& opt);
/**
* Print all the comments, separated by line breaks. Since a comment may
* contain line breaks, this output is not completely reliable, but it fits
* most cases.
*
* The output generated is meant to be parseable by #ot::read_tags.
*/
void print_comments(const std::list<std::string>& comments, FILE* output);
/**
* Parse the comments outputted by #ot::print_comments.
*/
std::list<std::string> read_comments(FILE* input);
/**
* Main loop of opustags. Read the packets from the reader, and forwards them to the writer.
* Transform the OpusTags packet on the fly.
*
* The writer is optional. When writer is nullptr, opustags runs in read-only mode.
*/
status process(ogg_reader& reader, ogg_writer* writer, const options &opt);
/**
* Open the input and output streams, then call #ot::process.
*
* This is the main entry point to the opustags program, and pretty much the same as calling
* opustags from the command-line.
*/
status run(options& opt);
/** \} */
}

View File

@ -1,103 +0,0 @@
#include "tags.h"
#include <algorithm>
using namespace opustags;
// ASCII only, but for tag keys it's good enough
static bool iequals(const std::string &a, const std::string &b)
{
if (a.size() != b.size())
return false;
for (size_t i = 0; i < a.size(); i++)
if (std::tolower(a[i]) != std::tolower(b[i]))
return false;
return true;
}
bool Tag::operator !=(const Tag &other_tag) const
{
return !operator ==(other_tag);
}
bool Tag::operator ==(const Tag &other_tag) const
{
return key == other_tag.key
&& value == other_tag.value;
}
Tags::Tags()
{
}
Tags::Tags(const std::vector<Tag> &tags) : tags(tags)
{
}
const std::vector<Tag> Tags::get_all() const
{
return tags;
}
std::string Tags::get(const std::string &key) const
{
for (auto &tag : tags)
if (iequals(tag.key, key))
return tag.value;
throw std::runtime_error("Tag '" + key + "' not found.");
}
void Tags::add(const Tag &tag)
{
tags.push_back(tag);
}
void Tags::add(const std::string &key, const std::string &value)
{
tags.push_back({key, value});
}
void Tags::clear()
{
tags.clear();
}
void Tags::remove(const std::string &key)
{
std::vector<Tag> new_tags;
std::copy_if(
tags.begin(),
tags.end(),
std::back_inserter(new_tags),
[&](const Tag &tag) { return !iequals(tag.key, key); });
tags = new_tags;
}
bool Tags::contains(const std::string &key) const
{
return std::count_if(
tags.begin(),
tags.end(),
[&](const Tag &tag) { return iequals(tag.key, key); }) > 0;
}
Tag opustags::parse_tag(const std::string &assoc)
{
size_t eq = assoc.find_first_of('=');
if (eq == std::string::npos)
throw std::runtime_error("misconstructed tag");
std::string name = assoc.substr(0, eq);
std::string value = assoc.substr(eq + 1);
return { name, value };
}
bool Tags::operator !=(const Tags &other_tags) const
{
return !operator ==(other_tags);
}
bool Tags::operator ==(const Tags &other_tags) const
{
return vendor == other_tags.vendor
&& extra == other_tags.extra
&& get_all() == other_tags.get_all();
}

View File

@ -1,49 +0,0 @@
#pragma once
#include <map>
#include <vector>
#include <utility>
namespace opustags {
struct Tag final
{
bool operator !=(const Tag &other_tag) const;
bool operator ==(const Tag &other_tag) const;
std::string key;
std::string value;
};
// A std::map adapter that keeps the order of insertion.
class Tags final
{
public:
Tags();
Tags(const std::vector<Tag> &tags);
const std::vector<Tag> get_all() const;
std::string get(const std::string &key) const;
void add(const Tag &tag);
void add(const std::string &key, const std::string &value);
void remove(const std::string &key);
bool contains(const std::string &key) const;
void clear();
bool operator !=(const Tags &other_tags) const;
bool operator ==(const Tags &other_tags) const;
// Additional fields are required to match the specs:
// https://tools.ietf.org/html/draft-ietf-codec-oggopus-14#section-5.2
std::string vendor;
std::string extra;
private:
std::vector<Tag> tags;
};
Tag parse_tag(const std::string &assoc); // KEY=value
}

View File

@ -1,50 +0,0 @@
#pragma once
#include "tags.h"
namespace opustags {
// TagsHandler define various operations related to tags and stream in
// order to control the main loop.
// In its implementation, it is expected to receive an option structure.
class ITagsHandler
{
public:
// Irrelevant streams don't even need to be parsed, so we can save some
// effort with this method.
// Returns true if the stream should be parsed, false if it should be
// ignored (list) or copied identically (edit).
virtual bool relevant(const int streamno) = 0;
// The list method is called by list_tags every time it has
// successfully parsed an OpusTags header.
virtual void list(const int streamno, const Tags &) = 0;
// Transform the tags at will.
// Returns true if the tags were indeed modified, false if they weren't.
// The latter case may be used for optimization.
virtual bool edit(const int streamno, Tags &) = 0;
// The work is done.
// When listing tags, once we've caught the streams we wanted, it's no
// use keeping reading the file for new streams. In that case, a true
// return value would abort any further processing.
virtual bool done() = 0;
// Signals a new stream was found.
// The meaning of type is in ogg::StreamType, but all you should assume
// is that when type is null (UNKNOWN_STREAM), list or edit won't be
// called.
virtual void start_of_stream(const int streamno, const int type) {}
// Signals the end of the file (and all the streams).
// If after this function is called, done() returns false, it's an
// error. However, it would be better to raise the error inside
// end_of_stream().
// For example, if you expect to find the stream #1 and reach the
// end-of-stream before finding it, better tell the user that you
// didn't do what he expected.
virtual void end_of_file() {}
};
}

View File

@ -1,44 +0,0 @@
#include "tags_handlers/composite_tags_handler.h"
using namespace opustags;
void CompositeTagsHandler::add_handler(std::shared_ptr<ITagsHandler> handler)
{
handlers.push_back(std::move(handler));
}
bool CompositeTagsHandler::relevant(const int streamno)
{
for (const auto &handler : handlers)
if (handler->relevant(streamno))
return true;
return false;
}
void CompositeTagsHandler::list(const int streamno, const Tags &tags)
{
for (const auto &handler : handlers)
handler->list(streamno, tags);
}
bool CompositeTagsHandler::edit(const int streamno, Tags &tags)
{
bool modified = false;
for (const auto &handler : handlers)
modified |= handler->edit(streamno, tags);
return modified;
}
bool CompositeTagsHandler::done()
{
for (const auto &handler : handlers)
if (!handler->done())
return false;
return true;
}
const std::vector<std::shared_ptr<ITagsHandler>>
CompositeTagsHandler::get_handlers() const
{
return handlers;
}

View File

@ -1,25 +0,0 @@
#pragma once
#include <memory>
#include <vector>
#include "tags_handler.h"
namespace opustags {
class CompositeTagsHandler final : public ITagsHandler
{
public:
void add_handler(std::shared_ptr<ITagsHandler> handler);
bool relevant(const int streamno) override;
void list(const int streamno, const Tags &) override;
bool edit(const int streamno, Tags &) override;
bool done() override;
const std::vector<std::shared_ptr<ITagsHandler>> get_handlers() const;
private:
std::vector<std::shared_ptr<ITagsHandler>> handlers;
};
}

View File

@ -1,31 +0,0 @@
#include "tags_handlers/export_tags_handler.h"
using namespace opustags;
ExportTagsHandler::ExportTagsHandler(std::ostream &output_stream)
: output_stream(output_stream)
{
}
bool ExportTagsHandler::relevant(const int)
{
return true;
}
void ExportTagsHandler::list(const int streamno, const Tags &tags)
{
output_stream << "[Stream " << streamno << "]\n";
for (const auto tag : tags.get_all())
output_stream << tag.key << "=" << tag.value << "\n";
output_stream << "\n";
}
bool ExportTagsHandler::edit(const int, Tags &)
{
return false;
}
bool ExportTagsHandler::done()
{
return false;
}

View File

@ -1,22 +0,0 @@
#pragma once
#include <iostream>
#include "tags_handler.h"
namespace opustags {
class ExportTagsHandler : public ITagsHandler
{
public:
ExportTagsHandler(std::ostream &output_stream);
bool relevant(const int streamno) override;
void list(const int streamno, const Tags &) override;
bool edit(const int streamno, Tags &) override;
bool done() override;
private:
std::ostream &output_stream;
};
}

View File

@ -1,22 +0,0 @@
#include "tags_handlers/external_edit_tags_handler.h"
using namespace opustags;
bool ExternalEditTagsHandler::relevant(const int streamno)
{
return false;
}
void ExternalEditTagsHandler::list(const int streamno, const Tags &)
{
}
bool ExternalEditTagsHandler::edit(const int streamno, Tags &)
{
return false;
}
bool ExternalEditTagsHandler::done()
{
return true;
}

View File

@ -1,17 +0,0 @@
#pragma once
#include <iostream>
#include "tags_handler.h"
namespace opustags {
class ExternalEditTagsHandler : public ITagsHandler
{
public:
bool relevant(const int streamno) override;
void list(const int streamno, const Tags &) override;
bool edit(const int streamno, Tags &) override;
bool done() override;
};
}

View File

@ -1,67 +0,0 @@
#include <regex>
#include "tags_handlers/import_tags_handler.h"
using namespace opustags;
ImportTagsHandler::ImportTagsHandler(std::istream &input_stream)
: parsed(false), input_stream(input_stream)
{
}
bool ImportTagsHandler::relevant(const int)
{
return true;
}
void ImportTagsHandler::list(const int, const Tags &)
{
}
bool ImportTagsHandler::edit(const int streamno, Tags &tags)
{
// the reason why we do it this way is because the tests indirectly create
// this handler with std::cin, and we do not want the constructor to block!
parse_input_stream_if_needed();
const auto old_tags = tags;
tags.clear();
if (tag_map.find(streamno) != tag_map.end()) {
const auto &source_tags = tag_map.at(streamno);
for (const auto &source_tag : source_tags.get_all())
tags.add(source_tag.key, source_tag.value);
}
return old_tags != tags;
}
bool ImportTagsHandler::done()
{
return false;
}
void ImportTagsHandler::parse_input_stream_if_needed()
{
if (parsed)
return;
parsed = true;
const std::regex whitespace_regex("^\\s*$");
const std::regex stream_header_regex(
"^\\s*\\[stream\\s+(\\d+)\\]\\s*$", std::regex_constants::icase);
const std::regex tag_regex(
"^\\s*([a-z0-9]+)\\s*=\\s*(.*?)\\s*$", std::regex_constants::icase);
int current_stream_number = 1;
std::string line;
while (std::getline(input_stream, line)) {
std::smatch match;
if (std::regex_match(line, match, stream_header_regex)) {
current_stream_number = std::atoi(match[1].str().c_str());
} else if (std::regex_match(line, match, tag_regex)) {
tag_map[current_stream_number].add(match[1], match[2]);
} else if (!std::regex_match(line, match, whitespace_regex)) {
throw std::runtime_error("Malformed input data near line " + line);
}
}
}

View File

@ -1,27 +0,0 @@
#pragma once
#include <iostream>
#include <map>
#include "tags_handler.h"
namespace opustags {
class ImportTagsHandler : public ITagsHandler
{
public:
ImportTagsHandler(std::istream &input_stream);
bool relevant(const int streamno) override;
void list(const int streamno, const Tags &) override;
bool edit(const int streamno, Tags &) override;
bool done() override;
private:
void parse_input_stream_if_needed();
bool parsed;
std::istream &input_stream;
std::map<int, Tags> tag_map;
};
}

View File

@ -1,28 +0,0 @@
#include "tags_handlers/insertion_tags_handler.h"
#include "tags_handlers_errors.h"
using namespace opustags;
InsertionTagsHandler::InsertionTagsHandler(
const int streamno,
const std::string &tag_key,
const std::string &tag_value)
: StreamTagsHandler(streamno), tag_key(tag_key), tag_value(tag_value)
{
}
std::string InsertionTagsHandler::get_tag_key() const
{
return tag_key;
}
std::string InsertionTagsHandler::get_tag_value() const
{
return tag_value;
}
bool InsertionTagsHandler::edit_impl(Tags &tags)
{
tags.add(tag_key, tag_value);
return true;
}

View File

@ -1,26 +0,0 @@
#pragma once
#include "tags_handlers/stream_tags_handler.h"
namespace opustags {
class InsertionTagsHandler : public StreamTagsHandler
{
public:
InsertionTagsHandler(
const int streamno,
const std::string &tag_key,
const std::string &tag_value);
std::string get_tag_key() const;
std::string get_tag_value() const;
protected:
bool edit_impl(Tags &) override;
private:
const std::string tag_key;
const std::string tag_value;
};
}

View File

@ -1,16 +0,0 @@
#include "tags_handlers/listing_tags_handler.h"
using namespace opustags;
ListingTagsHandler::ListingTagsHandler(
const int streamno,
std::ostream &output_stream)
: StreamTagsHandler(streamno), output_stream(output_stream)
{
}
void ListingTagsHandler::list_impl(const Tags &tags)
{
for (const auto &tag : tags.get_all())
output_stream << tag.key << "=" << tag.value << "\n";
}

View File

@ -1,20 +0,0 @@
#pragma once
#include <iostream>
#include "tags_handlers/stream_tags_handler.h"
namespace opustags {
class ListingTagsHandler : public StreamTagsHandler
{
public:
ListingTagsHandler(const int streamno, std::ostream &output_stream);
protected:
void list_impl(const Tags &) override;
private:
std::ostream &output_stream;
};
}

View File

@ -1,33 +0,0 @@
#include "tags_handlers/modification_tags_handler.h"
using namespace opustags;
ModificationTagsHandler::ModificationTagsHandler(
const int streamno,
const std::string &tag_key,
const std::string &tag_value)
: StreamTagsHandler(streamno), tag_key(tag_key), tag_value(tag_value)
{
}
std::string ModificationTagsHandler::get_tag_key() const
{
return tag_key;
}
std::string ModificationTagsHandler::get_tag_value() const
{
return tag_value;
}
bool ModificationTagsHandler::edit_impl(Tags &tags)
{
if (tags.contains(tag_key)) {
if (tags.get(tag_key) == tag_value)
return false;
tags.remove(tag_key);
}
tags.add(tag_key, tag_value);
return true;
}

View File

@ -1,26 +0,0 @@
#pragma once
#include "tags_handlers/stream_tags_handler.h"
namespace opustags {
class ModificationTagsHandler : public StreamTagsHandler
{
public:
ModificationTagsHandler(
const int streamno,
const std::string &tag_key,
const std::string &tag_value);
std::string get_tag_key() const;
std::string get_tag_value() const;
protected:
bool edit_impl(Tags &) override;
private:
const std::string tag_key;
const std::string tag_value;
};
}

View File

@ -1,35 +0,0 @@
#include "tags_handlers/removal_tags_handler.h"
#include "tags_handlers_errors.h"
using namespace opustags;
RemovalTagsHandler::RemovalTagsHandler(const int streamno)
: StreamTagsHandler(streamno)
{
}
RemovalTagsHandler::RemovalTagsHandler(
const int streamno, const std::string &tag_key)
: StreamTagsHandler(streamno), tag_key(tag_key)
{
}
std::string RemovalTagsHandler::get_tag_key() const
{
return tag_key;
}
bool RemovalTagsHandler::edit_impl(Tags &tags)
{
if (tag_key.empty()) {
const auto anything_removed = tags.get_all().size() > 0;
tags.clear();
return anything_removed;
} else {
if (!tags.contains(tag_key))
return false;
tags.remove(tag_key);
return true;
}
}

View File

@ -1,22 +0,0 @@
#pragma once
#include "tags_handlers/stream_tags_handler.h"
namespace opustags {
class RemovalTagsHandler : public StreamTagsHandler
{
public:
RemovalTagsHandler(const int streamno);
RemovalTagsHandler(const int streamno, const std::string &tag_key);
std::string get_tag_key() const;
protected:
bool edit_impl(Tags &) override;
private:
const std::string tag_key;
};
}

View File

@ -1,59 +0,0 @@
#include "tags_handlers/stream_tags_handler.h"
#include <iostream>
using namespace opustags;
const int StreamTagsHandler::ALL_STREAMS = -1;
StreamTagsHandler::StreamTagsHandler(const int streamno)
: streamno(streamno), work_finished(false)
{
}
int StreamTagsHandler::get_streamno() const
{
return streamno;
}
bool StreamTagsHandler::relevant(const int streamno)
{
return streamno == this->streamno || this->streamno == ALL_STREAMS;
}
void StreamTagsHandler::list(const int streamno, const Tags &tags)
{
if (!relevant(streamno))
return;
list_impl(tags);
work_finished = this->streamno != ALL_STREAMS;
}
bool StreamTagsHandler::edit(const int streamno, Tags &tags)
{
if (!relevant(streamno))
return false;
const auto ret = edit_impl(tags);
work_finished = this->streamno != ALL_STREAMS;
return ret;
}
bool StreamTagsHandler::done()
{
return work_finished;
}
void StreamTagsHandler::end_of_file()
{
if (!work_finished && streamno != ALL_STREAMS)
std::cerr << "warning: stream " << streamno << " wasn't found" << std::endl;
}
void StreamTagsHandler::list_impl(const Tags &)
{
}
bool StreamTagsHandler::edit_impl(Tags &)
{
return false;
}

View File

@ -1,33 +0,0 @@
#pragma once
#include "tags_handler.h"
namespace opustags {
// Base handler that holds the stream number it's supposed to work with
// and performs the usual boilerplate.
class StreamTagsHandler : public ITagsHandler
{
public:
static const int ALL_STREAMS;
StreamTagsHandler(const int streamno);
int get_streamno() const;
bool relevant(const int streamno) override;
void list(const int streamno, const Tags &) override;
bool edit(const int streamno, Tags &) override;
bool done() override;
void end_of_file() override;
protected:
virtual void list_impl(const Tags &);
virtual bool edit_impl(Tags &);
private:
const int streamno;
bool work_finished;
};
}

View File

@ -1,13 +0,0 @@
#include "tags_handlers_errors.h"
using namespace opustags;
TagAlreadyExistsError::TagAlreadyExistsError(const std::string &tag_key)
: std::runtime_error("Tag already exists: " + tag_key)
{
}
TagDoesNotExistError::TagDoesNotExistError(const std::string &tag_key)
: std::runtime_error("Tag does not exist: " + tag_key)
{
}

View File

@ -1,17 +0,0 @@
#pragma once
#include <stdexcept>
namespace opustags {
struct TagAlreadyExistsError : std::runtime_error
{
TagAlreadyExistsError(const std::string &tag_key);
};
struct TagDoesNotExistError : std::runtime_error
{
TagDoesNotExistError(const std::string &tag_key);
};
}

View File

@ -1,10 +0,0 @@
#pragma once
#include <string>
namespace opustags {
static const std::string version_short = "@VERSION_SHORT@";
static const std::string version_long = "@VERSION_LONG@";
}

16
t/CMakeLists.txt Normal file
View File

@ -0,0 +1,16 @@
add_executable(opus.t EXCLUDE_FROM_ALL opus.cc)
target_link_libraries(opus.t libopustags)
add_executable(ogg.t EXCLUDE_FROM_ALL ogg.cc)
target_link_libraries(ogg.t libopustags)
add_executable(cli.t EXCLUDE_FROM_ALL cli.cc)
target_link_libraries(cli.t libopustags)
configure_file(gobble.opus . COPYONLY)
add_custom_target(
check
COMMAND prove "${CMAKE_CURRENT_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}"
DEPENDS opustags gobble.opus opus.t ogg.t cli.t
)

26
t/cli.cc Normal file
View File

@ -0,0 +1,26 @@
#include <opustags.h>
#include "tap.h"
#include <string.h>
const char *user_comments = R"raw(
TITLE=a b c
ARTIST=X
Artist=Y)raw";
void check_read_comments()
{
ot::file input = fmemopen(const_cast<char*>(user_comments), strlen(user_comments), "r");
auto comments = ot::read_comments(input.get());
auto&& expected = {"TITLE=a b c", "ARTIST=X", "Artist=Y"};
if (!std::equal(comments.begin(), comments.end(), expected.begin(), expected.end()))
throw failure("parsed user comments did not match expectations");
}
int main(int argc, char **argv)
{
std::cout << "1..1\n";
run(check_read_comments, "check tags parsing");
return 0;
}

BIN
t/gobble.opus Normal file

Binary file not shown.

148
t/ogg.cc Normal file
View File

@ -0,0 +1,148 @@
#include <opustags.h>
#include "tap.h"
#include <string.h>
static void check_ref_ogg()
{
ot::file input = fopen("gobble.opus", "r");
if (input == nullptr)
throw failure("could not open gobble.opus");
ot::ogg_reader reader(input.get());
ot::status rc = reader.read_page();
if (rc != ot::st::ok)
throw failure("could not read the first page");
rc = reader.read_packet();
if (rc != ot::st::ok)
throw failure("could not read the first packet");
if (reader.packet.bytes != 19)
throw failure("unexpected length for the first packet");
rc = reader.read_packet();
if (rc != ot::st::end_of_page)
throw failure("got an unexpected second packet on the first page");
rc = reader.read_page();
if (rc != ot::st::ok)
throw failure("could not read the second page");
rc = reader.read_packet();
if (rc != ot::st::ok)
throw failure("could not read the second packet");
if (reader.packet.bytes != 62)
throw failure("unexpected length for the first packet");
rc = reader.read_packet();
if (rc != ot::st::end_of_page)
throw failure("got an unexpected second packet on the second page");
while (!ogg_page_eos(&reader.page)) {
rc = reader.read_page();
if (rc != ot::st::ok)
throw failure("failure reading a page");
}
rc = reader.read_page();
if (rc != ot::st::end_of_stream)
throw failure("did not correctly detect the end of stream");
}
static ogg_packet make_packet(const char* contents)
{
ogg_packet op {};
op.bytes = strlen(contents);
op.packet = (unsigned char*) contents;
return op;
}
static bool same_packet(const ogg_packet& lhs, const ogg_packet& rhs)
{
return lhs.bytes == rhs.bytes && memcmp(lhs.packet, rhs.packet, lhs.bytes) == 0;
}
/**
* Build an in-memory Ogg stream using ogg_writer, and then read it with ogg_reader.
*/
static void check_memory_ogg()
{
const ogg_packet first_packet = make_packet("First");
const ogg_packet second_packet = make_packet("Second");
const ogg_packet third_packet = make_packet("Third");
std::vector<unsigned char> my_ogg(128);
size_t my_ogg_size;
ot::status rc;
{
ot::file output = fmemopen(my_ogg.data(), my_ogg.size(), "w");
if (output == nullptr)
throw failure("could not open the output stream");
ot::ogg_writer writer(output.get());
rc = writer.prepare_stream(1234);
if (rc != ot::st::ok)
throw failure("could not prepare the stream for the first page");
writer.write_packet(first_packet);
if (rc != ot::st::ok)
throw failure("could not write the first packet");
writer.flush_page();
if (rc != ot::st::ok)
throw failure("could not flush the first page");
writer.prepare_stream(1234);
if (rc != ot::st::ok)
throw failure("could not prepare the stream for the second page");
writer.write_packet(second_packet);
if (rc != ot::st::ok)
throw failure("could not write the second packet");
writer.write_packet(third_packet);
if (rc != ot::st::ok)
throw failure("could not write the third packet");
writer.flush_page();
if (rc != ot::st::ok)
throw failure("could not flush the second page");
my_ogg_size = ftell(output.get());
if (my_ogg_size != 73)
throw failure("unexpected output size");
}
{
ot::file input = fmemopen(my_ogg.data(), my_ogg_size, "r");
if (input == nullptr)
throw failure("could not open the input stream");
ot::ogg_reader reader(input.get());
rc = reader.read_page();
if (rc != ot::st::ok)
throw failure("could not read the first page");
rc = reader.read_packet();
if (rc != ot::st::ok)
throw failure("could not read the first packet");
if (!same_packet(reader.packet, first_packet))
throw failure("unexpected content in the first packet");
rc = reader.read_packet();
if (rc != ot::st::end_of_page)
throw failure("unexpected second packet in the first page");
rc = reader.read_page();
if (rc != ot::st::ok)
throw failure("could not read the second page");
rc = reader.read_packet();
if (rc != ot::st::ok)
throw failure("could not read the second packet");
if (!same_packet(reader.packet, second_packet))
throw failure("unexpected content in the second packet");
rc = reader.read_packet();
if (rc != ot::st::ok)
throw failure("could not read the third packet");
if (!same_packet(reader.packet, third_packet))
throw failure("unexpected content in the third packet");
rc = reader.read_packet();
if (rc != ot::st::end_of_page)
throw failure("unexpected third packet in the second page");
rc = reader.read_page();
if (rc != ot::st::end_of_stream)
throw failure("unexpected third page");
}
}
int main(int argc, char **argv)
{
std::cout << "1..2\n";
run(check_ref_ogg, "check a reference ogg stream");
run(check_memory_ogg, "build and check a fresh stream");
return 0;
}

166
t/opus.cc Normal file
View File

@ -0,0 +1,166 @@
#include <opustags.h>
#include "tap.h"
#include <string.h>
using namespace std::literals::string_literals;
static void check_identification()
{
ogg_packet packet {};
packet.packet = (unsigned char*) "OpusHead..";
packet.bytes = 10;
if (ot::validate_identification_header(packet) != ot::st::ok)
throw failure("did not accept a good OpusHead");
packet.bytes = 7;
if (ot::validate_identification_header(packet) != ot::st::cut_magic_number)
throw failure("accepted an OpusHead that is too short");
packet.packet = (unsigned char*) "NotOpusHead";
packet.bytes = 11;
if (ot::validate_identification_header(packet) != ot::st::bad_magic_number)
throw failure("did not report the right status for a bad OpusHead");
}
static const char standard_OpusTags[] =
"OpusTags"
"\x14\x00\x00\x00" "opustags test packet"
"\x02\x00\x00\x00"
"\x09\x00\x00\x00" "TITLE=Foo"
"\x0a\x00\x00\x00" "ARTIST=Bar";
static void parse_standard()
{
ot::opus_tags tags;
ogg_packet op;
op.bytes = sizeof(standard_OpusTags) - 1;
op.packet = (unsigned char*) standard_OpusTags;
auto rc = ot::parse_tags(op, tags);
if (rc != ot::st::ok)
throw failure("ot::parse_tags did not return ok");
if (tags.vendor != "opustags test packet")
throw failure("bad vendor string");
if (tags.comments.size() != 2)
throw failure("bad number of comments");
auto it = tags.comments.begin();
if (*it != "TITLE=Foo")
throw failure("bad title");
++it;
if (*it != "ARTIST=Bar")
throw failure("bad artist");
if (tags.extra_data.size() != 0)
throw failure("found mysterious padding data");
}
/**
* Try parse_tags with packets that should not valid, or that might even
* corrupt the memory. Run this one with valgrind to ensure we're not
* overflowing.
*/
static void parse_corrupted()
{
size_t size = sizeof(standard_OpusTags);
char packet[size];
memcpy(packet, standard_OpusTags, size);
ot::opus_tags tags;
ogg_packet op;
op.packet = (unsigned char*) packet;
op.bytes = size;
char* header_data = packet;
char* vendor_length = header_data + 8;
char* vendor_string = vendor_length + 4;
char* comment_count = vendor_string + *vendor_length;
char* first_comment_length = comment_count + 4;
char* first_comment_data = first_comment_length + 4;
char* end = packet + size;
op.bytes = 7;
if (ot::parse_tags(op, tags) != ot::st::cut_magic_number)
throw failure("did not detect the overflowing magic number");
op.bytes = 11;
if (ot::parse_tags(op, tags) != ot::st::cut_vendor_length)
throw failure("did not detect the overflowing vendor string length");
op.bytes = size;
header_data[0] = 'o';
if (ot::parse_tags(op, tags) != ot::st::bad_magic_number)
throw failure("did not detect the bad magic number");
header_data[0] = 'O';
*vendor_length = end - vendor_string + 1;
if (ot::parse_tags(op, tags) != ot::st::cut_vendor_data)
throw failure("did not detect the overflowing vendor string");
*vendor_length = end - vendor_string - 3;
if (ot::parse_tags(op, tags) != ot::st::cut_comment_count)
throw failure("did not detect the overflowing comment count");
*vendor_length = comment_count - vendor_string;
++*comment_count;
if (ot::parse_tags(op, tags) != ot::st::cut_comment_length)
throw failure("did not detect the overflowing comment length");
*first_comment_length = end - first_comment_data + 1;
if (ot::parse_tags(op, tags) != ot::st::cut_comment_data)
throw failure("did not detect the overflowing comment data");
}
static void recode_standard()
{
ot::opus_tags tags;
ogg_packet op;
op.bytes = sizeof(standard_OpusTags) - 1;
op.packet = (unsigned char*) standard_OpusTags;
auto rc = ot::parse_tags(op, tags);
if (rc != ot::st::ok)
throw failure("ot::parse_tags did not return ok");
auto packet = ot::render_tags(tags);
if (packet.b_o_s != 0)
throw failure("b_o_s should not be set");
if (packet.e_o_s != 0)
throw failure("e_o_s should not be set");
if (packet.granulepos != 0)
throw failure("granule_post should be 0");
if (packet.packetno != 1)
throw failure("packetno should be 1");
if (packet.bytes != sizeof(standard_OpusTags) - 1)
throw failure("the packet is not the right size");
if (memcmp(packet.packet, standard_OpusTags, packet.bytes) != 0)
throw failure("the rendered packet is not what we expected");
}
static void recode_padding()
{
ot::opus_tags tags;
std::string padded_OpusTags(standard_OpusTags, sizeof(standard_OpusTags));
// ^ note: padded_OpusTags ends with a null byte here
padded_OpusTags += "hello";
ogg_packet op;
op.bytes = padded_OpusTags.size();
op.packet = (unsigned char*) padded_OpusTags.data();
auto rc = ot::parse_tags(op, tags);
if (rc != ot::st::ok)
throw failure("ot::parse_tags did not return ok");
if (tags.extra_data != "\0hello"s)
throw failure("corrupted extra data");
// recode the packet and ensure it's exactly the same
auto packet = ot::render_tags(tags);
if (static_cast<size_t>(packet.bytes) < padded_OpusTags.size())
throw failure("the packet was truncated");
if (static_cast<size_t>(packet.bytes) > padded_OpusTags.size())
throw failure("the packet got too big");
if (memcmp(packet.packet, padded_OpusTags.data(), packet.bytes) != 0)
throw failure("the rendered packet is not what we expected");
}
int main()
{
std::cout << "1..5\n";
run(check_identification, "check the OpusHead packet");
run(parse_standard, "parse a standard OpusTags packet");
run(parse_corrupted, "correctly reject invalid packets");
run(recode_standard, "recode a standard OpusTags packet");
run(recode_padding, "recode a OpusTags packet with padding");
return 0;
}

193
t/opustags.t Normal file
View File

@ -0,0 +1,193 @@
#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
use Test::More tests => 27;
use Digest::MD5;
use File::Basename;
use IPC::Open3;
use Symbol 'gensym';
my $opustags = '../opustags';
BAIL_OUT("$opustags does not exist or is not executable") if (! -x $opustags);
sub opustags {
my %opt;
%opt = %{pop @_} if ref $_[-1];
my ($pid, $pin, $pout, $perr);
$perr = gensym;
$pid = open3($pin, $pout, $perr, $opustags, @_);
binmode($pin, $opt{mode} // ':utf8');
binmode($pout, $opt{mode} // ':utf8');
binmode($perr, ':utf8');
local $/;
print $pin $opt{in} if defined $opt{in};
close $pin;
my $out = <$pout>;
my $err = <$perr>;
waitpid($pid, 0);
[$out, $err, $?]
}
####################################################################################################
# Tests related to the overall opustags executable, like the help message.
# No Opus file is manipulated here.
my $usage = opustags();
$usage->[0] =~ /^([^\n]*+)/;
my $version = $1;
like($version, qr/^opustags version (\d+\.\d+\.\d+)/, 'get the version string');
is_deeply($usage, [<<"EOF", "", 0], 'no options show the usage');
$version
Usage: opustags --help
opustags [OPTIONS] FILE
opustags OPTIONS FILE -o FILE
EOF
my $help = <<"EOF";
$version
Usage: opustags --help
opustags [OPTIONS] FILE
opustags OPTIONS FILE -o FILE
Options:
-h, --help print this help
-o, --output FILE set the output file
-i, --in-place overwrite the input file instead of writing a different output file
-y, --overwrite overwrite the output file if it already exists
-a, --add FIELD=VALUE add a comment
-d, --delete FIELD delete all previously existing comments of a specific type
-D, --delete-all delete all the previously existing comments
-s, --set FIELD=VALUE replace a comment (shorthand for --delete FIELD --add FIELD=VALUE)
-S, --set-all replace all the comments with the ones read from standard input
See the man page for extensive documentation.
EOF
is_deeply(opustags('--help'), [$help, '', 0], '--help displays the help message');
is_deeply(opustags('-h'), [$help, '', 0], '-h displays the help message too');
is_deeply(opustags('--derp'), ['', <<"EOF", 256], 'unrecognized option shows an error');
$opustags: unrecognized option '--derp'
EOF
####################################################################################################
# Test the main features of opustags on an Ogg Opus sample file.
sub md5 {
my ($file) = @_;
open(my $fh, '<', $file) or return;
my $ctx = Digest::MD5->new;
$ctx->addfile($fh);
$ctx->hexdigest
}
is(md5('gobble.opus'), '111a483596ac32352fbce4d14d16abd2', 'the sample is the one we expect');
is_deeply(opustags('gobble.opus'), [<<'EOF', '', 0], 'read the initial tags');
encoder=Lavc58.18.100 libopus
EOF
unlink('out.opus');
is_deeply(opustags(qw(gobble.opus -o out.opus)), ['', '', 0], 'copy the file without changes');
is(md5('out.opus'), '111a483596ac32352fbce4d14d16abd2', 'the copy is faithful');
# empty out.opus
{ my $fh; open($fh, '>', 'out.opus') and close($fh) or die }
is_deeply(opustags(qw(gobble.opus -o out.opus)), ['', <<'EOF', 256], 'refuse to override');
error: 'out.opus' already exists (use -y to overwrite)
EOF
is(md5('out.opus'), 'd41d8cd98f00b204e9800998ecf8427e', 'the output wasn\'t written');
is_deeply(opustags(qw(out.opus -o out.opus)), ['', <<'EOF', 256], 'output and input can\'t be the same');
error: Input and output files are the same
EOF
is_deeply(opustags(qw(gobble.opus -o out.opus --overwrite)), ['', '', 0], 'overwrite');
is(md5('out.opus'), '111a483596ac32352fbce4d14d16abd2', 'successfully overwritten');
is_deeply(opustags(qw(--in-place out.opus -a A=B --add=A=C --add), "TITLE=Foo Bar",
qw(--delete A --add TITLE=七面鳥 --set encoder=whatever -s 1=2 -s X=1 -a X=2 -s X=3)),
['', '', 0], 'complex tag editing');
is(md5('out.opus'), '66780307a6081523dc9040f3c47b0448', 'check the footprint');
is_deeply(opustags('out.opus'), [<<'EOF', '', 0], 'check the tags written');
A=B
A=C
TITLE=Foo Bar
TITLE=七面鳥
encoder=whatever
1=2
X=1
X=2
X=3
EOF
is_deeply(opustags(qw(out.opus -d A -d foo -s X=4 -a TITLE=gobble -d TITLE)), [<<'EOF', '', 0], 'dry editing');
encoder=whatever
1=2
X=4
TITLE=gobble
EOF
is(md5('out.opus'), '66780307a6081523dc9040f3c47b0448', 'the file did not change');
is_deeply(opustags(qw(-i out.opus -a fatal=yes -a FOO -a BAR)), ['', <<'EOF', 256], 'bad tag with --add');
invalid comment: 'FOO'
EOF
is(md5('out.opus'), '66780307a6081523dc9040f3c47b0448', 'the file did not change');
is_deeply(opustags(qw(-i out.opus -s fatal=yes -s FOO -s BAR)), ['', <<'EOF', 256], 'bad tag with --set');
invalid comment: 'FOO'
EOF
is(md5('out.opus'), '66780307a6081523dc9040f3c47b0448', 'the file did not change');
is_deeply(opustags(qw(out.opus --delete-all -a OK=yes)), [<<'EOF', '', 0], 'delete all');
OK=yes
EOF
is_deeply(opustags(qw(out.opus --set-all -a A=B -s X=Z -d OK), {in => <<'END_IN'}), [<<'END_OUT', '', 0], 'set all');
OK=yes again
ARTIST=七面鳥
A=A
X=Y
END_IN
OK=yes again
ARTIST=七面鳥
A=A
X=Y
A=B
X=Z
END_OUT
is_deeply(opustags(qw(out.opus -S), {in => <<'END_IN'}), [<<'END_OUT', <<'END_ERR', 0], 'set all with bad tags');
whatever
# thing
!
wrong=yes
END_IN
wrong=yes
END_OUT
warning: skipping malformed tag
warning: skipping malformed tag
warning: skipping malformed tag
END_ERR
sub slurp {
my ($filename) = @_;
local $/;
open(my $fh, '<', $filename);
binmode($fh);
my $data = <$fh>;
$data
}
my $data = slurp 'out.opus';
is_deeply(opustags('-', '-o', '-', {in => $data, mode => ':raw'}), [$data, '', 0], 'read opus from stdin and write to stdout');
unlink('out.opus');

29
t/tap.h Normal file
View File

@ -0,0 +1,29 @@
/**
* \file t/tap.h
*
* \brief
* Helpers for following the Test Anything Protocol.
*/
#pragma once
#include <exception>
#include <iostream>
class failure : public std::runtime_error {
public:
failure(const char *message) : std::runtime_error(message) {}
};
template <typename F>
static void run(F test, const char *name)
{
bool ok = false;
try {
test();
ok = true;
} catch (failure& e) {
std::cout << "# " << e.what() << "\n";
}
std::cout << (ok ? "ok" : "not ok") << " - " << name << "\n";
}

View File

@ -1,169 +0,0 @@
#include "actions.h"
#include "catch.h"
#include "ogg.h"
#include "tags_handlers/insertion_tags_handler.h"
#include "tags_handlers/stream_tags_handler.h"
#include <fstream>
#include <cstring>
using namespace opustags;
static bool same_streams(ogg::Decoder &a, ogg::Decoder &b)
{
std::shared_ptr<ogg::Stream> sa, sb;
ogg_packet pa, pb;
int ra, rb;
for (;;) {
sa = a.read_page();
sb = b.read_page();
if (!sa && !sb)
break; // both reached the end at the same time
if (!sa || !sb)
return false; // one stream is shorter than the other
if (sa->stream.serialno != sb->stream.serialno)
return false;
for (;;) {
ra = ogg_stream_packetout(&sa->stream, &pa);
rb = ogg_stream_packetout(&sb->stream, &pb);
if (ra != rb)
return false;
else if (ra == 0)
break;
else if (ra < 0)
throw std::runtime_error("ogg_stream_packetout failed");
// otherwise we got a valid packet on both sides
if (pa.bytes != pb.bytes || pa.b_o_s != pb.b_o_s || pa.e_o_s != pb.e_o_s)
return false;
if (pa.granulepos != pb.granulepos || pa.packetno != pb.packetno)
return false;
if (memcmp(pa.packet, pb.packet, pa.bytes) != 0)
return false;
}
}
return true;
}
static bool same_files(std::istream &a, std::istream &b)
{
static const size_t block = 1024;
char ba[block], bb[block];
while (a && b) {
a.read(ba, block);
b.read(bb, block);
if (a.gcount() != b.gcount())
return false;
if (memcmp(ba, bb, a.gcount()) != 0)
return false;
}
if (a || b)
return false; // one of them is shorter
return true;
}
TEST_CASE("editing an unknown stream should do nothing", "[actions]")
{
std::ifstream in("../tests/samples/beep.ogg");
std::stringstream out;
ogg::Decoder dec(in);
ogg::Encoder enc(out);
StreamTagsHandler editor(StreamTagsHandler::ALL_STREAMS);
edit_tags(dec, enc, editor);
in.clear();
in.seekg(0, in.beg);
out.clear();
out.seekg(0, out.beg);
REQUIRE(same_files(in, out));
}
TEST_CASE("fake editing of an Opus stream should preserve the stream", "[actions]")
{
std::ifstream in("../tests/samples/mystery-beep.ogg");
std::stringstream out;
ogg::Decoder dec(in);
ogg::Encoder enc(out);
StreamTagsHandler editor(StreamTagsHandler::ALL_STREAMS);
edit_tags(dec, enc, editor);
in.clear();
in.seekg(0, in.beg);
out.clear();
out.seekg(0, out.beg);
REQUIRE(same_files(in, out));
}
TEST_CASE("editing a specific stream", "[actions]")
{
std::ifstream in("../tests/samples/mystery-beep.ogg");
std::stringstream out;
{
ogg::Decoder dec(in);
ogg::Encoder enc(out);
InsertionTagsHandler editor(3, "pwnd", "yes");
edit_tags(dec, enc, editor);
}
in.clear();
in.seekg(0, in.beg);
out.clear();
out.seekg(0, out.beg);
{
ogg::Decoder a(in);
ogg::Decoder b(out);
std::shared_ptr<ogg::Stream> s2[6];
for (int i = 0; i < 6; i++) {
// get the headers
a.read_page();
s2[i] = b.read_page();
}
REQUIRE(s2[0]->type == ogg::OPUS_STREAM);
REQUIRE(s2[1]->type == ogg::UNKNOWN_STREAM);
REQUIRE(s2[2]->type == ogg::OPUS_STREAM);
REQUIRE(!s2[0]->tags.contains("pwnd"));
REQUIRE(s2[2]->tags.get("pwnd") == "yes");
REQUIRE(same_streams(a, b));
}
}
class GetLanguages : public StreamTagsHandler {
public:
GetLanguages() : StreamTagsHandler(StreamTagsHandler::ALL_STREAMS) {}
std::vector<std::string> languages;
protected:
void list_impl(const Tags &tags) {
languages.push_back(tags.get("LANGUAGE"));
}
};
TEST_CASE("listing tags", "[actions]")
{
std::ifstream in("../tests/samples/mystery-beep.ogg");
ogg::Decoder dec(in);
GetLanguages lister;
list_tags(dec, lister);
REQUIRE(lister.languages.size() == 2);
REQUIRE(lister.languages[0] == "und");
REQUIRE(lister.languages[1] == "und");
}
TEST_CASE("listing tags, full scan", "[actions]")
{
std::ifstream in("../tests/samples/mystery-beep.ogg");
ogg::Decoder dec(in);
GetLanguages lister;
list_tags(dec, lister, true);
REQUIRE(lister.languages.size() == 2);
REQUIRE(lister.languages[0] == "und");
REQUIRE(lister.languages[1] == "und");
}

View File

@ -1,2 +0,0 @@
#define CATCH_CONFIG_MAIN
#include "catch.h"

View File

@ -1,148 +0,0 @@
#include "ogg.h"
#include "catch.h"
#include <fstream>
using namespace opustags;
TEST_CASE("decoding a single-stream Ogg Opus file", "[ogg]")
{
std::ifstream src("../tests/samples/mystery.ogg");
ogg::Decoder dec(src);
std::shared_ptr<ogg::Stream> s = dec.read_page();
REQUIRE(s != nullptr);
REQUIRE(s->state == ogg::HEADER_READY);
REQUIRE(s->type == ogg::OPUS_STREAM);
std::shared_ptr<ogg::Stream> s2 = dec.read_page();
REQUIRE(s2 == s);
REQUIRE(s->state == ogg::TAGS_READY);
REQUIRE(s->type == ogg::OPUS_STREAM);
REQUIRE(s->tags.get("encoder") == "Lavc57.24.102 libopus");
}
TEST_CASE("decoding garbage", "[ogg]")
{
std::ifstream src("Makefile");
ogg::Decoder dec(src);
REQUIRE_THROWS(dec.read_page());
}
TEST_CASE("decoding an Ogg Vorbis file", "[ogg]")
{
std::ifstream src("../tests/samples/beep.ogg");
ogg::Decoder dec(src);
std::shared_ptr<ogg::Stream> s = dec.read_page();
REQUIRE(s != nullptr);
REQUIRE(s->state == ogg::HEADER_READY);
REQUIRE(s->type == ogg::UNKNOWN_STREAM);
s = dec.read_page();
REQUIRE(s != nullptr);
REQUIRE(s->state == ogg::RAW_READY);
}
TEST_CASE("decoding a multi-stream file", "[ogg]")
{
std::ifstream src("../tests/samples/mystery-beep.ogg");
ogg::Decoder dec(src);
std::shared_ptr<ogg::Stream> first, second, third, s;
s = dec.read_page();
first = s;
REQUIRE(s != nullptr);
REQUIRE(s->state == ogg::HEADER_READY);
REQUIRE(s->type == ogg::OPUS_STREAM);
s = dec.read_page();
second = s;
REQUIRE(s != nullptr);
REQUIRE(s != first);
REQUIRE(s->state == ogg::HEADER_READY);
REQUIRE(s->type == ogg::UNKNOWN_STREAM);
s = dec.read_page();
third = s;
REQUIRE(s != nullptr);
REQUIRE(s != first);
REQUIRE(s != second);
REQUIRE(s->state == ogg::HEADER_READY);
REQUIRE(s->type == ogg::OPUS_STREAM);
s = dec.read_page();
REQUIRE(s == first);
REQUIRE(s->state == ogg::TAGS_READY);
s = dec.read_page();
REQUIRE(s == second);
REQUIRE(s->state == ogg::RAW_READY);
s = dec.read_page();
REQUIRE(s == third);
REQUIRE(s->state == ogg::TAGS_READY);
}
static void craft_stream(std::ostream &out, const std::string &tags_data)
{
ogg::Encoder enc(out);
ogg_stream_state os;
ogg_page og;
ogg_packet op;
ogg_stream_init(&os, 0);
op.packet = reinterpret_cast<unsigned char*>(const_cast<char*>("OpusHead"));
op.bytes = 8;
op.b_o_s = 1;
op.e_o_s = 0;
op.granulepos = 0;
op.packetno = 0;
ogg_stream_packetin(&os, &op);
ogg_stream_flush(&os, &og);
enc.write_raw_page(og);
op.packet = reinterpret_cast<unsigned char*>(const_cast<char*>(tags_data.data()));
op.bytes = tags_data.size();
op.b_o_s = 0;
op.e_o_s = 1;
op.granulepos = 0;
op.packetno = 1;
ogg_stream_packetin(&os, &op);
ogg_stream_flush(&os, &og);
enc.write_raw_page(og);
ogg_stream_clear(&os);
}
const char *evil_tags =
"OpusTags"
"\x00\x00\x00\x00" "" /* vendor */
"\x01\x00\x00\x00" /* one comment */
"\xFA\x00\x00\x00" "TITLE=Evil"
/* ^ should be \x0A as the length of the comment is 10 */
;
TEST_CASE("decoding a malicious Ogg Opus file", "[ogg]")
{
std::stringstream buf;
craft_stream(buf, std::string(evil_tags, 8 + 4 + 4 + 4 + 10));
buf.seekg(0, buf.beg);
ogg::Decoder dec(buf);
std::shared_ptr<ogg::Stream> s = dec.read_page();
REQUIRE(s != nullptr);
REQUIRE(s->state == ogg::HEADER_READY);
REQUIRE(s->type == ogg::OPUS_STREAM);
REQUIRE_THROWS(dec.read_page());
}
TEST_CASE("decoding a bad stream", "[ogg]")
{
std::ifstream in("uioprheuio");
REQUIRE_THROWS(std::make_shared<ogg::Decoder>(in));
}
// Encoding is trickier, and might as well be done in actions_test.cc, given
// opustags::edit_tags covers all of Encoder's regular code.

View File

@ -1,205 +0,0 @@
#include "options.h"
#include "catch.h"
#include "tags_handlers/export_tags_handler.h"
#include "tags_handlers/external_edit_tags_handler.h"
#include "tags_handlers/import_tags_handler.h"
#include "tags_handlers/insertion_tags_handler.h"
#include "tags_handlers/listing_tags_handler.h"
#include "tags_handlers/modification_tags_handler.h"
#include "tags_handlers/removal_tags_handler.h"
#include <memory>
using namespace opustags;
static std::unique_ptr<char[]> string_to_uptr(const std::string &str)
{
auto ret = std::make_unique<char[]>(str.size() + 1);
for (size_t i = 0; i < str.size(); i++)
ret[i] = str[i];
ret[str.size()] = 0;
return ret;
}
static Options retrieve_options(
std::vector<std::string> args, bool fake_input_path = true)
{
// need to pass non-const char*, but we got const objects. make copies
std::vector<std::unique_ptr<char[]>> arg_holders;
arg_holders.push_back(string_to_uptr("fake/path/to/program"));
for (size_t i = 0; i < args.size(); i++)
arg_holders.push_back(string_to_uptr(args[i]));
if (fake_input_path)
arg_holders.push_back(string_to_uptr("fake/path/to/input"));
auto plain_args = std::make_unique<char*[]>(arg_holders.size());
for (size_t i = 0; i < arg_holders.size(); i++)
plain_args[i] = arg_holders[i].get();
return parse_args(arg_holders.size(), plain_args.get());
}
// retrieve a specific TagsHandler from the CompositeTagsHandler in options
template<typename T> static T *get_handler(
const Options &options, const size_t index)
{
const auto handlers = options.tags_handler.get_handlers();
const auto handler = dynamic_cast<T*>(handlers.at(index).get());
REQUIRE(handler);
return handler;
}
TEST_CASE("option parsing", "[options]")
{
SECTION("--help") {
REQUIRE(retrieve_options({"--help"}).show_help);
REQUIRE(retrieve_options({"-h"}).show_help);
REQUIRE(!retrieve_options({}).show_help);
}
SECTION("--version") {
REQUIRE(retrieve_options({"--version"}).show_version);
REQUIRE(retrieve_options({"-V"}).show_version);
REQUIRE(!retrieve_options({}).show_version);
}
SECTION("--overwrite") {
REQUIRE(retrieve_options({"--overwrite"}).overwrite);
REQUIRE(retrieve_options({"-y"}).overwrite);
REQUIRE(!retrieve_options({}).overwrite);
}
SECTION("--full") {
REQUIRE(retrieve_options({"--full"}).full);
REQUIRE(!retrieve_options({}).full);
}
SECTION("--in-place") {
REQUIRE(!retrieve_options({}).in_place);
REQUIRE(retrieve_options({}).path_out.empty());
REQUIRE(retrieve_options({"--in-place", "ABC"}, false).in_place);
REQUIRE(
retrieve_options({"--in-place", "ABC"}, false).path_out == ".otmp");
REQUIRE(retrieve_options({"--in-place"}).in_place);
REQUIRE(retrieve_options({"--in-place"}).path_out == ".otmp");
REQUIRE(retrieve_options({"--in-place=ABC"}).in_place);
REQUIRE(retrieve_options({"--in-place=ABC"}).path_out == "ABC");
REQUIRE(retrieve_options({"-i", "ABC"}, false).in_place);
REQUIRE(retrieve_options({"-i", "ABC"}, false).path_out == ".otmp");
REQUIRE(retrieve_options({"-i"}).in_place);
REQUIRE(retrieve_options({"-i"}).path_out == ".otmp");
REQUIRE(retrieve_options({"-iABC"}).in_place);
REQUIRE(retrieve_options({"-iABC"}).path_out == "ABC");
}
SECTION("input") {
REQUIRE_THROWS(retrieve_options({}, false));
REQUIRE_THROWS(retrieve_options({""}, false));
REQUIRE(retrieve_options({"input"}, false).path_in == "input");
REQUIRE_THROWS(retrieve_options({"input", "extra argument"}, false));
}
SECTION("--output") {
REQUIRE(retrieve_options({"--output", "ABC"}).path_out == "ABC");
REQUIRE(retrieve_options({"-o", "ABC"}).path_out == "ABC");
REQUIRE_THROWS(retrieve_options({"--output", ""}));
REQUIRE_THROWS(retrieve_options({"-o", ""}));
}
SECTION("--import") {
get_handler<ImportTagsHandler>(retrieve_options({"--import"}), 0);
}
SECTION("--export") {
get_handler<ExportTagsHandler>(retrieve_options({"--export"}), 0);
}
SECTION("--edit") {
get_handler<ExternalEditTagsHandler>(retrieve_options({"--edit"}), 0);
get_handler<ExternalEditTagsHandler>(retrieve_options({"-e"}), 0);
}
SECTION("--list") {
get_handler<ListingTagsHandler>(retrieve_options({"--list"}), 0);
get_handler<ListingTagsHandler>(retrieve_options({"-l"}), 0);
// TODO:
// test enabling / disabling colors, which should be
// contained inside the state of ListingTagsHandler
// TODO:
// it should be the default operation for readonly mode - test this too
}
SECTION("--delete-all") {
REQUIRE(get_handler<RemovalTagsHandler>(
retrieve_options({"--delete-all"}), 0)->get_tag_key().empty());
REQUIRE(get_handler<RemovalTagsHandler>(
retrieve_options({"-D"}), 0)->get_tag_key().empty());
}
SECTION("--delete") {
const auto opts = retrieve_options({"--delete", "A", "-d", "B"});
REQUIRE(get_handler<RemovalTagsHandler>(opts, 0)->get_tag_key() == "A");
REQUIRE(get_handler<RemovalTagsHandler>(opts, 1)->get_tag_key() == "B");
REQUIRE_THROWS(retrieve_options({"--delete", "invalid="}));
}
SECTION("--add") {
const auto opts = retrieve_options({"--add", "A=1", "-a", "B=2"});
const auto handler1 = get_handler<InsertionTagsHandler>(opts, 0);
const auto handler2 = get_handler<InsertionTagsHandler>(opts, 1);
REQUIRE(handler1->get_tag_key() == "A");
REQUIRE(handler1->get_tag_value() == "1");
REQUIRE(handler2->get_tag_key() == "B");
REQUIRE(handler2->get_tag_value() == "2");
REQUIRE_THROWS(retrieve_options({"--add", "invalid"}));
}
SECTION("--set") {
const auto opts = retrieve_options({"--set", "A=1", "-s", "B=2"});
const auto handler1 = get_handler<ModificationTagsHandler>(opts, 0);
const auto handler2 = get_handler<ModificationTagsHandler>(opts, 1);
REQUIRE(handler1->get_tag_key() == "A");
REQUIRE(handler1->get_tag_value() == "1");
REQUIRE(handler2->get_tag_key() == "B");
REQUIRE(handler2->get_tag_value() == "2");
REQUIRE_THROWS(retrieve_options({"--set", "invalid"}));
}
SECTION("--stream") {
// by default, use all the streams
REQUIRE(
get_handler<RemovalTagsHandler>(retrieve_options({"-d", "xyz"}), 0)
->get_streamno() == StreamTagsHandler::ALL_STREAMS);
// ...unless the user supplies an option to use a specific stream
REQUIRE(
get_handler<RemovalTagsHandler>(
retrieve_options({"--stream", "1", "-d", "xyz"}), 0)
->get_streamno() == 1);
// ...which can be combined multiple times
{
const auto opts = retrieve_options({
"--stream", "1",
"-d", "xyz",
"--stream", "2",
"-d", "abc"});
const auto handler1 = get_handler<RemovalTagsHandler>(opts, 0);
const auto handler2 = get_handler<RemovalTagsHandler>(opts, 1);
REQUIRE(handler1->get_streamno() == 1);
REQUIRE(handler2->get_streamno() == 2);
}
// ...or contain comma separated values
{
const auto opts = retrieve_options({
"--stream", "1,2", "-d", "xyz"});
const auto handler1 = get_handler<RemovalTagsHandler>(opts, 0);
const auto handler2 = get_handler<RemovalTagsHandler>(opts, 1);
REQUIRE(handler1->get_streamno() == 1);
REQUIRE(handler2->get_streamno() == 2);
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,96 +0,0 @@
#include "tags_handlers/composite_tags_handler.h"
#include "catch.h"
using namespace opustags;
namespace
{
struct DummyTagsHandler : ITagsHandler
{
DummyTagsHandler();
bool relevant(const int streamno) override;
void list(const int streamno, const Tags &) override;
bool edit(const int streamno, Tags &) override;
bool done() override;
bool relevant_ret, edit_ret, done_ret, list_fired;
};
}
DummyTagsHandler::DummyTagsHandler()
: relevant_ret(true), edit_ret(true), done_ret(true), list_fired(false)
{
}
bool DummyTagsHandler::relevant(const int)
{
return relevant_ret;
}
void DummyTagsHandler::list(const int, const Tags &)
{
list_fired = true;
}
bool DummyTagsHandler::edit(const int, Tags &)
{
return edit_ret;
}
bool DummyTagsHandler::done()
{
return done_ret;
}
TEST_CASE("composite tags handler", "[tags_handlers]")
{
auto handler1 = std::make_shared<DummyTagsHandler>();
auto handler2 = std::make_shared<DummyTagsHandler>();
CompositeTagsHandler composite_handler;
composite_handler.add_handler(handler1);
composite_handler.add_handler(handler2);
SECTION("relevance") {
const int dummy_streamno = 1;
handler1->relevant_ret = true;
handler2->relevant_ret = true;
REQUIRE(composite_handler.relevant(dummy_streamno));
handler1->relevant_ret = false;
REQUIRE(composite_handler.relevant(dummy_streamno));
handler2->relevant_ret = false;
REQUIRE(!composite_handler.relevant(dummy_streamno));
}
SECTION("listing") {
const int dummy_streamno = 1;
Tags dummy_tags;
REQUIRE(!handler1->list_fired);
REQUIRE(!handler2->list_fired);
composite_handler.list(dummy_streamno, dummy_tags);
REQUIRE(handler1->list_fired);
REQUIRE(handler2->list_fired);
}
SECTION("editing") {
const int dummy_streamno = 1;
Tags dummy_tags;
handler1->edit_ret = true;
handler2->edit_ret = true;
REQUIRE(composite_handler.edit(dummy_streamno, dummy_tags));
handler1->edit_ret = false;
REQUIRE(composite_handler.edit(dummy_streamno, dummy_tags));
handler2->edit_ret = false;
REQUIRE(!composite_handler.edit(dummy_streamno, dummy_tags));
}
SECTION("finish") {
handler1->done_ret = true;
handler2->done_ret = true;
REQUIRE(composite_handler.done());
handler1->done_ret = false;
REQUIRE(!composite_handler.done());
handler2->done_ret = false;
REQUIRE(!composite_handler.done());
}
}

View File

@ -1,23 +0,0 @@
#include "tags_handlers/export_tags_handler.h"
#include "catch.h"
#include <sstream>
using namespace opustags;
TEST_CASE("export tags handler", "[tags_handlers]")
{
std::stringstream ss;
ExportTagsHandler handler(ss);
handler.list(1, {{{"a", "value1"}, {"b", "value2"}}});
handler.list(2, {{{"c", "value3"}, {"d", "value4"}}});
REQUIRE(ss.str() == R"([Stream 1]
a=value1
b=value2
[Stream 2]
c=value3
d=value4
)");
}

View File

@ -1,142 +0,0 @@
#include "tags_handlers/import_tags_handler.h"
#include "catch.h"
#include <sstream>
using namespace opustags;
TEST_CASE("import tags handler", "[tags_handlers]")
{
SECTION("tags for streams not mentioned in import get emptied")
{
Tags tags = {{{"remove", "me"}}};
std::stringstream ss;
ImportTagsHandler handler(ss);
REQUIRE(handler.edit(1, tags));
REQUIRE(tags.get_all().empty());
}
SECTION("streams that do not exist in the input file are ignored")
{
// TODO: rather than ignoring, at least print a warning
// requires #6
Tags tags;
std::stringstream ss;
ss << "[Stream 5]\nkey = value\n";
ImportTagsHandler handler(ss);
REQUIRE(!handler.edit(1, tags));
REQUIRE(tags.get_all().empty());
}
SECTION("adding unique tags")
{
Tags tags;
std::stringstream ss;
ss << "[Stream 1]\nkey = value\nkey2 = value2\n";
ImportTagsHandler handler(ss);
REQUIRE(handler.edit(1, tags));
REQUIRE(tags.get_all().size() == 2);
REQUIRE(tags.get("key") == "value");
REQUIRE(tags.get("key2") == "value2");
}
SECTION("adding tags with the same key")
{
Tags tags;
std::stringstream ss;
ss << "[Stream 1]\nkey = value1\nkey = value2\n";
ImportTagsHandler handler(ss);
REQUIRE(handler.edit(1, tags));
REQUIRE(tags.get_all().size() == 2);
REQUIRE(tags.get_all().at(0).key == "key");
REQUIRE(tags.get_all().at(1).key == "key");
REQUIRE(tags.get_all().at(0).value == "value1");
REQUIRE(tags.get_all().at(1).value == "value2");
}
SECTION("overwriting existing tags")
{
Tags tags = {{{"remove", "me"}}};
std::stringstream ss;
ss << "[Stream 1]\nkey = value\nkey2 = value2\n";
ImportTagsHandler handler(ss);
REQUIRE(handler.edit(1, tags));
REQUIRE(tags.get_all().size() == 2);
REQUIRE(tags.get("key") == "value");
REQUIRE(tags.get("key2") == "value2");
}
SECTION("various whitespace issues are worked around")
{
Tags tags;
std::stringstream ss;
ss << " [StrEaM 1] \n\n key = value \nkey2=value2\n";
ImportTagsHandler handler(ss);
REQUIRE(handler.edit(1, tags));
REQUIRE(tags.get_all().size() == 2);
REQUIRE(tags.get("key") == "value");
REQUIRE(tags.get("key2") == "value2");
}
SECTION("not specifying stream assumes first stream")
{
Tags tags;
std::stringstream ss;
ss << "key=value";
ImportTagsHandler handler(ss);
REQUIRE(handler.edit(1, tags));
REQUIRE(tags.get_all().size() == 1);
REQUIRE(tags.get("key") == "value");
}
SECTION("multiple streams")
{
Tags tags1;
Tags tags2;
std::stringstream ss;
ss << "[stream 1]\nkey=value\n[stream 2]\nkey2=value2";
ImportTagsHandler handler(ss);
REQUIRE(handler.edit(1, tags1));
REQUIRE(handler.edit(2, tags2));
REQUIRE(tags1.get_all().size() == 1);
REQUIRE(tags2.get_all().size() == 1);
REQUIRE(tags1.get("key") == "value");
REQUIRE(tags2.get("key2") == "value2");
}
SECTION("sections listed twice are concatenated")
{
Tags tags;
std::stringstream ss;
ss << "[stream 1]\nkey=value\n"
"[stream 2]\nkey=irrelevant\n"
"[Stream 1]\nkey2=value2";
ImportTagsHandler handler(ss);
REQUIRE(handler.edit(1, tags));
REQUIRE(tags.get_all().size() == 2);
REQUIRE(tags.get("key") == "value");
REQUIRE(tags.get("key2") == "value2");
}
SECTION("weird input throws errors - malformed section headers")
{
Tags tags;
std::stringstream ss;
ss << "[stream huh]\n";
REQUIRE_THROWS({
ImportTagsHandler handler(ss);
handler.edit(1, tags);
});
}
SECTION("weird input throws errors - malformed lines")
{
Tags tags;
std::stringstream ss;
ss << "tag huh\n";
REQUIRE_THROWS({
ImportTagsHandler handler(ss);
handler.edit(1, tags);
});
}
}

View File

@ -1,22 +0,0 @@
#include "tags_handlers/insertion_tags_handler.h"
#include "catch.h"
using namespace opustags;
TEST_CASE("insertion tags handler", "[tags_handlers]")
{
Tags tags;
const auto streamno = 1;
const auto expected_tag_key = "tag_key";
const auto expected_tag_value = "tag_value";
InsertionTagsHandler handler(
streamno, expected_tag_key, expected_tag_value);
REQUIRE(handler.edit(streamno, tags));
REQUIRE(tags.get_all().size() == 1);
REQUIRE(tags.get(expected_tag_key) == expected_tag_value);
REQUIRE(handler.edit(streamno, tags));
REQUIRE(tags.get_all().size() == 2);
REQUIRE(tags.get(expected_tag_key) == expected_tag_value);
}

View File

@ -1,22 +0,0 @@
#include "tags_handlers/listing_tags_handler.h"
#include "catch.h"
#include <sstream>
using namespace opustags;
TEST_CASE("listing tags handler", "[tags_handlers]")
{
const auto streamno = 1;
Tags tags;
tags.add("z", "value1");
tags.add("a", "value2");
tags.add("y", "value3");
tags.add("c", "value4");
std::stringstream ss;
ListingTagsHandler handler(streamno, ss);
handler.list(streamno, tags);
REQUIRE(ss.str() == "z=value1\na=value2\ny=value3\nc=value4\n");
}

View File

@ -1,33 +0,0 @@
#include "tags_handlers/modification_tags_handler.h"
#include "catch.h"
using namespace opustags;
TEST_CASE("modification tags handler", "[tags_handlers]")
{
const auto streamno = 1;
const auto first_tag_key = "tag_key";
const auto other_tag_key = "other_tag_key";
const auto dummy_value = "dummy";
const auto new_value = "dummy 2";
Tags tags;
tags.add(first_tag_key, dummy_value);
REQUIRE(tags.get_all().size() == 1);
// setting nonexistent keys adds them
ModificationTagsHandler handler1(streamno, other_tag_key, dummy_value);
REQUIRE(handler1.edit(streamno, tags));
REQUIRE(tags.get_all().size() == 2);
REQUIRE(tags.get(other_tag_key) == dummy_value);
// setting existing keys overrides their values
ModificationTagsHandler handler2(streamno, other_tag_key, new_value);
REQUIRE(handler2.edit(streamno, tags));
REQUIRE(tags.get_all().size() == 2);
REQUIRE(tags.get(other_tag_key) == new_value);
// setting existing keys reports no modifications if values are the same
REQUIRE(!handler2.edit(streamno, tags));
}

View File

@ -1,43 +0,0 @@
#include "tags_handlers/removal_tags_handler.h"
#include "catch.h"
using namespace opustags;
TEST_CASE("removal tags handler", "[tags_handlers]")
{
const auto streamno = 1;
SECTION("removing a single tag")
{
const auto expected_tag_key = "tag_key";
const auto other_tag_key = "other_tag_key";
const auto dummy_value = "dummy";
RemovalTagsHandler handler(streamno, expected_tag_key);
Tags tags;
tags.add(expected_tag_key, dummy_value);
tags.add(other_tag_key, dummy_value);
REQUIRE(tags.get_all().size() == 2);
REQUIRE(handler.edit(streamno, tags));
REQUIRE(tags.get_all().size() == 1);
REQUIRE(tags.contains(other_tag_key));
REQUIRE(!handler.edit(streamno, tags));
}
SECTION("removing all tags")
{
RemovalTagsHandler handler(streamno);
Tags tags;
tags.add("z", "value1");
tags.add("a", "value2");
tags.add("y", "value3");
tags.add("c", "value4");
REQUIRE(tags.get_all().size() == 4);
REQUIRE(handler.edit(streamno, tags));
REQUIRE(tags.get_all().size() == 0);
REQUIRE(!handler.edit(streamno, tags));
}
}

View File

@ -1,92 +0,0 @@
#include "tags_handlers/stream_tags_handler.h"
#include "catch.h"
using namespace opustags;
namespace
{
class DummyTagsHandler final : public StreamTagsHandler
{
public:
DummyTagsHandler(const int streamno);
protected:
void list_impl(const Tags &) override;
bool edit_impl(Tags &) override;
public:
bool list_fired, edit_fired;
};
}
DummyTagsHandler::DummyTagsHandler(const int streamno)
: StreamTagsHandler(streamno), list_fired(false), edit_fired(false)
{
}
void DummyTagsHandler::list_impl(const Tags &)
{
list_fired = true;
}
bool DummyTagsHandler::edit_impl(Tags &)
{
edit_fired = true;
return true;
}
TEST_CASE("stream-based tags handler", "[tags_handlers]")
{
SECTION("concrete stream") {
const auto relevant_stream_number = 1;
const auto irrelevant_stream_number = 2;
Tags dummy_tags;
DummyTagsHandler handler(relevant_stream_number);
SECTION("relevance") {
REQUIRE(!handler.relevant(irrelevant_stream_number));
REQUIRE(handler.relevant(relevant_stream_number));
}
SECTION("listing") {
handler.list(irrelevant_stream_number, dummy_tags);
REQUIRE(!handler.list_fired);
handler.list(relevant_stream_number, dummy_tags);
REQUIRE(handler.list_fired);
}
SECTION("Editing") {
REQUIRE(!handler.edit(irrelevant_stream_number, dummy_tags));
REQUIRE(!handler.edit_fired);
REQUIRE(handler.edit(relevant_stream_number, dummy_tags));
REQUIRE(handler.edit_fired);
}
SECTION("finish through listing") {
REQUIRE(!handler.edit(irrelevant_stream_number, dummy_tags));
REQUIRE(!handler.done());
REQUIRE(handler.edit(relevant_stream_number, dummy_tags));
REQUIRE(handler.done());
}
SECTION("finish through editing") {
handler.list(irrelevant_stream_number, dummy_tags);
REQUIRE(!handler.done());
handler.list(relevant_stream_number, dummy_tags);
REQUIRE(handler.done());
}
}
SECTION("any stream") {
Tags dummy_tags;
DummyTagsHandler handler(StreamTagsHandler::ALL_STREAMS);
REQUIRE(handler.relevant(1));
REQUIRE(handler.relevant(2));
REQUIRE(handler.relevant(3));
REQUIRE(!handler.done());
handler.list(1, dummy_tags);
REQUIRE(!handler.done());
handler.list(2, dummy_tags);
REQUIRE(!handler.done());
}
}

View File

@ -1,85 +0,0 @@
#include "tags.h"
#include "catch.h"
using namespace opustags;
TEST_CASE("tag manipulation", "[tags]")
{
SECTION("basic operations") {
Tags tags;
REQUIRE(!tags.contains("a"));
tags.add("a", "1");
REQUIRE(tags.get("a") == "1");
REQUIRE(tags.contains("a"));
tags.remove("a");
REQUIRE(!tags.contains("a"));
REQUIRE_THROWS(tags.get("a"));
}
SECTION("clearing") {
Tags tags;
tags.add("a", "1");
tags.add("b", "2");
REQUIRE(tags.get_all().size() == 2);
tags.clear();
REQUIRE(tags.get_all().empty());
}
SECTION("maintaing order of insertions") {
Tags tags;
tags.add("z", "1");
tags.add("y", "2");
tags.add("x", "3");
tags.add("y", "4");
REQUIRE(tags.get_all().size() == 4);
REQUIRE(tags.get_all()[0].key == "z");
REQUIRE(tags.get_all()[1].key == "y");
REQUIRE(tags.get_all()[2].key == "x");
REQUIRE(tags.get_all()[3].key == "y");
tags.remove("z");
REQUIRE(tags.get_all().size() == 3);
REQUIRE(tags.get_all()[0].key == "y");
REQUIRE(tags.get_all()[1].key == "x");
REQUIRE(tags.get_all()[2].key == "y");
tags.add("gamma", "5");
REQUIRE(tags.get_all().size() == 4);
REQUIRE(tags.get_all()[0].key == "y");
REQUIRE(tags.get_all()[1].key == "x");
REQUIRE(tags.get_all()[2].key == "y");
REQUIRE(tags.get_all()[3].key == "gamma");
}
SECTION("key to multiple values") {
// ARTIST is set once per artist.
// https://www.xiph.org/vorbis/doc/v-comment.html
Tags tags;
tags.add("ARTIST", "You");
tags.add("ARTIST", "Me");
REQUIRE(tags.get_all().size() == 2);
}
SECTION("removing multivalues should remove all of them") {
Tags tags;
tags.add("ARTIST", "You");
tags.add("ARTIST", "Me");
tags.remove("ARTIST");
REQUIRE(tags.get_all().empty());
}
SECTION("tag parsing") {
Tag t = parse_tag("TITLE=Foo=Bar");
REQUIRE(t.key == "TITLE");
REQUIRE(t.value == "Foo=Bar");
}
SECTION("case insensitiveness for keys") {
Tags tags;
tags.add("TITLE", "Boop");
REQUIRE(tags.get("title") == "Boop");
tags.remove("titLE");
REQUIRE(tags.get_all().empty());
}
}