mirror of
				https://github.com/fmang/opustags.git
				synced 2025-10-31 00:48:10 +01:00 
			
		
		
		
	Compare commits
	
		
			23 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 37deeb32d3 | ||
|  | cd99ac50c7 | ||
|  | 82a5124f14 | ||
|  | 6c0d4fc297 | ||
|  | c74e19922f | ||
|  | 5c1a7b3a99 | ||
|  | b70e65f0d4 | ||
|  | fc7e5e939e | ||
|  | e8b66a6207 | ||
|  | ba5c151b5d | ||
|  | 76afc0efd5 | ||
|  | a54bac8f55 | ||
|  | 3293647e8f | ||
|  | d9b051210b | ||
|  | 3da23b58c9 | ||
|  | 6ae008befd | ||
|  | 0067162ffb | ||
|  | 7ec3551f62 | ||
|  | a63c06dc05 | ||
|  | e2e7e2a5a0 | ||
|  | 70500a6aac | ||
|  | 49bb94841e | ||
|  | dcb128f179 | 
							
								
								
									
										30
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| name: Continuous Integration | ||||
| on: | ||||
|   push: | ||||
|     branches: [master] | ||||
|   pull_request: | ||||
|     branches: [master] | ||||
|   workflow_dispatch: | ||||
| env: | ||||
|   LC_CTYPE: C.UTF-8 | ||||
|   CMAKE_COLOR_DIAGNOSTICS: ON | ||||
| jobs: | ||||
|   build: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|     - name: Checkout git repository | ||||
|       uses: actions/checkout@v4 | ||||
|     - name: Install dependencies | ||||
|       run: | | ||||
|         sudo apt install cmake g++ pkg-config libogg-dev ffmpeg libtest-harness-perl libtest-deep-perl liblist-moreutils-perl libtest-utf8-perl | ||||
|     - name: Build | ||||
|       env: | ||||
|         CXX: g++ | ||||
|         CXXFLAGS: -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS -D_GLIBCXX_DEBUG -O2 -flto=auto -g -Wall -Wextra -Werror=format-security -fstack-protector-strong -fstack-clash-protection -fcf-protection -fsanitize=address,undefined | ||||
|         LDFLAGS: -fsanitize=address,undefined | ||||
|       run: | | ||||
|         cmake -B target -DCMAKE_VERBOSE_MAKEFILE:BOOL=ON | ||||
|         cmake --build target | ||||
|     - name: Test | ||||
|       run: | | ||||
|         cmake --build target --target check | ||||
							
								
								
									
										83
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | ||||
| name: Release | ||||
|  | ||||
| on: | ||||
|   release: | ||||
|     types: [published] | ||||
|  | ||||
| jobs: | ||||
|   extract-tag: | ||||
|     name: Extract release tag | ||||
|     runs-on: ubuntu-latest | ||||
|     outputs: | ||||
|       tag: ${{ env.RELEASE_TAG }} | ||||
|       version: ${{ env.RELEASE_VERSION }} | ||||
|     steps: | ||||
|       - name: Parse refs | ||||
|         run: | | ||||
|           RELEASE_TAG=${GITHUB_REF#refs/*/} | ||||
|           test -z $RELEASE_TAG && exit 1 | ||||
|           RELEASE_VERSION=$(echo "$RELEASE_TAG" | sed 's/^[^0-9]*//') | ||||
|           test -z $RELEASE_VERSION && exit 1 | ||||
|           echo "Releasing version: $RELEASE_VERSION; Tag: $RELEASE_TAG" | ||||
|           echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_ENV | ||||
|           echo "RELEASE_TAG=$RELEASE_TAG" >> $GITHUB_ENV | ||||
|  | ||||
|   build: | ||||
|     name: Build | ||||
|     runs-on: ubuntu-22.04 | ||||
|     needs: ["extract-tag"] | ||||
|     steps: | ||||
|       - name: git checkout | ||||
|         uses: actions/checkout@v4 | ||||
|  | ||||
|       - name: Install dependencies | ||||
|         run: sudo apt update && sudo apt install -y g++ cmake pkg-config libogg-dev libogg0 | ||||
|  | ||||
|       - name: CMake build | ||||
|         run: | | ||||
|           mkdir build | ||||
|           cmake -DCMAKE_INSTALL_PREFIX=/usr -S . | ||||
|           make | ||||
|           make install DESTDIR=./build | ||||
|        | ||||
|       - name: Create control file | ||||
|         run: | | ||||
|           mkdir -p build/DEBIAN | ||||
|           cat << EOF > build/DEBIAN/control | ||||
|           Package: opustags | ||||
|           Version: ${{ needs.extract-tag.outputs.version }} | ||||
|           Architecture: amd64 | ||||
|           Maintainer: github.com/fmang | ||||
|           Depends: libogg0 (>= 1.3.4) | libogg (>= 1.3.4) | ||||
|           Priority: optional | ||||
|           Description: Ogg Opus tags editor | ||||
|           EOF | ||||
|        | ||||
|       - name: Create package file | ||||
|         run: | | ||||
|           dpkg-deb -v --build ./build | ||||
|           mv build.deb opustags-${{ needs.extract-tag.outputs.version }}-amd64.deb | ||||
|  | ||||
|       - name: Upload artifact | ||||
|         uses: actions/upload-artifact@v4 | ||||
|         with: | ||||
|           name: debian-pkg | ||||
|           path: opustags-${{ needs.extract-tag.outputs.version }}-amd64.deb | ||||
|  | ||||
|   upload-assets: | ||||
|     name: Upload release assets | ||||
|     needs: ["extract-tag", "build"] | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Download all workflow run artifacts | ||||
|         uses: actions/download-artifact@v4 | ||||
|         with: | ||||
|           merge-multiple: true | ||||
|           path: . | ||||
|  | ||||
|       - name: Upload .deb package file | ||||
|         run: | | ||||
|           gh release upload ${{ needs.extract-tag.outputs.tag }} opustags-${{ needs.extract-tag.outputs.version }}-amd64.deb | ||||
|         env: | ||||
|           GH_TOKEN: ${{ github.token }} | ||||
|           GH_REPO: ${{ github.repository }} | ||||
							
								
								
									
										19
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,6 +1,25 @@ | ||||
| opustags changelog | ||||
| ================== | ||||
|  | ||||
| 1.10.1 - 2024-05-19 | ||||
| ------------------- | ||||
|  | ||||
| Fix a build error on recent systems. | ||||
|  | ||||
| 1.10.0 - 2024-05-03 | ||||
| ------------------- | ||||
|  | ||||
| - Introduce -z to delimit tags with null bytes. | ||||
|  | ||||
| This option makes it possible to leverage GNU sed or GNU grep for automated tag edition with | ||||
| `opustags -z … | sed -z … | opustags -z -S …`, while also supporting multi-line tags. | ||||
|  | ||||
| 1.9.0 - 2023-06-07 | ||||
| ------------------ | ||||
|  | ||||
| - Introduce --vendor and --set-vendor. | ||||
| - Close the input file before finalizing the output, in order to fix --in-place on SMB drives. | ||||
|  | ||||
| 1.8.0 - 2023-03-07 | ||||
| ------------------ | ||||
|  | ||||
|   | ||||
| @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.11) | ||||
|  | ||||
| project( | ||||
| 	opustags | ||||
| 	VERSION 1.8.0 | ||||
| 	VERSION 1.10.1 | ||||
| 	LANGUAGES CXX | ||||
| ) | ||||
|  | ||||
| @@ -51,5 +51,6 @@ 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") | ||||
| install(FILES CHANGELOG.md CONTRIBUTING.md LICENSE README.md DESTINATION ${CMAKE_INSTALL_DOCDIR}) | ||||
|  | ||||
| add_subdirectory(t) | ||||
|   | ||||
| @@ -25,6 +25,9 @@ You should check that your changes don't break the test suite by running | ||||
| Following these practices is important to keep the history clean, and to allow | ||||
| for better code reviews. | ||||
|  | ||||
| You can submit pull requests on GitHub at <https://github.com/fmang/opustags>, | ||||
| or email me your patches at <fmang+opustags@mg0.fr>. | ||||
|  | ||||
| ## History of opustags | ||||
|  | ||||
| opustags is originally a small project made to fill a need to edit tags in Opus | ||||
| @@ -49,6 +52,8 @@ modules, and reviewed for safety. | ||||
| 1.3.0 was focused on correctness, and detects edge cases as early as possible, | ||||
| instead of hoping something will eventually fail if something is weird. | ||||
|  | ||||
| Subsequent releases have been adding new features. | ||||
|  | ||||
| ## Candidate features | ||||
|  | ||||
| The code contains a few `\todo` markers where something could be improved in the | ||||
| @@ -59,13 +64,7 @@ More generally, here are a few features that could be added in the future: | ||||
| - Discouraging non-ASCII field names. | ||||
| - Logicial stream listing and selection for multiplexed files. | ||||
| - Escaping control characters with --escape. | ||||
| - Dump binary packets with --binary. | ||||
| - Edition of the vendor string. | ||||
| - Edition of the arbitrary binary block past the comments. | ||||
| - Support for OpusTags packets spanning multiple pages (> 64 kB). | ||||
| - Interactive edition of comments inside the EDITOR (--edit). | ||||
| - Support for cover arts. | ||||
| - Load tags from a file with --set-all=tags.txt. | ||||
| - Colored output. | ||||
|  | ||||
| Don't hesitate to contact me before you do anything, I'll give you directions. | ||||
|   | ||||
							
								
								
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							| @@ -1,4 +1,4 @@ | ||||
| Copyright (c) 2013-2018, Frédéric Mangano-Tarumi | ||||
| Copyright (c) 2013-2024, Frédéric Mangano | ||||
| All rights reserved. | ||||
|  | ||||
| Redistribution and use in source and binary forms, with or without modification, | ||||
|   | ||||
| @@ -17,6 +17,8 @@ ever be performed. | ||||
| opustags is tag-agnostic: you can write arbitrary key-value tags, and none of them will be treated | ||||
| specially. After all, common tags like TITLE or ARTIST are nothing more than conventions. | ||||
|  | ||||
| The project’s homepage is located at <https://github.com/fmang/opustags>. | ||||
|  | ||||
| Requirements | ||||
| ------------ | ||||
|  | ||||
| @@ -64,6 +66,9 @@ Documentation | ||||
|       -e, --edit                    edit tags interactively in VISUAL/EDITOR | ||||
|       --output-cover FILE           extract and save the cover art, if any | ||||
|       --set-cover FILE              sets the cover art | ||||
|       --vendor                      print the vendor string | ||||
|       --set-vendor VALUE            set the vendor string | ||||
|       --raw                         disable encoding conversion | ||||
|       -z                            delimit tags with NUL | ||||
|  | ||||
| See the man page, `opustags.1`, for extensive documentation. | ||||
|   | ||||
							
								
								
									
										73
									
								
								opustags.1
									
									
									
									
									
								
							
							
						
						
									
										73
									
								
								opustags.1
									
									
									
									
									
								
							| @@ -1,4 +1,4 @@ | ||||
| .TH opustags 1 "March 2023" "@PROJECT_NAME@ @PROJECT_VERSION@" | ||||
| .TH opustags 1 "March 2025" "@PROJECT_NAME@ @PROJECT_VERSION@" | ||||
| .SH NAME | ||||
| opustags \- Ogg Opus tag editor | ||||
| .SH SYNOPSIS | ||||
| @@ -11,7 +11,7 @@ opustags \- Ogg Opus tag editor | ||||
| .B opustags | ||||
| .I OPTIONS | ||||
| .B -i | ||||
| .R \fIFILE\fP... | ||||
| \fIFILE\fP... | ||||
| .br | ||||
| .B opustags | ||||
| .I OPTIONS | ||||
| @@ -43,13 +43,13 @@ to set new tags without being bothered by the old ones. | ||||
| 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. | ||||
| All the previously existing tags are deleted. | ||||
| .PP | ||||
| The Opus format specifications requires that tags are encoded in UTF-8, so that's the only encoding | ||||
| opustags supports. If your system encoding is different, the tags are automatically converted to and | ||||
| from your system locale. When you edit an Opus file whose tags contains characters unsupported by | ||||
| your system encoding, the original UTF-8 values will be preserved for the tags you don't explictly | ||||
| modify. | ||||
| The Opus format specification requires that tags are encoded in UTF-8, so that’s the only encoding | ||||
| \fBopustags\fP supports. If your system encoding is different, the tags are automatically converted | ||||
| to and from your system locale. When you edit an Opus file whose tags contain characters unsupported | ||||
| by your system encoding, the original UTF-8 values will be preserved for the tags you don’t | ||||
| explicitly modify. | ||||
| .SH OPTIONS | ||||
| .TP | ||||
| .B \-h, \-\-help | ||||
| @@ -67,7 +67,7 @@ setting \fB--output\fP to the same path as the input file and enabling \fB--over | ||||
| This option conflicts with \fB--output\fP. | ||||
| .TP | ||||
| .B \-y, \-\-overwrite | ||||
| By default, \fBopustags\fP refuses to overwrite an already-existent file. | ||||
| By default, \fBopustags\fP refuses to overwrite an already-existing file. | ||||
| Use \fB-y\fP to allow overwriting. | ||||
| Note that this option is not needed when the output is a special file like \fI/dev/null\fP. | ||||
| .TP | ||||
| @@ -79,10 +79,10 @@ In both cases, the field names are case-insensitive, and expected to be ASCII. | ||||
| .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. | ||||
| When \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 | ||||
| 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 tags of the same | ||||
| @@ -93,33 +93,43 @@ added with \fB--add\fP. | ||||
| Delete all the previously existing tags. | ||||
| .TP | ||||
| .B \-S, \-\-set-all | ||||
| Sets the tags from scratch. | ||||
| Set 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. | ||||
| Empty lines and lines starting with \fI#\fP are ignored. | ||||
| Multiline tags must have their continuation lines prefixed by a single tab (in other words, every | ||||
| Multi-line tags must have their continuation lines prefixed by a single tab (in other words, every | ||||
| \fI\\n\fP must be replaced by \fI\\n\\t\fP). | ||||
| .TP | ||||
| .B \-e, \-\-edit | ||||
| Edit tags interactively by spawning the program specified by the EDITOR | ||||
| environment variable. The allowed format is the same as \fB--set-all\fP. | ||||
| environment variable. The allowed format is the same as with \fB--set-all\fP. | ||||
| If TERM and VISUAL are set, VISUAL takes precedence over EDITOR. | ||||
| .TP | ||||
| .B \-\-output-cover \fIFILE\fP | ||||
| Save the cover art of the input Opus file to the specified location. | ||||
| Extract the cover art from the \fBMETADATA_BLOCK_PICTURE\fP tag into the specified location. | ||||
| If the input file does not contain any cover art, this option has no effect. | ||||
| To allow overwriting the target location, specify \fB--overwrite\fP. | ||||
| In the case of multiple pictures embedded in the Opus tags, only the first one is saved. | ||||
| Note that the since the image format is not fixed, you should consider an extension-less file name | ||||
| and rely on the magic number to deduce the type. opustags does not add or check the target file’s | ||||
| extension. | ||||
| Note that since the image format is not fixed, you should consider an extension-less file name and | ||||
| rely on the magic number to deduce the type. | ||||
| \fBopustags\fP does not add or check the target file’s extension. | ||||
| You can specify \fB-\fP for standard output, in which case the regular output will be suppressed. | ||||
| .TP | ||||
| .B \-\-set-cover \fIFILE\fP | ||||
| Replace or set the cover art to the specified picture. | ||||
| Set the cover art by embedding the specified picture into the \fBMETADATA_BLOCK_PICTURE\fP tag, | ||||
| replacing any existing values. | ||||
| Specify \fB-\fP to read the picture from standard input. | ||||
| In theory, an Opus file may contain multiple pictures with different roles, though in practice only | ||||
| the front cover really matters. opustags can currently only handle one front cover and nothing else. | ||||
| the front cover really matters. | ||||
| \fBopustags\fP can currently only handle one front cover and nothing else. | ||||
| .TP | ||||
| .B \-\-vendor | ||||
| Print the vendor string from the OpusTags packet and do nothing else. Standard tags operations are | ||||
| not supported when specifying this flag. | ||||
| .TP | ||||
| .B \-\-set-vendor \fIVALUE\fP | ||||
| Replace the vendor string by the specified value. This action can be performed alongside tag | ||||
| edition. | ||||
| .TP | ||||
| .B \-\-raw | ||||
| OpusTags metadata should always be encoded in UTF-8, as per RFC 7845. However, some files may be | ||||
| @@ -127,6 +137,15 @@ corrupted or possibly even contain intentional binary data. In that case, --raw | ||||
| kind of binary data without ensuring the validity of the tags encoding. This option may also be | ||||
| useful when your system encoding is different from UTF-8 and you wish to preserve the full UTF-8 | ||||
| character set even though your system cannot display it. | ||||
| .TP | ||||
| .B \-z | ||||
| When editing tags programmatically with line-based tools like grep or sed, tags containing newlines | ||||
| are likely to corrupt the result because these tools won’t interpret multi-line tags as a whole. To | ||||
| make automatic processing easier, \fB-z\fP delimits tags by a null byte (ASCII NUL) instead of line | ||||
| feeds. That same \fB-z\fP flag is also supported by GNU grep or GNU sed and, combined with | ||||
| \fBopustags -z\fP, would make them process the input tag-by-tag instead of line-by-line, thus | ||||
| supporting multi-line tags as well. | ||||
| This option also disables the tab prefix for continuation lines after a line feed. | ||||
| .SH EXAMPLES | ||||
| .PP | ||||
| List all the tags in file foo.opus: | ||||
| @@ -137,10 +156,6 @@ 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 | ||||
| @@ -149,10 +164,18 @@ tags without writing them to the Opus file: | ||||
| Edit tags interactively in Vim: | ||||
| .PP | ||||
| 	EDITOR=vim opustags --in-place --edit file.opus | ||||
| .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 | ||||
| Use GNU grep to remove all the CHAPTER* tags, with -z to support multi-line tags: | ||||
| .PP | ||||
| 	opustags -z file.opus | grep -z -v ^CHAPTER | opustags -z --in-place file.opus --set-all | ||||
| .SH CAVEATS | ||||
| .PP | ||||
| \fBopustags\fP currently has the following limitations: | ||||
| .IP \[bu] | ||||
| .IP \[bu] 2n | ||||
| Multiplexed streams are not supported. | ||||
| .IP \[bu] | ||||
| Control characters inside tags are printed raw rather than being escaped. | ||||
|   | ||||
| @@ -27,7 +27,7 @@ std::u8string ot::encode_base64(ot::byte_string_view src) | ||||
| 	std::u8string out; | ||||
| 	out.resize(olen); | ||||
|  | ||||
| 	const uint8_t* in = src.data(); | ||||
| 	const uint8_t* in = reinterpret_cast<const uint8_t*>(src.data()); | ||||
| 	const uint8_t* end = in + len; | ||||
| 	char8_t* pos = out.data(); | ||||
| 	while (end - in >= 3) { | ||||
| @@ -56,7 +56,7 @@ std::u8string ot::encode_base64(ot::byte_string_view src) | ||||
| ot::byte_string ot::decode_base64(std::u8string_view src) | ||||
| { | ||||
| 	// Remove the padding and rely on the string length instead. | ||||
| 	while (src.back() == u8'=') | ||||
| 	while (!src.empty() && src.back() == u8'=') | ||||
| 		src.remove_suffix(1); | ||||
|  | ||||
| 	size_t olen = src.size() / 4 * 3; // Whole blocks; | ||||
| @@ -68,7 +68,7 @@ ot::byte_string ot::decode_base64(std::u8string_view src) | ||||
|  | ||||
| 	ot::byte_string out; | ||||
| 	out.resize(olen); | ||||
| 	uint8_t* pos = out.data(); | ||||
| 	uint8_t* pos = reinterpret_cast<uint8_t*>(out.data()); | ||||
|  | ||||
| 	unsigned char dtable[256]; | ||||
| 	memset(dtable, 0x80, 256); | ||||
| @@ -84,11 +84,11 @@ ot::byte_string ot::decode_base64(std::u8string_view src) | ||||
|  | ||||
| 		block[count++] = tmp; | ||||
| 		if (count == 2) { | ||||
| 			*pos++ = (block[0] << 2) | (block[1] >> 4); | ||||
| 			*pos++ = 0xFF & (block[0] << 2) | (block[1] >> 4); | ||||
| 		} else if (count == 3) { | ||||
| 			*pos++ = (block[1] << 4) | (block[2] >> 2); | ||||
| 			*pos++ = 0xFF & (block[1] << 4) | (block[2] >> 2); | ||||
| 		} else if (count == 4) { | ||||
| 			*pos++ = (block[2] << 6) | block[3]; | ||||
| 			*pos++ = 0xFF & (block[2] << 6) | block[3]; | ||||
| 			count = 0; | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
							
								
								
									
										115
									
								
								src/cli.cc
									
									
									
									
									
								
							
							
						
						
									
										115
									
								
								src/cli.cc
									
									
									
									
									
								
							| @@ -15,6 +15,7 @@ | ||||
| #include <string.h> | ||||
| #include <sys/stat.h> | ||||
| #include <unistd.h> | ||||
| #include <algorithm> | ||||
|  | ||||
| static const char help_message[] = | ||||
| PROJECT_NAME " version " PROJECT_VERSION | ||||
| @@ -38,7 +39,10 @@ Options: | ||||
|   -e, --edit                    edit tags interactively in VISUAL/EDITOR | ||||
|   --output-cover FILE           extract and save the cover art, if any | ||||
|   --set-cover FILE              sets the cover art | ||||
|   --vendor                      print the vendor string | ||||
|   --set-vendor VALUE            set the vendor string | ||||
|   --raw                         disable encoding conversion | ||||
|   -z                            delimit tags with NUL | ||||
|  | ||||
| See the man page for extensive documentation. | ||||
| )raw"; | ||||
| @@ -56,6 +60,8 @@ static struct option getopt_options[] = { | ||||
| 	{"edit", no_argument, 0, 'e'}, | ||||
| 	{"output-cover", required_argument, 0, 'c'}, | ||||
| 	{"set-cover", required_argument, 0, 'C'}, | ||||
| 	{"vendor", no_argument, 0, 'v'}, | ||||
| 	{"set-vendor", required_argument, 0, 'V'}, | ||||
| 	{"raw", no_argument, 0, 'r'}, | ||||
| 	{NULL, 0, 0, 0} | ||||
| }; | ||||
| @@ -69,12 +75,13 @@ ot::options ot::parse_options(int argc, char** argv, FILE* comments_input) | ||||
| 	std::list<std::string> local_to_delete; // opt.to_delete before UTF-8 conversion. | ||||
| 	bool set_all = false; | ||||
| 	std::optional<std::string> set_cover; | ||||
| 	std::optional<std::string> set_vendor; | ||||
| 	opt = {}; | ||||
| 	if (argc == 1) | ||||
| 		throw status {st::bad_arguments, "No arguments specified. Use -h for help."}; | ||||
| 	int c; | ||||
| 	optind = 0; | ||||
| 	while ((c = getopt_long(argc, argv, ":ho:iyd:a:s:DSe", getopt_options, NULL)) != -1) { | ||||
| 	while ((c = getopt_long(argc, argv, ":ho:iyd:a:s:DSez", getopt_options, NULL)) != -1) { | ||||
| 		switch (c) { | ||||
| 		case 'h': | ||||
| 			opt.print_help = true; | ||||
| @@ -123,9 +130,20 @@ ot::options ot::parse_options(int argc, char** argv, FILE* comments_input) | ||||
| 				throw status {st::bad_arguments, "Cannot specify --set-cover more than once."}; | ||||
| 			set_cover = optarg; | ||||
| 			break; | ||||
| 		case 'v': | ||||
| 			opt.print_vendor = true; | ||||
| 			break; | ||||
| 		case 'V': | ||||
| 			if (set_vendor) | ||||
| 				throw status {st::bad_arguments, "Cannot specify --set-vendor more than once."}; | ||||
| 			set_vendor = optarg; | ||||
| 			break; | ||||
| 		case 'r': | ||||
| 			opt.raw = true; | ||||
| 			break; | ||||
| 		case 'z': | ||||
| 			opt.tag_delimiter = '\0'; | ||||
| 			break; | ||||
| 		case ':': | ||||
| 			throw status {st::bad_arguments, "Missing value for option '"s + argv[optind - 1] + "'."}; | ||||
| 		default: | ||||
| @@ -161,17 +179,23 @@ ot::options ot::parse_options(int argc, char** argv, FILE* comments_input) | ||||
| 			       std::back_inserter(opt.to_add), cast_to_utf8); | ||||
| 		std::transform(local_to_delete.begin(), local_to_delete.end(), | ||||
| 			       std::back_inserter(opt.to_delete), cast_to_utf8); | ||||
| 		if (set_vendor) | ||||
| 			opt.set_vendor = cast_to_utf8(*set_vendor); | ||||
| 	} else { | ||||
| 		try { | ||||
| 			std::transform(local_to_add.begin(), local_to_add.end(), | ||||
| 			               std::back_inserter(opt.to_add), encode_utf8); | ||||
| 			std::transform(local_to_delete.begin(), local_to_delete.end(), | ||||
| 			               std::back_inserter(opt.to_delete), encode_utf8); | ||||
| 			if (set_vendor) | ||||
| 				opt.set_vendor = encode_utf8(*set_vendor); | ||||
| 		} catch (const ot::status& rc) { | ||||
| 			throw status {st::bad_arguments, "Could not encode argument into UTF-8: " + rc.message}; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	bool read_only = !opt.in_place && !opt.path_out.has_value(); | ||||
|  | ||||
| 	if (opt.in_place && opt.path_out) | ||||
| 		throw status {st::bad_arguments, "Cannot combine --in-place and --output."}; | ||||
|  | ||||
| @@ -184,7 +208,7 @@ ot::options ot::parse_options(int argc, char** argv, FILE* comments_input) | ||||
| 	if (opt.edit_interactively && (stdin_as_input || opt.path_out == "-" || opt.cover_out == "-")) | ||||
| 		throw status {st::bad_arguments, "Cannot edit interactively when standard input or standard output are already used."}; | ||||
|  | ||||
| 	if (opt.edit_interactively && !opt.path_out.has_value() && !opt.in_place) | ||||
| 	if (opt.edit_interactively && read_only) | ||||
| 		throw status {st::bad_arguments, "Cannot edit interactively when no output is specified."}; | ||||
|  | ||||
| 	if (opt.edit_interactively && (opt.delete_all || !opt.to_add.empty() || !opt.to_delete.empty())) | ||||
| @@ -196,6 +220,9 @@ ot::options ot::parse_options(int argc, char** argv, FILE* comments_input) | ||||
| 	if (opt.cover_out && opt.paths_in.size() > 1) | ||||
| 		throw status {st::bad_arguments, "Cannot use --output-cover with multiple input files."}; | ||||
|  | ||||
| 	if (opt.print_vendor && !read_only) | ||||
| 		throw status {st::bad_arguments, "--vendor is only supported in read-only mode."}; | ||||
|  | ||||
| 	if (set_cover) { | ||||
| 		byte_string picture_data = ot::slurp_binary_file(set_cover->c_str()); | ||||
| 		opt.to_delete.push_back(u8"METADATA_BLOCK_PICTURE"s); | ||||
| @@ -204,17 +231,17 @@ ot::options ot::parse_options(int argc, char** argv, FILE* comments_input) | ||||
|  | ||||
| 	if (set_all) { | ||||
| 		// Read comments from stdin and prepend them to opt.to_add. | ||||
| 		std::list<std::u8string> comments = read_comments(comments_input, opt.raw); | ||||
| 		std::list<std::u8string> comments = read_comments(comments_input, opt); | ||||
| 		opt.to_add.splice(opt.to_add.begin(), std::move(comments)); | ||||
| 	} | ||||
| 	return opt; | ||||
| } | ||||
|  | ||||
| /** Format a UTF-8 string by adding tabulations (\t) after line feeds (\n) to mark continuation for | ||||
|  *  multiline values. */ | ||||
| static std::u8string format_value(const std::u8string& source) | ||||
|  *  multiline values. With -z, this behavior applies for embedded NUL characters instead of LF. */ | ||||
| static std::u8string format_value(const std::u8string& source, const ot::options& opt) | ||||
| { | ||||
| 	auto newline_count = std::count(source.begin(), source.end(), u8'\n'); | ||||
| 	auto newline_count = std::count(source.begin(), source.end(), opt.tag_delimiter); | ||||
|  | ||||
| 	// General case: the value fits on a single line. Use std::string’s copy constructor for the | ||||
| 	// most efficient copy we could hope for. | ||||
| @@ -225,19 +252,39 @@ static std::u8string format_value(const std::u8string& source) | ||||
| 	formatted.reserve(source.size() + newline_count); | ||||
| 	for (auto c : source) { | ||||
| 		formatted.push_back(c); | ||||
| 		if (c == '\n') | ||||
| 		if (c == opt.tag_delimiter) | ||||
| 			formatted.push_back(u8'\t'); | ||||
| 	} | ||||
| 	return formatted; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Convert the comment from UTF-8 to the system encoding if relevant, and print it with a trailing | ||||
|  * line feed. | ||||
|  */ | ||||
| static void puts_utf8(std::u8string_view str, FILE* output, const ot::options& opt) | ||||
| { | ||||
| 	if (opt.raw) { | ||||
| 		fwrite(str.data(), 1, str.size(), output); | ||||
| 	} else { | ||||
| 		try { | ||||
| 			std::string local = ot::decode_utf8(str); | ||||
| 			fwrite(local.data(), 1, local.size(), output); | ||||
| 		} catch (ot::status& rc) { | ||||
| 			rc.message += " See --raw."; | ||||
| 			throw; | ||||
| 		} | ||||
| 	} | ||||
| 	putc(opt.tag_delimiter, output); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Print comments in a human readable format that can also be read back in by #read_comment. | ||||
|  * | ||||
|  * To disambiguate between a newline embedded in a comment and a newline representing the start of | ||||
|  * the next tag, continuation lines always have a single TAB (^I) character added to the beginning. | ||||
|  */ | ||||
| void ot::print_comments(const std::list<std::u8string>& comments, FILE* output, bool raw) | ||||
| void ot::print_comments(const std::list<std::u8string>& comments, FILE* output, const ot::options& opt) | ||||
| { | ||||
| 	bool has_control = false; | ||||
| 	for (const std::u8string& source_comment : comments) { | ||||
| @@ -249,27 +296,14 @@ void ot::print_comments(const std::list<std::u8string>& comments, FILE* output, | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		std::u8string utf8_comment = format_value(source_comment); | ||||
| 		// Convert the comment from UTF-8 to the system encoding if relevant. | ||||
| 		if (raw) { | ||||
| 			fwrite(utf8_comment.data(), 1, utf8_comment.size(), output); | ||||
| 		} else { | ||||
| 			try { | ||||
| 				std::string local = decode_utf8(utf8_comment); | ||||
| 				fwrite(local.data(), 1, local.size(), output); | ||||
| 			} catch (ot::status& rc) { | ||||
| 				rc.message += " See --raw."; | ||||
| 				throw; | ||||
| 			} | ||||
| 		} | ||||
| 		putc('\n', output); | ||||
| 		std::u8string utf8_comment = format_value(source_comment, opt); | ||||
| 		puts_utf8(utf8_comment, output, opt); | ||||
| 	} | ||||
| 	if (has_control) | ||||
| 		fputs("warning: Some tags contain control characters.\n", stderr); | ||||
| } | ||||
|  | ||||
| std::list<std::u8string> ot::read_comments(FILE* input, bool raw) | ||||
| std::list<std::u8string> ot::read_comments(FILE* input, const ot::options& opt) | ||||
| { | ||||
| 	std::list<std::u8string> comments; | ||||
| 	comments.clear(); | ||||
| @@ -277,12 +311,12 @@ std::list<std::u8string> ot::read_comments(FILE* input, bool raw) | ||||
| 	size_t buflen = 0; | ||||
| 	ssize_t nread; | ||||
| 	std::u8string* previous_comment = nullptr; | ||||
| 	while ((nread = getline(&source_line, &buflen, input)) != -1) { | ||||
| 		if (nread > 0 && source_line[nread - 1] == '\n') | ||||
| 	while ((nread = getdelim(&source_line, &buflen, opt.tag_delimiter, input)) != -1) { | ||||
| 		if (nread > 0 && source_line[nread - 1] == opt.tag_delimiter) | ||||
| 			--nread; // Chomp. | ||||
|  | ||||
| 		std::u8string line; | ||||
| 		if (raw) { | ||||
| 		if (opt.raw) { | ||||
| 			line = std::u8string(reinterpret_cast<char8_t*>(source_line), nread); | ||||
| 		} else { | ||||
| 			try { | ||||
| @@ -306,7 +340,7 @@ std::list<std::u8string> ot::read_comments(FILE* input, bool raw) | ||||
| 				free(source_line); | ||||
| 				throw rc; | ||||
| 			} else { | ||||
| 				line[0] = '\n'; | ||||
| 				line[0] = opt.tag_delimiter; | ||||
| 				previous_comment->append(line); | ||||
| 			} | ||||
| 		} else if (line.find(u8'=') == decltype(line)::npos) { | ||||
| @@ -348,6 +382,9 @@ void ot::delete_comments(std::list<std::u8string>& comments, const std::u8string | ||||
| /** Apply the modifications requested by the user to the opustags packet. */ | ||||
| static void edit_tags(ot::opus_tags& tags, const ot::options& opt) | ||||
| { | ||||
| 	if (opt.set_vendor) | ||||
| 		tags.vendor = *opt.set_vendor; | ||||
|  | ||||
| 	if (opt.delete_all) { | ||||
| 		tags.comments.clear(); | ||||
| 	} else for (const std::u8string& name : opt.to_delete) { | ||||
| @@ -359,7 +396,7 @@ static void edit_tags(ot::opus_tags& tags, const ot::options& opt) | ||||
| } | ||||
|  | ||||
| /** Spawn VISUAL or EDITOR to edit the given tags. */ | ||||
| static void edit_tags_interactively(ot::opus_tags& tags, const std::optional<std::string>& base_path, bool raw) | ||||
| static void edit_tags_interactively(ot::opus_tags& tags, const std::optional<std::string>& base_path, const ot::options& opt) | ||||
| { | ||||
| 	const char* editor = nullptr; | ||||
| 	if (getenv("TERM") != nullptr) | ||||
| @@ -378,7 +415,7 @@ static void edit_tags_interactively(ot::opus_tags& tags, const std::optional<std | ||||
| 	if (fd == -1 || (tags_file = fdopen(fd, "w")) == nullptr) | ||||
| 		throw ot::status {ot::st::standard_error, | ||||
| 		                  "Could not open '" + tags_path + "': " + strerror(errno)}; | ||||
| 	ot::print_comments(tags.comments, tags_file.get(), raw); | ||||
| 	ot::print_comments(tags.comments, tags_file.get(), opt); | ||||
| 	tags_file.reset(); | ||||
|  | ||||
| 	// Spawn the editor, and watch the modification timestamps. | ||||
| @@ -409,7 +446,7 @@ static void edit_tags_interactively(ot::opus_tags& tags, const std::optional<std | ||||
| 	if (tags_file == nullptr) | ||||
| 		throw ot::status {ot::st::standard_error, "Error opening " + tags_path + ": " + strerror(errno)}; | ||||
| 	try { | ||||
| 		tags.comments = ot::read_comments(tags_file.get(), raw); | ||||
| 		tags.comments = ot::read_comments(tags_file.get(), opt); | ||||
| 	} catch (const ot::status& rc) { | ||||
| 		fprintf(stderr, "warning: Leaving %s on the disk.\n", tags_path.c_str()); | ||||
| 		throw; | ||||
| @@ -466,7 +503,7 @@ static void process(ot::ogg_reader& reader, ot::ogg_writer* writer, const ot::op | ||||
| 	 *  output stream, we need to renumber all the succeeding pages. If the input stream | ||||
| 	 *  contains gaps, the offset will naively reproduce the gaps: page numbers 0 (1) 2 4 will | ||||
| 	 *  become 0 (1 2) 3 5, where (…) is the OpusTags packet, and not 0 (1 2) 3 4. */ | ||||
| 	int pageno_offset = 0; | ||||
| 	long pageno_offset = 0; | ||||
|  | ||||
| 	while (reader.next_page()) { | ||||
| 		auto serialno = ogg_page_serialno(&reader.page); | ||||
| @@ -492,14 +529,18 @@ static void process(ot::ogg_reader& reader, ot::ogg_writer* writer, const ot::op | ||||
| 			if (writer) { | ||||
| 				if (opt.edit_interactively) { | ||||
| 					fflush(writer->file); // flush before calling the subprocess | ||||
| 					edit_tags_interactively(tags, writer->path, opt.raw); | ||||
| 					edit_tags_interactively(tags, writer->path, opt); | ||||
| 				} | ||||
| 				auto packet = ot::render_tags(tags); | ||||
| 				writer->write_header_packet(serialno, pageno, packet); | ||||
| 				pageno_offset = writer->next_page_no - 1 - reader.absolute_page_no; | ||||
| 			} else { | ||||
| 				if (opt.cover_out != "-") | ||||
| 					ot::print_comments(tags.comments, stdout, opt.raw); | ||||
| 				if (opt.cover_out != "-") { | ||||
| 					if (opt.print_vendor) | ||||
| 						puts_utf8(tags.vendor, stdout, opt); | ||||
| 					else | ||||
| 						ot::print_comments(tags.comments, stdout, opt); | ||||
| 				} | ||||
| 				break; | ||||
| 			} | ||||
| 		} else if (writer) { | ||||
| @@ -577,6 +618,10 @@ static void run_single(const ot::options& opt, const std::string& path_in, const | ||||
| 	ot::ogg_writer writer(output); | ||||
| 	writer.path = path_out; | ||||
| 	process(reader, &writer, opt); | ||||
|  | ||||
| 	// Close the input file and finalize the output. When --in-place is specified, some file | ||||
| 	// systems like SMB require that the input is closed first. | ||||
| 	input.reset(); | ||||
| 	temporary_output.commit(); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -28,8 +28,8 @@ bool ot::ogg_reader::next_page() | ||||
| 	while ((rc = ogg_sync_pageout(&sync, &page)) != 1) { | ||||
| 		if (rc == -1) { | ||||
| 			throw status {st::bad_stream, | ||||
| 			              absolute_page_no == (size_t) -1 ? "Input is not a valid Ogg file." | ||||
| 			                                              : "Unsynced data in stream."}; | ||||
| 			              absolute_page_no == -1 ? "Input is not a valid Ogg file." | ||||
| 			                                     : "Unsynced data in stream."}; | ||||
| 		} | ||||
| 		if (ogg_sync_check(&sync) != 0) | ||||
| 			throw status {st::libogg_error, "ogg_sync_check signalled an error."}; | ||||
|   | ||||
							
								
								
									
										32
									
								
								src/opus.cc
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								src/opus.cc
									
									
									
									
									
								
							| @@ -3,7 +3,7 @@ | ||||
|  * \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). | ||||
|  * with here is defined by [RFC 7845](https://tools.ietf.org/html/rfc7845.html). | ||||
|  * | ||||
|  * Section 3 "Packet Organization" is critical for us: | ||||
|  * | ||||
| @@ -24,6 +24,7 @@ | ||||
| #include <opustags.h> | ||||
|  | ||||
| #include <string.h> | ||||
| #include <algorithm> | ||||
|  | ||||
| ot::opus_tags ot::parse_tags(const ogg_packet& packet) | ||||
| { | ||||
| @@ -54,7 +55,9 @@ ot::opus_tags ot::parse_tags(const ogg_packet& packet) | ||||
| 	// Comment count | ||||
| 	if (pos + 4 > size) | ||||
| 		throw status {st::cut_comment_count, "Comment count did not fit the comment header"}; | ||||
| 	uint32_t count = le32toh(*((uint32_t*) (data + pos))); | ||||
| 	uint32_t count; | ||||
| 	memcpy(&count, data + pos, sizeof(count)); | ||||
| 	count = le32toh(count); | ||||
| 	pos += 4; | ||||
|  | ||||
| 	// Comments' data | ||||
| @@ -62,7 +65,9 @@ ot::opus_tags ot::parse_tags(const ogg_packet& packet) | ||||
| 		if (pos + 4 > size) | ||||
| 			throw status {st::cut_comment_length, | ||||
| 			              "Comment length did not fit the comment header"}; | ||||
| 		uint32_t comment_length = le32toh(*((uint32_t*) (data + pos))); | ||||
| 		uint32_t comment_length; | ||||
| 		memcpy(&comment_length, data + pos, sizeof(comment_length)); | ||||
| 		comment_length = le32toh(comment_length); | ||||
| 		if (pos + 4 + comment_length > size) | ||||
| 			throw status {st::cut_comment_data, | ||||
| 			              "Comment string did not fit the comment header"}; | ||||
| @@ -72,7 +77,7 @@ ot::opus_tags ot::parse_tags(const ogg_packet& packet) | ||||
| 	} | ||||
|  | ||||
| 	// Extra data | ||||
| 	my_tags.extra_data = byte_string(data + pos, size - pos); | ||||
| 	my_tags.extra_data = byte_string(reinterpret_cast<const char*>(data + pos), size - pos); | ||||
|  | ||||
| 	return my_tags; | ||||
| } | ||||
| @@ -133,12 +138,16 @@ ot::picture::picture(ot::byte_string block) | ||||
| 	size_t desc_offset = mime_offset + 4 + mime_size; | ||||
| 	if (storage.size() < desc_offset + 4) | ||||
| 		throw status { st::invalid_size, "missing description in picture block" }; | ||||
| 	uint32_t desc_size = be32toh(*reinterpret_cast<const uint32_t*>(&storage[desc_offset])); | ||||
| 	uint32_t desc_size; | ||||
| 	memcpy(&desc_size, &storage[desc_offset], sizeof(desc_size)); | ||||
| 	desc_size = be32toh(desc_size); | ||||
|  | ||||
| 	size_t pic_offset = desc_offset + 4 + desc_size + 16; | ||||
| 	if (storage.size() < pic_offset + 4) | ||||
| 		throw status { st::invalid_size, "missing picture data in picture block" }; | ||||
| 	uint32_t pic_size = be32toh(*reinterpret_cast<const uint32_t*>(&storage[pic_offset])); | ||||
| 	uint32_t pic_size; | ||||
| 	memcpy(&pic_size, &storage[pic_offset], sizeof(pic_size)); | ||||
| 	pic_size = be32toh(pic_size); | ||||
|  | ||||
| 	if (storage.size() != pic_offset + 4 + pic_size) | ||||
| 		throw status { st::invalid_size, "invalid picture block size" }; | ||||
| @@ -156,7 +165,8 @@ ot::byte_string ot::picture::serialize() const | ||||
| 	*reinterpret_cast<uint32_t*>(&bytes[0]) = htobe32(3); // Picture type: front cover. | ||||
| 	*reinterpret_cast<uint32_t*>(&bytes[mime_offset]) = htobe32(mime_type.size()); | ||||
| 	std::copy(mime_type.begin(), mime_type.end(), std::next(bytes.begin(), mime_offset + 4)); | ||||
| 	*reinterpret_cast<uint32_t*>(&bytes[pic_offset]) = htobe32(picture_data.size()); | ||||
| 	uint32_t picture_data_size = htobe32(picture_data.size()); | ||||
| 	memcpy(&bytes[pic_offset], &picture_data_size, sizeof(picture_data_size)); | ||||
| 	std::copy(picture_data.begin(), picture_data.end(), std::next(bytes.begin(), pic_offset + 4)); | ||||
| 	return bytes; | ||||
| } | ||||
| @@ -190,16 +200,16 @@ std::optional<ot::picture> ot::extract_cover(const ot::opus_tags& tags) | ||||
| static ot::byte_string_view detect_mime_type(ot::byte_string_view data) | ||||
| { | ||||
| 	static std::initializer_list<std::pair<ot::byte_string_view, ot::byte_string_view>> magic_numbers = { | ||||
| 		{ "\xff\xd8\xff"_bsv, "image/jpeg"_bsv }, | ||||
| 		{ "\x89PNG"_bsv, "image/png"_bsv }, | ||||
| 		{ "GIF8"_bsv, "image/gif"_bsv }, | ||||
| 		{ "\xff\xd8\xff"sv, "image/jpeg"sv }, | ||||
| 		{ "\x89PNG"sv, "image/png"sv }, | ||||
| 		{ "GIF8"sv, "image/gif"sv }, | ||||
| 	}; | ||||
| 	for (auto [magic, mime] : magic_numbers) { | ||||
| 		if (data.starts_with(magic)) | ||||
| 			return mime; | ||||
| 	} | ||||
| 	fputs("warning: Could not identify the MIME type of the picture; defaulting to application/octet-stream.\n", stderr); | ||||
| 	return "application/octet-stream"_bsv; | ||||
| 	return "application/octet-stream"sv; | ||||
| } | ||||
|  | ||||
| std::u8string ot::make_cover(ot::byte_string_view picture_data) | ||||
|   | ||||
| @@ -111,21 +111,27 @@ struct status { | ||||
| 	std::string message; | ||||
| }; | ||||
|  | ||||
| using byte_string = std::basic_string<uint8_t>; | ||||
| using byte_string_view = std::basic_string_view<uint8_t>; | ||||
| /** | ||||
|  * Alias for binary data strings. Concretely the same as regular strings but reflect the intent. | ||||
|  */ | ||||
| using byte_string = std::string; | ||||
| using byte_string_view = std::string_view; | ||||
|  | ||||
| /***********************************************************************************************//** | ||||
|  * \defgroup system System | ||||
|  * \{ | ||||
|  */ | ||||
|  | ||||
| /** fclose wrapper for std::unique_ptr’s deleter. */ | ||||
| void close_file(FILE*); | ||||
|  | ||||
| /** | ||||
|  * 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) {} | ||||
| struct file : std::unique_ptr<FILE, decltype(&close_file)> { | ||||
| 	file(FILE* f = nullptr) : std::unique_ptr<FILE, decltype(&close_file)>(f, &close_file) {} | ||||
| }; | ||||
|  | ||||
| /** | ||||
| @@ -260,10 +266,9 @@ struct ogg_reader { | ||||
| 	ogg_page page; | ||||
| 	/** | ||||
| 	 * Page number in the physical stream of the last read page, disregarding multiplexed | ||||
| 	 * streams. The first page number is 0. When no page has been read, its value is | ||||
| 	 * (size_t) -1. | ||||
| 	 * streams. The first page number is 0. When no page has been read, its value is -1. | ||||
| 	 */ | ||||
| 	size_t absolute_page_no = -1; | ||||
| 	long absolute_page_no = -1; | ||||
| 	/** | ||||
| 	 * The file is our source of binary data. It is not integrated to libogg, so we need to | ||||
| 	 * handle it ourselves. | ||||
| @@ -515,12 +520,32 @@ struct options { | ||||
| 	 * Option: --output-cover | ||||
| 	 */ | ||||
| 	std::optional<std::string> cover_out; | ||||
| 	/** | ||||
| 	 * Print the vendor string at the beginning of the OpusTags packet instead of printing the | ||||
| 	 * tags. Only applicable in read-only mode. | ||||
| 	 * | ||||
| 	 * Option: --vendor | ||||
| 	 */ | ||||
| 	bool print_vendor = false; | ||||
| 	/** | ||||
| 	 * Replace the vendor string by the one specified by the user. | ||||
| 	 * | ||||
| 	 * Option: --set-vendor | ||||
| 	 */ | ||||
| 	std::optional<std::u8string> set_vendor; | ||||
| 	/** | ||||
| 	 * Disable encoding conversions. OpusTags are specified to always be encoded as UTF-8, but | ||||
| 	 * if for some reason a specific file contains binary tags that someone would like to | ||||
| 	 * extract and set as-is, encoding conversion would get in the way. | ||||
| 	 */ | ||||
| 	bool raw = false; | ||||
| 	/** | ||||
| 	 * In text mode (default), tags are separated by a line feed. However, when combining | ||||
| 	 * opustags with grep or other line-based tools, this proves to be a bad separator because | ||||
| 	 * tag values may contain newlines. Changing the delimiter to '\0' with -z eases the | ||||
| 	 * processing of multi-line tags with other tools that support null-terminated lines. | ||||
| 	 */ | ||||
| 	char tag_delimiter = '\n'; | ||||
| }; | ||||
|  | ||||
| /** | ||||
| @@ -538,13 +563,13 @@ options parse_options(int argc, char** argv, FILE* comments); | ||||
|  * | ||||
|  * The output generated is meant to be parseable by #ot::read_comments. | ||||
|  */ | ||||
| void print_comments(const std::list<std::u8string>& comments, FILE* output, bool raw); | ||||
| void print_comments(const std::list<std::u8string>& comments, FILE* output, const options& opt); | ||||
|  | ||||
| /** | ||||
|  * Parse the comments outputted by #ot::print_comments. Unless raw is true, the comments are | ||||
|  * converted from the system encoding to UTF-8, and returned as UTF-8. | ||||
|  */ | ||||
| std::list<std::u8string> read_comments(FILE* input, bool raw); | ||||
| std::list<std::u8string> read_comments(FILE* input, const options& opt); | ||||
|  | ||||
| /** | ||||
|  * Remove all comments matching the specified selector, which may either be a field name or a | ||||
| @@ -561,7 +586,3 @@ void run(const options& opt); | ||||
| /** \} */ | ||||
|  | ||||
| } | ||||
|  | ||||
| /** Handy literal suffix for building byte strings. */ | ||||
| ot::byte_string operator""_bs(const char* data, size_t size); | ||||
| ot::byte_string_view operator""_bsv(const char* data, size_t size); | ||||
|   | ||||
| @@ -19,14 +19,9 @@ | ||||
| #include <sys/wait.h> | ||||
| #include <unistd.h> | ||||
|  | ||||
| ot::byte_string operator""_bs(const char* data, size_t size) | ||||
| void ot::close_file(FILE* file) | ||||
| { | ||||
| 	return ot::byte_string(reinterpret_cast<const uint8_t*>(data), size); | ||||
| } | ||||
|  | ||||
| ot::byte_string_view operator""_bsv(const char* data, size_t size) | ||||
| { | ||||
| 	return ot::byte_string_view(reinterpret_cast<const uint8_t*>(data), size); | ||||
| 	fclose(file); | ||||
| } | ||||
|  | ||||
| void ot::partial_file::open(const char* destination) | ||||
| @@ -122,9 +117,9 @@ ot::byte_string ot::slurp_binary_file(const char* filename) | ||||
|  | ||||
| 	byte_string content; | ||||
| 	long file_size = get_file_size(f.get()); | ||||
| 	if (file_size == -1) { | ||||
| 	if (file_size < 0) { | ||||
| 		// Read the input stream block by block and resize the output byte string as needed. | ||||
| 		uint8_t buffer[4096]; | ||||
| 		char buffer[4096]; | ||||
| 		while (!feof(f.get())) { | ||||
| 			size_t read_len = fread(buffer, 1, sizeof(buffer), f.get()); | ||||
| 			content.append(buffer, read_len); | ||||
| @@ -135,7 +130,7 @@ ot::byte_string ot::slurp_binary_file(const char* filename) | ||||
| 	} else { | ||||
| 		// Lucky! We know the file size, so let’s slurp it at once. | ||||
| 		content.resize(file_size); | ||||
| 		if (fread(content.data(), 1, file_size, f.get()) < file_size) | ||||
| 		if (fread(content.data(), 1, file_size, f.get()) < size_t(file_size)) | ||||
| 			throw status { st::standard_error, | ||||
| 				       "Could not read '"s + filename + "': " + strerror(errno) + "." }; | ||||
| 	} | ||||
| @@ -244,7 +239,7 @@ std::string ot::shell_escape(std::string_view word) | ||||
|  | ||||
| void ot::run_editor(std::string_view editor, std::string_view path) | ||||
| { | ||||
| 	std::string command = std::string(editor) + " " + shell_escape(path); | ||||
| 	std::string command = std::string(editor) + " -- " + shell_escape(path); | ||||
| 	int status = system(command.c_str()); | ||||
|  | ||||
| 	if (status == -1) | ||||
|   | ||||
							
								
								
									
										32
									
								
								t/base64.cc
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								t/base64.cc
									
									
									
									
									
								
							| @@ -3,26 +3,26 @@ | ||||
|  | ||||
| static void check_encode_base64() | ||||
| { | ||||
| 	opaque_is(ot::encode_base64(""_bsv), u8"", "empty"); | ||||
| 	opaque_is(ot::encode_base64("a"_bsv), u8"YQ==", "1 character"); | ||||
| 	opaque_is(ot::encode_base64("aa"_bsv), u8"YWE=", "2 characters"); | ||||
| 	opaque_is(ot::encode_base64("aaa"_bsv), u8"YWFh", "3 characters"); | ||||
| 	opaque_is(ot::encode_base64("aaaa"_bsv), u8"YWFhYQ==", "4 characters"); | ||||
| 	opaque_is(ot::encode_base64("\xFF\xFF\xFE"_bsv), u8"///+", "RFC alphabet"); | ||||
| 	opaque_is(ot::encode_base64("\0x"_bsv), u8"AHg=", "embedded null bytes"); | ||||
| 	opaque_is(ot::encode_base64(""sv), u8"", "empty"); | ||||
| 	opaque_is(ot::encode_base64("a"sv), u8"YQ==", "1 character"); | ||||
| 	opaque_is(ot::encode_base64("aa"sv), u8"YWE=", "2 characters"); | ||||
| 	opaque_is(ot::encode_base64("aaa"sv), u8"YWFh", "3 characters"); | ||||
| 	opaque_is(ot::encode_base64("aaaa"sv), u8"YWFhYQ==", "4 characters"); | ||||
| 	opaque_is(ot::encode_base64("\xFF\xFF\xFE"sv), u8"///+", "RFC alphabet"); | ||||
| 	opaque_is(ot::encode_base64("\0x"sv), u8"AHg=", "embedded null bytes"); | ||||
| } | ||||
|  | ||||
| static void check_decode_base64() | ||||
| { | ||||
| 	opaque_is(ot::decode_base64(u8""), ""_bsv, "empty"); | ||||
| 	opaque_is(ot::decode_base64(u8"YQ=="), "a"_bsv, "1 character"); | ||||
| 	opaque_is(ot::decode_base64(u8"YWE="), "aa"_bsv, "2 characters"); | ||||
| 	opaque_is(ot::decode_base64(u8"YQ"), "a"_bsv, "padless 1 character"); | ||||
| 	opaque_is(ot::decode_base64(u8"YWE"), "aa"_bsv, "padless 2 characters"); | ||||
| 	opaque_is(ot::decode_base64(u8"YWFh"), "aaa"_bsv, "3 characters"); | ||||
| 	opaque_is(ot::decode_base64(u8"YWFhYQ=="), "aaaa"_bsv, "4 characters"); | ||||
| 	opaque_is(ot::decode_base64(u8"///+"), "\xFF\xFF\xFE"_bsv, "RFC alphabet"); | ||||
| 	opaque_is(ot::decode_base64(u8"AHg="), "\0x"_bsv, "embedded null bytes"); | ||||
| 	opaque_is(ot::decode_base64(u8""), ""sv, "empty"); | ||||
| 	opaque_is(ot::decode_base64(u8"YQ=="), "a"sv, "1 character"); | ||||
| 	opaque_is(ot::decode_base64(u8"YWE="), "aa"sv, "2 characters"); | ||||
| 	opaque_is(ot::decode_base64(u8"YQ"), "a"sv, "padless 1 character"); | ||||
| 	opaque_is(ot::decode_base64(u8"YWE"), "aa"sv, "padless 2 characters"); | ||||
| 	opaque_is(ot::decode_base64(u8"YWFh"), "aaa"sv, "3 characters"); | ||||
| 	opaque_is(ot::decode_base64(u8"YWFhYQ=="), "aaaa"sv, "4 characters"); | ||||
| 	opaque_is(ot::decode_base64(u8"///+"), "\xFF\xFF\xFE"sv, "RFC alphabet"); | ||||
| 	opaque_is(ot::decode_base64(u8"AHg="), "\0x"sv, "embedded null bytes"); | ||||
|  | ||||
| 	try { | ||||
| 		ot::decode_base64(u8"Y==="); | ||||
|   | ||||
							
								
								
									
										6
									
								
								t/cli.cc
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								t/cli.cc
									
									
									
									
									
								
							| @@ -5,8 +5,10 @@ | ||||
|  | ||||
| static ot::status read_comments(FILE* input, std::list<std::u8string>& comments, bool raw) | ||||
| { | ||||
| 	ot::options opt; | ||||
| 	opt.raw = raw; | ||||
| 	try { | ||||
| 		comments = ot::read_comments(input, raw); | ||||
| 		comments = ot::read_comments(input, opt); | ||||
| 	} catch (const ot::status& rc) { | ||||
| 		return rc; | ||||
| 	} | ||||
| @@ -185,6 +187,8 @@ void check_bad_arguments() | ||||
| 	            "Cannot specify standard output for both --output and --output-cover.", "-o and --output-cover conflict"); | ||||
| 	error_case({"opustags", "-i", "x", "y", "--output-cover", "z"}, | ||||
| 	            "Cannot use --output-cover with multiple input files.", "--output-cover with multiple input"); | ||||
| 	error_case({"opustags", "-i", "--vendor", "x"}, | ||||
| 	            "--vendor is only supported in read-only mode.", "--vendor when editing"); | ||||
| 	error_case({"opustags", "-d", "\xFF", "x"}, | ||||
| 	           "Could not encode argument into UTF-8:", | ||||
| 	           "-d with binary data"); | ||||
|   | ||||
							
								
								
									
										12
									
								
								t/opus.cc
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								t/opus.cc
									
									
									
									
									
								
							| @@ -123,7 +123,7 @@ static void recode_padding() | ||||
| 	op.packet = (unsigned char*) padded_OpusTags.data(); | ||||
|  | ||||
| 	ot::opus_tags tags = ot::parse_tags(op); | ||||
| 	if (tags.extra_data != "\0hello"_bsv) | ||||
| 	if (tags.extra_data != "\0hello"sv) | ||||
| 		throw failure("corrupted extra data"); | ||||
| 	// recode the packet and ensure it's exactly the same | ||||
| 	auto packet = ot::render_tags(tags); | ||||
| @@ -137,7 +137,7 @@ static void recode_padding() | ||||
|  | ||||
| static void extract_cover() | ||||
| { | ||||
| 	ot::byte_string_view picture_data = ""_bsv | ||||
| 	ot::byte_string_view picture_data = ""sv | ||||
| 		"\x00\x00\x00\x03" // Picture type 3. | ||||
| 		"\x00\x00\x00\x09" "image/foo" // MIME type. | ||||
| 		"\x00\x00\x00\x00" "" // Description. | ||||
| @@ -152,9 +152,9 @@ static void extract_cover() | ||||
| 	std::optional<ot::picture> cover = ot::extract_cover(tags); | ||||
| 	if (!cover) | ||||
| 		throw failure("could not extract the cover"); | ||||
| 	if (cover->mime_type != "image/foo"_bsv) | ||||
| 	if (cover->mime_type != "image/foo"sv) | ||||
| 		throw failure("bad extracted MIME type"); | ||||
| 	if (cover->picture_data != "Picture data"_bsv) | ||||
| 	if (cover->picture_data != "Picture data"sv) | ||||
| 		throw failure("bad extracted picture data"); | ||||
|  | ||||
| 	ot::byte_string_view truncated_data = picture_data.substr(0, picture_data.size() - 1); | ||||
| @@ -167,7 +167,7 @@ static void extract_cover() | ||||
|  | ||||
| static void make_cover() | ||||
| { | ||||
| 	ot::byte_string_view picture_block = ""_bsv | ||||
| 	ot::byte_string_view picture_block = ""sv | ||||
| 		"\x00\x00\x00\x03" // Picture type 3. | ||||
| 		"\x00\x00\x00\x09" "image/png" // MIME type. | ||||
| 		"\x00\x00\x00\x00" "" // Description. | ||||
| @@ -178,7 +178,7 @@ static void make_cover() | ||||
| 		"\x00\x00\x00\x11" "\x89PNG Picture data"; | ||||
|  | ||||
| 	std::u8string expected = u8"METADATA_BLOCK_PICTURE=" + ot::encode_base64(picture_block); | ||||
| 	opaque_is(ot::make_cover("\x89PNG Picture data"_bsv), expected, "build the picture tag"); | ||||
| 	opaque_is(ot::make_cover("\x89PNG Picture data"sv), expected, "build the picture tag"); | ||||
| } | ||||
|  | ||||
| int main() | ||||
|   | ||||
							
								
								
									
										58
									
								
								t/opustags.t
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								t/opustags.t
									
									
									
									
									
								
							| @@ -4,7 +4,8 @@ use strict; | ||||
| use warnings; | ||||
| use utf8; | ||||
|  | ||||
| use Test::More tests => 59; | ||||
| use Test::More tests => 66; | ||||
| use Test::Deep qw(cmp_deeply re); | ||||
|  | ||||
| use Digest::MD5; | ||||
| use File::Basename; | ||||
| @@ -53,34 +54,9 @@ $help->[0] =~ /^([^\n]*+)/; | ||||
| my $version = $1; | ||||
| like($version, qr/^opustags version (\d+\.\d+\.\d+)/, 'get the version string'); | ||||
|  | ||||
| my $expected_help = <<"EOF"; | ||||
| $version | ||||
|  | ||||
| Usage: opustags --help | ||||
|        opustags [OPTIONS] FILE | ||||
|        opustags OPTIONS -i FILE... | ||||
|        opustags OPTIONS FILE -o FILE | ||||
|  | ||||
| Options: | ||||
|   -h, --help                    print this help | ||||
|   -o, --output FILE             specify the output file | ||||
|   -i, --in-place                overwrite the input files | ||||
|   -y, --overwrite               overwrite the output file if it already exists | ||||
|   -a, --add FIELD=VALUE         add a comment | ||||
|   -d, --delete FIELD[=VALUE]    delete previously existing comments | ||||
|   -D, --delete-all              delete all the previously existing comments | ||||
|   -s, --set FIELD=VALUE         replace a comment | ||||
|   -S, --set-all                 import comments from standard input | ||||
|   -e, --edit                    edit tags interactively in VISUAL/EDITOR | ||||
|   --output-cover FILE           extract and save the cover art, if any | ||||
|   --set-cover FILE              sets the cover art | ||||
|   --raw                         disable encoding conversion | ||||
|  | ||||
| See the man page for extensive documentation. | ||||
| EOF | ||||
|  | ||||
| is_deeply(opustags('--help'), [$expected_help, '', 0], '--help displays the help message'); | ||||
| is_deeply(opustags('-h'), [$expected_help, '', 0], '-h displays the help message too'); | ||||
| my $expected_help = qr{opustags version .*\n\nUsage: opustags --help\n}; | ||||
| cmp_deeply(opustags('--help'), [re($expected_help), '', 0], '--help displays the help message'); | ||||
| cmp_deeply(opustags('-h'), [re($expected_help), '', 0], '-h displays the help message too'); | ||||
|  | ||||
| is_deeply(opustags('--derp'), ['', <<"EOF", 512], 'unrecognized option shows an error'); | ||||
| error: Unrecognized option '--derp'. | ||||
| @@ -342,3 +318,27 @@ unlink('out.png'); | ||||
| is_deeply(opustags(qw(-D --set-cover - gobble.opus), { in => "GIF8 x" }), [<<'END_OUT', '', 0], 'read the cover from stdin'); | ||||
| METADATA_BLOCK_PICTURE=AAAAAwAAAAlpbWFnZS9naWYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZHSUY4IHg= | ||||
| END_OUT | ||||
|  | ||||
| #################################################################################################### | ||||
| # Vendor string | ||||
|  | ||||
| is_deeply(opustags(qw(--vendor gobble.opus)), ["Lavf58.12.100\n", '', 0], 'print the vendor string'); | ||||
|  | ||||
| is_deeply(opustags(qw(--set-vendor opustags gobble.opus -o out.opus)), ['', '', 0], 'set the vendor string'); | ||||
| is_deeply(opustags(qw(--vendor out.opus)), ["opustags\n", '', 0], 'the vendor string was updated'); | ||||
| unlink('out.opus'); | ||||
|  | ||||
| #################################################################################################### | ||||
| # Multi-line tags | ||||
|  | ||||
| is_deeply(opustags(qw(--set-all gobble.opus -o out.opus), { in => "MULTILINE=one\n\ttwo\nSIMPLE=three\n" }), ['', '', 0], 'parses continuation lines'); | ||||
| is_deeply(opustags(qw(out.opus -z)), ["MULTILINE=one\ntwo\0SIMPLE=three\0", '', 0], 'delimits output with NUL on -z'); | ||||
| unlink('out.opus'); | ||||
|  | ||||
| is_deeply(opustags(qw(--set-all gobble.opus -o out.opus -z), { in => "MULTILINE=one\ntwo\0SIMPLE=three\0" }), ['', '', 0], 'delimits input with NUL on -z'); | ||||
| is_deeply(opustags(qw(out.opus)), [<<'END', '', 0], 'indents continuation lines'); | ||||
| MULTILINE=one | ||||
| 	two | ||||
| SIMPLE=three | ||||
| END | ||||
| unlink('out.opus'); | ||||
|   | ||||
| @@ -36,7 +36,7 @@ void check_partial_files() | ||||
|  | ||||
| void check_slurp() | ||||
| { | ||||
| 	static const ot::byte_string_view pixel = ""_bsv | ||||
| 	static const ot::byte_string_view pixel = ""sv | ||||
| 		"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d" | ||||
| 		"\x49\x48\x44\x52\x00\x00\x00\x01\x00\x00\x00\x01" | ||||
| 		"\x08\x02\x00\x00\x00\x90\x77\x53\xde\x00\x00\x00" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user