Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/base64.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <opustags.h>

#include <string.h>
#include <cstdint>

static const char8_t base64_table[65] =
u8"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
Expand Down
15 changes: 11 additions & 4 deletions src/cli.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
#include <sys/stat.h>
#include <unistd.h>
#include <algorithm>
#include <cstdint>

#if defined(_WIN32) || defined(__MINGW32__)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why check both _WIN32 and __MINGW32__. Isn’t _WIN32 enough?

#include <io.h>
#include <fcntl.h>
#include "win32_compat.h"
#endif

static const char help_message[] =
PROJECT_NAME " version " PROJECT_VERSION
Expand Down Expand Up @@ -442,7 +449,7 @@ static void edit_tags_interactively(ot::opus_tags& tags, const std::optional<std
}

// Applying the new tags.
tags_file = fopen(tags_path.c_str(), "re");
tags_file = fopen(tags_path.c_str(), "rb");

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove the e flag?

if (tags_file == nullptr)
throw ot::status {ot::st::standard_error, "Error opening " + tags_path + ": " + strerror(errno)};
try {
Expand Down Expand Up @@ -479,7 +486,7 @@ static void output_cover(const ot::opus_tags& tags, const ot::options &opt)
throw ot::status {ot::st::error, "Could not identify '" + opt.cover_out.value() + "': " + strerror(errno)};
}

output = fopen(opt.cover_out->c_str(), "w");
output = fopen(opt.cover_out->c_str(), "wb");
if (output == nullptr)
throw ot::status {ot::st::standard_error, "Could not open '" + opt.cover_out.value() + "' for writing: " + strerror(errno)};
}
Expand Down Expand Up @@ -557,7 +564,7 @@ static void run_single(const ot::options& opt, const std::string& path_in, const
ot::file input;
if (path_in == "-")
input = stdin;
else if ((input = fopen(path_in.c_str(), "re")) == nullptr)
else if ((input = fopen(path_in.c_str(), "rb")) == nullptr)
throw ot::status {ot::st::standard_error,
"Could not open '" + path_in + "' for reading: " + strerror(errno)};
ot::ogg_reader reader(input.get());
Expand Down Expand Up @@ -598,7 +605,7 @@ static void run_single(const ot::options& opt, const std::string& path_in, const
/* The output file exists. */
if (!S_ISREG(output_info.st_mode)) {
/* Special files are opened for writing directly. */
if ((final_output = fopen(path_out->c_str(), "we")) == nullptr)
if ((final_output = fopen(path_out->c_str(), "wb")) == nullptr)
throw ot::status {ot::st::standard_error,
"Could not open '" + path_out.value() + "' for writing: " + strerror(errno)};
output = final_output.get();
Expand Down
1 change: 1 addition & 0 deletions src/ogg.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

#include <errno.h>
#include <string.h>
#include <cstdint>

bool ot::is_opus_stream(const ogg_page& identification_header)
{
Expand Down
1 change: 1 addition & 0 deletions src/opus.cc
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

#include <string.h>
#include <algorithm>
#include <cstdint>

ot::opus_tags ot::parse_tags(const ogg_packet& packet)
{
Expand Down
11 changes: 11 additions & 0 deletions src/opustags.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@
#define be32toh(x) OSSwapBigToHostInt32(x)
#endif

#if defined(_WIN32) || defined(__MINGW32__)
#include <cstdint>

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than include cstdint in the .h and in every .cc, including it in opustags.h only is enough since all the .cc include it.

inline uint32_t htole32(uint32_t x) { return x; }
inline uint32_t le32toh(uint32_t x) { return x; }
inline uint32_t htobe32(uint32_t x) {
return ((x & 0xffu) << 24) | ((x & 0xff00u) << 8) |
((x & 0xff0000u) >> 8) | ((x >> 24) & 0xffu);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the & 0xffu necessary?

Also, doesn’t Windows already provide such a function, like htonl?

}
inline uint32_t be32toh(uint32_t x) { return htobe32(x); }
#endif

using namespace std::literals;

namespace ot {
Expand Down
51 changes: 50 additions & 1 deletion src/system.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,18 @@
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cstdint>

#ifndef _WIN32

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it curious to mix #ifndef and #if defined. Why not use an #else instead?

#include <sys/wait.h>
#endif

#if defined(_WIN32) || defined(__MINGW32__)
#include <io.h>
#include <fcntl.h>
#include "win32_compat.h"
#endif

void ot::close_file(FILE* file)
{
Expand All @@ -40,6 +50,14 @@ void ot::partial_file::open(const char* destination)
strerror(errno)};
}

#if defined(_WIN32) || defined(__MINGW32__)
// Windows does not use Unix-style file modes the same way.
// Just leave the default permissions created by the OS.
static void copy_permissions(const char* /*source*/, const char* /*dest*/)
{
// no-op on Windows
}
#else
static mode_t get_umask()
{
// libc doesn’t seem to provide a way to get umask without changing it, so we need this workaround.
Expand Down Expand Up @@ -72,13 +90,22 @@ static void copy_permissions(const char* source, const char* dest)
if (chmod(dest, target_mode) == -1)
fprintf(stderr, "warning: Could not set mode of %s: %s\n", dest, strerror(errno));
}
#endif

void ot::partial_file::commit()
{
if (file == nullptr)
return;
file.reset();
copy_permissions(final_name.c_str(), temporary_name.c_str());
#if defined(_WIN32) || defined(__MINGW32__)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could put the call to copy_permissions in an #else here instead of defining a blank function.

// Windows rename() refuses to overwrite an existing file
if (remove(final_name.c_str()) != 0 && errno != ENOENT) {
throw status {st::standard_error,
"Could not remove original file '" + final_name + "': " +
strerror(errno) + "."};
}
#endif
if (rename(temporary_name.c_str(), final_name.c_str()) == -1)
throw status {st::standard_error,
"Could not move the result file '" + temporary_name + "' to '" +
Expand Down Expand Up @@ -219,6 +246,10 @@ std::string ot::decode_utf8(std::u8string_view in)

std::string ot::shell_escape(std::string_view word)
{
#ifdef _WIN32
// Windows cmd.exe: double quotes protect spaces.
return "\"" + std::string(word) + "\"";

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If word contains a double quote, that’s an open door to serious shell injection vulnerabilities.

#else
std::string escaped_word;
// Pre-allocate the result, assuming most of the time enclosing it in single quotes is enough.
escaped_word.reserve(2 + word.size());
Expand All @@ -235,13 +266,27 @@ std::string ot::shell_escape(std::string_view word)
escaped_word += '\'';

return escaped_word;
#endif
}

void ot::run_editor(std::string_view editor, std::string_view path)
{
#if defined(_WIN32) || defined(__MINGW32__)
std::string command = std::string(editor) + " " + shell_escape(path);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The -- is there to prevent passing options to the editor, and is a common convention. What editor do you use?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use Neovim. I tested -e thoroughly on Windows after compiling, and the -- did not work with Notepad or some other editors. It did work with Neovim, although Neovim also worked when -- was omitted. Windows command-line argument handling seems to differ from the Unix behavior here.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue with dropping the -- argument is that whenever the file name begins with a -, the editor would interpret is as an option. That could lead to unintended behavior. Maybe the surest way would be to make the path absolute.

#else
std::string command = std::string(editor) + " -- " + shell_escape(path);
#endif

int status = system(command.c_str());

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is the only line in common between Windows and Unix, you might as well make on big #if instead of two halves.


#if defined(_WIN32) || defined(__MINGW32__)
// On Windows, system() returns the exit code directly (or -1 on error)
if (status == -1)
throw ot::status {st::standard_error, "system() error: "s + strerror(errno)};
else if (status != 0)
throw ot::status {st::child_process_failed,
"Child process exited with " + std::to_string(status)};
#else
if (status == -1)
throw ot::status {st::standard_error, "waitpid error: "s + strerror(errno)};
else if (!WIFEXITED(status))
Expand All @@ -250,6 +295,7 @@ void ot::run_editor(std::string_view editor, std::string_view path)
else if (WEXITSTATUS(status) != 0)
throw ot::status {st::child_process_failed,
"Child process exited with " + std::to_string(WEXITSTATUS(status))};
#endif
}

timespec ot::get_file_timestamp(const char* path)
Expand All @@ -262,6 +308,9 @@ timespec ot::get_file_timestamp(const char* path)
mtime = st.st_mtim;
#elif defined(HAVE_STAT_ST_MTIMESPEC)
mtime = st.st_mtimespec;
#elif defined(_WIN32) || defined(__MINGW32__)
mtime.tv_sec = st.st_mtime;
mtime.tv_nsec = 0;
#else
mtime.tv_sec = st.st_mtime;
mtime.tv_nsec = st.st_mtimensec;
Expand Down
60 changes: 60 additions & 0 deletions src/win32_compat.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#pragma once
#ifdef _WIN32

#include <io.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
#include <cstring>

#define strncasecmp _strnicmp

#ifndef getdelim

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this for? Is it possible that getdelim is sometimes defined as a macro, sometimes not, but never defined as a function?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is so it uses the windows equivalent of strncasecmp, I'll work on all these changes and make it suitable. I have a busy week coming up and start work in 2 hours, I'l try to clean all this up and explain on Saturday.

Sorry for the lack of explanation on these changes and having to wait until Saturday.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I misunderstood your question. I added the #ifndef getdelim because I wanted to prevent a duplicate definition if the platform already supplied getdelim(). I realize now that this only checks for a preprocessor macro, not whether the function is already provided.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly. If the _WIN32 macro check is not enough, we should rely on CMake because that’s precisely what it’s for. See CheckFunctionExists.

static inline ssize_t getdelim(char** lineptr, size_t* n, int delim, FILE* stream) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is huge for an inline function. You could move the definition into system.cc and add the declaration into opustags.h instead so that it is built and linked like a regular function.

By the way, where does the implementation come from?

@kungfubeaner kungfubeaner Aug 17, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrote the getdelim (and mkstemps) implementations myself to get the Windows build working. I looked at a few public versions for reference while writing it, but this is my own code. Happy to move both functions out of the header into system.cc (and just declare them in the header or in opustags.h) if you prefer that style. I can also clean them up further if you’d like.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. Since C++ is loaded with footguns so I’ll make sure to review it attentively, unless we find a battle-tested alternative.

Maybe we could replace getdelim with std::getline, provided the code does not rely too much on stdio. Regarding mkstemps, Windows seems to have _mktemp that could do some of the work.

@kungfubeaner kungfubeaner Aug 18, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, I will look into _mktemp, Windows seems very messy. I'll look into all this more thoroughly on Saturday including using getline instead of getdelim. My PR was pretty much out of excitement that I actually got it to compile and work on Windows. I've been wanting / needing opustags for Windows for a while.

if (lineptr == nullptr || n == nullptr || stream == nullptr) {
errno = EINVAL;
return -1;
}
size_t capacity = (*n > 0) ? *n : 128;
if (*lineptr == nullptr) {
*lineptr = static_cast<char*>(malloc(capacity));
if (*lineptr == nullptr) return -1;
*n = capacity;
}
size_t pos = 0;
int c;
while ((c = fgetc(stream)) != EOF) {
if (pos + 1 >= capacity) {
capacity *= 2;
char* newbuf = static_cast<char*>(realloc(*lineptr, capacity));
if (newbuf == nullptr) return -1;
*lineptr = newbuf;
*n = capacity;
}
(*lineptr)[pos++] = static_cast<char>(c);
if (c == delim) break;
}
if (pos == 0 && c == EOF) return -1;
(*lineptr)[pos] = '\0';
return static_cast<ssize_t>(pos);
}
#endif

#ifndef mkstemps
static inline int mkstemps(char* tmpl, int suffixlen) {
size_t len = strlen(tmpl);
if (len < (size_t)(6 + suffixlen)) return -1;
char* xxxx = tmpl + len - 6 - suffixlen;
static const char chars[] = "abcdefghijklmnopqrstuvwxyz0123456789";
for (int attempt = 0; attempt < 100; ++attempt) {
for (int i = 0; i < 6; ++i)
xxxx[i] = chars[rand() % (sizeof(chars) - 1)];
int fd = _open(tmpl, _O_CREAT | _O_EXCL | _O_RDWR | _O_BINARY, _S_IREAD | _S_IWRITE);
if (fd != -1) return fd;
if (errno != EEXIST) return -1;
}
return -1;
}
#endif

#endif