Skip to content
Closed
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
15 changes: 15 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,21 @@ include(CheckStructHasMember)
check_struct_has_member("struct stat" st_mtim sys/stat.h HAVE_STAT_ST_MTIM LANGUAGE CXX)
check_struct_has_member("struct stat" st_mtimespec sys/stat.h HAVE_STAT_ST_MTIMESPEC LANGUAGE CXX)

# getdelim and mkstemps are POSIX/BSD extensions that MSVCRT/UCRT does not
# provide. win32_compat.h supplies fallback implementations, scoped to
# Windows only, guarded by these HAVE_* macros so we only compile them in
# when the target runtime doesn't already have the real thing.

include(CheckFunctionExists)
check_function_exists(getdelim HAVE_GETDELIM)
check_function_exists(mkstemps HAVE_MKSTEMPS)
if(HAVE_GETDELIM)
add_definitions(-DHAVE_GETDELIM)
endif()
if(HAVE_MKSTEMPS)
add_definitions(-DHAVE_MKSTEMPS)
endif()

configure_file(src/config.h.in config.h @ONLY)
include_directories(BEFORE src "${CMAKE_BINARY_DIR}" ${OGG_INCLUDE_DIRS} ${Iconv_INCLUDE_DIRS})

Expand Down
14 changes: 10 additions & 4 deletions src/cli.cc
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
#include <unistd.h>
#include <algorithm>

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

static const char help_message[] =
PROJECT_NAME " version " PROJECT_VERSION
R"raw(
Expand Down Expand Up @@ -442,7 +448,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(), OT_FOPEN_RE);
if (tags_file == nullptr)
throw ot::status {ot::st::standard_error, "Error opening " + tags_path + ": " + strerror(errno)};
try {
Expand Down Expand Up @@ -479,7 +485,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(), OT_FOPEN_W);
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 +563,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(), OT_FOPEN_RE)) == 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 +604,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(), OT_FOPEN_WE)) == nullptr)
throw ot::status {ot::st::standard_error,
"Could not open '" + path_out.value() + "' for writing: " + strerror(errno)};
output = final_output.get();
Expand Down
35 changes: 35 additions & 0 deletions src/opustags.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#include <stdio.h>
#include <time.h>

#include <cstdint>
#include <functional>
#include <list>
#include <memory>
Expand All @@ -55,6 +56,40 @@
#define be32toh(x) OSSwapBigToHostInt32(x)
#endif

#ifdef _WIN32
// Windows has no <endian.h>. It's always little-endian in practice, so the
// LE conversions are no-ops; for BE, use the compiler's byte-swap builtin
// rather than hand-rolled masking/shifting. Both MinGW-w64 (GCC/Clang) and
// MSVC provide one.

# if defined(__GNUC__) || defined(__clang__)
inline uint32_t htobe32(uint32_t x) { return __builtin_bswap32(x); }
inline uint32_t be32toh(uint32_t x) { return __builtin_bswap32(x); }
# else
# include <cstdlib>
inline uint32_t htobe32(uint32_t x) { return _byteswap_ulong(x); }
inline uint32_t be32toh(uint32_t x) { return _byteswap_ulong(x); }
# endif
inline uint32_t htole32(uint32_t x) { return x; }
inline uint32_t le32toh(uint32_t x) { return x; }
#endif

#ifdef _WIN32
// MSVCRT/UCRT's fopen does not understand the glibc "e" mode character
// (O_CLOEXEC). We gate it per platform instead of dropping it outright, so
// POSIX builds keep close-on-exec, and Windows builds get explicit binary
// mode, which it needs since text mode does CRLF translation that would
// corrupt tag data and Ogg streams.

# define OT_FOPEN_RE "rb"
# define OT_FOPEN_WE "wb"
# define OT_FOPEN_W "wb"
#else
# define OT_FOPEN_RE "re"
# define OT_FOPEN_WE "we"
# define OT_FOPEN_W "w"
#endif

using namespace std::literals;

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

#ifdef _WIN32
#include <io.h>
#include <fcntl.h>
#include <share.h>
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include "win32_compat.h"
#else
#include <sys/wait.h>
#endif

#ifdef _WIN32
// getdelim() and mkstemps() are POSIX/BSD extensions the MSVCRT/UCRT does
// not provide. HAVE_GETDELIM / HAVE_MKSTEMPS are defined by CMake via
// check_function_exists() when the target runtime already has them, so
// these definitions only compile in when actually needed, rather than
// relying on a macro-name guard that can't detect a real function.
//
// These are scoped to Windows only and match the real functions' exact
// signatures, so they're drop-in replacements: ot::read_comments() in
// cli.cc calls getdelim() exactly the same way on every platform, and
// this file is the only place that changes.

#ifndef HAVE_GETDELIM
ssize_t getdelim(char** lineptr, size_t* n, int delim, FILE* stream)
{
if (lineptr == nullptr || n == nullptr || stream == nullptr) {
errno = EINVAL;
return -1;
}

// Accumulate into a std::string, which grows itself, rather than
// manually doubling a malloc'd buffer by hand. We still hand the
// result back through the caller's malloc'd buffer at the end, since
// that's the contract getdelim() callers (and free()) expect.
std::string buffer;
int c;
while ((c = fgetc(stream)) != EOF) {
buffer.push_back(static_cast<char>(c));
if (c == delim)
break;
}
if (buffer.empty())
return -1; // Nothing left to read.

size_t needed = buffer.size() + 1; // +1 for the null terminator.
if (*lineptr == nullptr || *n < needed) {
char* newbuf = static_cast<char*>(realloc(*lineptr, needed));
if (newbuf == nullptr)
return -1;
*lineptr = newbuf;
*n = needed;
}
memcpy(*lineptr, buffer.data(), buffer.size());
(*lineptr)[buffer.size()] = '\0';
return static_cast<ssize_t>(buffer.size());
}
#endif // HAVE_GETDELIM

#ifndef HAVE_MKSTEMPS
int mkstemps(char* tmpl, int suffixlen)
{
size_t len = strlen(tmpl);
if (suffixlen < 0 || len < static_cast<size_t>(6 + suffixlen))
return -1;
size_t placeholder_end = len - static_cast<size_t>(suffixlen);

// _mktemp_s requires the "XXXXXX" placeholder to be the last six
// characters of the string it operates on, but mkstemps' templates
// have a suffix after the placeholder (e.g. ".part"), which _mktemp_s
// does not support directly. Work around this by temporarily
// truncating the suffix off, letting _mktemp_s fill in the XXXXXX
// portion with its own (better than rand()) uniqueness algorithm,
// then restoring the suffix.

char saved_suffix[32];
if (static_cast<size_t>(suffixlen) >= sizeof(saved_suffix))
return -1;
memcpy(saved_suffix, tmpl + placeholder_end, static_cast<size_t>(suffixlen));
tmpl[placeholder_end] = '\0';

errno_t err = _mktemp_s(tmpl, placeholder_end + 1);

memcpy(tmpl + placeholder_end, saved_suffix, static_cast<size_t>(suffixlen));
tmpl[len] = '\0';

if (err != 0) {
errno = err;
return -1;
}

// _mktemp_s only picks a name, it does not create or open the file, so
// the O_CREAT|O_EXCL open below is what actually gives us mkstemps'
// atomicity guarantee (the name didn't exist and now it's ours).

int fd;
if (_sopen_s(&fd, tmpl, _O_CREAT | _O_EXCL | _O_RDWR | _O_BINARY, _SH_DENYNO,
_S_IREAD | _S_IWRITE) != 0)
return -1;
return fd;
}
#endif // HAVE_MKSTEMPS
#endif // _WIN32

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

#ifndef _WIN32
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 +181,27 @@ 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();
#ifndef _WIN32
// Windows does not use Unix-style file modes; the temporary file already has the correct
// default permissions. On Unix, we copy the original file's permissions.
copy_permissions(final_name.c_str(), temporary_name.c_str());
#endif

#ifdef _WIN32
// 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 +342,34 @@ std::string ot::decode_utf8(std::u8string_view in)

std::string ot::shell_escape(std::string_view word)
{
#ifdef _WIN32
if (!word.empty() && word.find_first_of(" \t\n\v\"") == std::string_view::npos)
return std::string(word);

std::string escaped = "\"";
for (auto it = word.begin();; ++it) {
unsigned backslashes = 0;
while (it != word.end() && *it == '\\') {
++backslashes;
++it;
}
if (it == word.end()) {
// Escape all backslashes, since they're followed by the closing quote.
escaped.append(backslashes * 2, '\\');
break;
} else if (*it == '"') {
// Escape all backslashes and the quote itself.
escaped.append(backslashes * 2 + 1, '\\');
escaped += '"';
} else {
// Backslashes aren't special here.
escaped.append(backslashes, '\\');
escaped += *it;
}
}
escaped += '"';
return escaped;
#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 +386,56 @@ std::string ot::shell_escape(std::string_view word)
escaped_word += '\'';

return escaped_word;
#endif
}

#ifdef _WIN32
/**
* Resolve a path to an absolute one using the Win32 API directly, rather
* than std::filesystem::absolute(). libstdc++'s filesystem implementation
* on MinGW does a narrow<->wide character-set conversion internally and
* throws filesystem_error on byte sequences it considers "illegal" under
* the current locale/codepage -- this was observed to crash on ordinary
* accented Latin characters.
*
* GetFullPathNameA operates on raw bytes with no encoding validation at
* all, so it can't fail this way.
*/
static std::string win32_absolute_path(std::string_view path)
{
std::string input(path);
char buffer[MAX_PATH];
DWORD len = GetFullPathNameA(input.c_str(), MAX_PATH, buffer, nullptr);
if (len == 0 || len >= MAX_PATH)
return input; // Fall back to the original path if resolution fails.
return std::string(buffer, len);
}
#endif

void ot::run_editor(std::string_view editor, std::string_view path)
{
// Always pass an absolute path to the editor. This is the surest way to
// avoid the editor misinterpreting the path as an option if it happens
// to start with '-' -- more reliable than "--", which not every editor
// respects the same way (observed difference in behavior between
// Notepad and Neovim on Windows).
#ifdef _WIN32
std::string abs_path = win32_absolute_path(path);
std::string command = std::string(editor) + " " + shell_escape(abs_path);
#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.


#ifdef _WIN32
// 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 +444,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 +457,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)
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
Loading