11021 lines
308 KiB
C++
11021 lines
308 KiB
C++
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: 2025 Niels Martignène <niels.martignene@protonmail.com>
|
|
|
|
#include "base.hh"
|
|
#include "crc.inc"
|
|
#include "unicode.inc"
|
|
|
|
#if __has_include("vendor/dragonbox/include/dragonbox/dragonbox.h")
|
|
#include "vendor/dragonbox/include/dragonbox/dragonbox.h"
|
|
#endif
|
|
|
|
#if defined(_WIN32)
|
|
#if !defined(NOMINMAX)
|
|
#define NOMINMAX
|
|
#endif
|
|
#if !defined(WIN32_LEAN_AND_MEAN)
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#endif
|
|
#include <ws2tcpip.h>
|
|
#include <windows.h>
|
|
#include <fcntl.h>
|
|
#include <io.h>
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <direct.h>
|
|
#include <shlobj.h>
|
|
#if !defined(ENABLE_VIRTUAL_TERMINAL_PROCESSING)
|
|
#define ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x0004
|
|
#endif
|
|
|
|
#if !defined(UNIX_PATH_MAX)
|
|
#define UNIX_PATH_MAX 108
|
|
#endif
|
|
typedef struct sockaddr_un {
|
|
ADDRESS_FAMILY sun_family;
|
|
char sun_path[UNIX_PATH_MAX];
|
|
} SOCKADDR_UN, *PSOCKADDR_UN;
|
|
|
|
#if defined(__MINGW32__)
|
|
// Some MinGW distributions set it to 0 by default
|
|
int _CRT_glob = 1;
|
|
#endif
|
|
|
|
#define RtlGenRandom SystemFunction036
|
|
extern "C" BOOLEAN NTAPI RtlGenRandom(PVOID RandomBuffer, ULONG RandomBufferLength);
|
|
|
|
typedef struct _IO_STATUS_BLOCK {
|
|
union {
|
|
LONG Status;
|
|
PVOID Pointer;
|
|
};
|
|
ULONG_PTR Information;
|
|
} IO_STATUS_BLOCK, *PIO_STATUS_BLOCK;
|
|
|
|
typedef LONG NTAPI NtCopyFileChunkFunc(HANDLE SourceHandle, HANDLE DestHandle, HANDLE Event,
|
|
PIO_STATUS_BLOCK IoStatusBlock, ULONG Length,
|
|
PLARGE_INTEGER SourceOffset, PLARGE_INTEGER DestOffset,
|
|
PULONG SourceKey, PULONG DestKey, ULONG Flags);
|
|
typedef ULONG RtlNtStatusToDosErrorFunc(LONG Status);
|
|
#elif defined(__wasi__)
|
|
#include <dirent.h>
|
|
#include <fcntl.h>
|
|
#include <fnmatch.h>
|
|
#include <poll.h>
|
|
#include <sys/ioctl.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <time.h>
|
|
|
|
extern char **environ;
|
|
#else
|
|
#include <dlfcn.h>
|
|
#include <dirent.h>
|
|
#include <fcntl.h>
|
|
#include <fnmatch.h>
|
|
#include <grp.h>
|
|
#include <poll.h>
|
|
#include <signal.h>
|
|
#include <spawn.h>
|
|
#include <sys/ioctl.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/resource.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/statvfs.h>
|
|
#include <sys/types.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/un.h>
|
|
#include <sys/wait.h>
|
|
#include <netinet/in.h>
|
|
#include <netinet/tcp.h>
|
|
#include <arpa/inet.h>
|
|
#include <termios.h>
|
|
#include <time.h>
|
|
|
|
extern char **environ;
|
|
#endif
|
|
#if defined(__linux__)
|
|
#include <sys/syscall.h>
|
|
#include <sys/sendfile.h>
|
|
#include <sys/eventfd.h>
|
|
#endif
|
|
#if defined(__APPLE__)
|
|
#include <sys/random.h>
|
|
#include <mach-o/dyld.h>
|
|
#include <copyfile.h>
|
|
#endif
|
|
#if defined(__OpenBSD__) || defined(__FreeBSD__)
|
|
#include <pthread_np.h>
|
|
#include <sys/param.h>
|
|
#include <sys/sysctl.h>
|
|
#endif
|
|
#if defined(__EMSCRIPTEN__)
|
|
#include <emscripten.h>
|
|
#endif
|
|
#include <chrono>
|
|
#include <random>
|
|
#include <thread>
|
|
|
|
namespace K {
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Utility
|
|
// ------------------------------------------------------------------------
|
|
|
|
#if !defined(FELIX)
|
|
#if defined(FELIX_TARGET)
|
|
const char *FelixTarget = K_STRINGIFY(FELIX_TARGET);
|
|
#else
|
|
const char *FelixTarget = "????";
|
|
#endif
|
|
const char *FelixVersion = "(unknown version)";
|
|
const char *FelixCompiler = "????";
|
|
#endif
|
|
|
|
extern "C" void AssertMessage(const char *filename, int line, const char *cond)
|
|
{
|
|
Print(StdErr, "%1:%2: Assertion '%3' failed\n", filename, line, cond);
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
|
|
void *MemMem(const void *src, Size src_len, const void *needle, Size needle_len)
|
|
{
|
|
K_ASSERT(src_len >= 0);
|
|
K_ASSERT(needle_len > 0);
|
|
|
|
src_len -= needle_len - 1;
|
|
|
|
int needle0 = *(const uint8_t *)needle;
|
|
Size offset = 0;
|
|
|
|
while (offset < src_len) {
|
|
uint8_t *next = (uint8_t *)memchr((uint8_t *)src + offset, needle0, (size_t)(src_len - offset));
|
|
|
|
if (!next)
|
|
return nullptr;
|
|
if (!memcmp(next, needle, (size_t)needle_len))
|
|
return next;
|
|
|
|
offset = next - (const uint8_t *)src + 1;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
#endif
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Memory / Allocator
|
|
// ------------------------------------------------------------------------
|
|
|
|
// This Allocator design should allow efficient and mostly-transparent use of memory
|
|
// arenas and simple pointer-bumping allocator. This will be implemented later, for
|
|
// now it's just a doubly linked list of malloc() memory blocks.
|
|
|
|
class MallocAllocator: public Allocator {
|
|
protected:
|
|
void *Allocate(Size size, unsigned int flags) override
|
|
{
|
|
void *ptr = malloc((size_t)size);
|
|
K_CRITICAL(ptr, "Failed to allocate %1 of memory", FmtMemSize(size));
|
|
|
|
if (flags & (int)AllocFlag::Zero) {
|
|
MemSet(ptr, 0, size);
|
|
}
|
|
|
|
return ptr;
|
|
}
|
|
|
|
void *Resize(void *ptr, Size old_size, Size new_size, unsigned int flags) override
|
|
{
|
|
if (!new_size) {
|
|
Release(ptr, old_size);
|
|
ptr = nullptr;
|
|
} else {
|
|
void *new_ptr = realloc(ptr, (size_t)new_size);
|
|
K_CRITICAL(new_ptr || !new_size, "Failed to resize %1 memory block to %2",
|
|
FmtMemSize(old_size), FmtMemSize(new_size));
|
|
|
|
if ((flags & (int)AllocFlag::Zero) && new_size > old_size) {
|
|
MemSet((uint8_t *)new_ptr + old_size, 0, new_size - old_size);
|
|
}
|
|
|
|
ptr = new_ptr;
|
|
}
|
|
|
|
return ptr;
|
|
}
|
|
|
|
void Release(const void *ptr, Size) override
|
|
{
|
|
free((void *)ptr);
|
|
}
|
|
};
|
|
|
|
class NullAllocator: public Allocator {
|
|
protected:
|
|
void *Allocate(Size, unsigned int) override { K_UNREACHABLE(); }
|
|
void *Resize(void *, Size, Size, unsigned int) override { K_UNREACHABLE(); }
|
|
void Release(const void *, Size) override {}
|
|
};
|
|
|
|
Allocator *GetDefaultAllocator()
|
|
{
|
|
static Allocator *default_allocator = new K_DEFAULT_ALLOCATOR;
|
|
return default_allocator;
|
|
}
|
|
|
|
Allocator *GetNullAllocator()
|
|
{
|
|
static Allocator *null_allocator = new NullAllocator;
|
|
return null_allocator;
|
|
}
|
|
|
|
LinkedAllocator& LinkedAllocator::operator=(LinkedAllocator &&other)
|
|
{
|
|
ReleaseAll();
|
|
list = other.list;
|
|
other.list = nullptr;
|
|
|
|
return *this;
|
|
}
|
|
|
|
void LinkedAllocator::ReleaseAll()
|
|
{
|
|
if (!list)
|
|
return;
|
|
|
|
Bucket *bucket = list;
|
|
|
|
do {
|
|
Bucket *next = bucket->next;
|
|
ReleaseRaw(allocator, bucket, -1);
|
|
bucket = next;
|
|
} while (bucket != list);
|
|
|
|
list = nullptr;
|
|
}
|
|
|
|
void LinkedAllocator::ReleaseAllExcept(void *ptr)
|
|
{
|
|
K_ASSERT(ptr);
|
|
|
|
Bucket *keep = PointerToBucket(ptr);
|
|
Bucket *bucket = keep->next;
|
|
|
|
while (bucket != keep) {
|
|
Bucket *next = bucket->next;
|
|
ReleaseRaw(allocator, bucket, -1);
|
|
bucket = next;
|
|
}
|
|
|
|
list = keep;
|
|
|
|
keep->prev = keep;
|
|
keep->next = keep;
|
|
}
|
|
|
|
void *LinkedAllocator::Allocate(Size size, unsigned int flags)
|
|
{
|
|
Bucket *bucket = (Bucket *)AllocateRaw(allocator, K_SIZE(Bucket) + size, flags);
|
|
|
|
bucket->prev = bucket;
|
|
bucket->next = bucket;
|
|
|
|
list = list ? list : bucket;
|
|
|
|
bucket->prev = list;
|
|
bucket->next = list->next;
|
|
list->next->prev = bucket;
|
|
list->next = bucket;
|
|
|
|
return (void *)bucket->data;
|
|
}
|
|
|
|
void *LinkedAllocator::Resize(void *ptr, Size old_size, Size new_size, unsigned int flags)
|
|
{
|
|
if (!ptr) {
|
|
ptr = Allocate(new_size, flags);
|
|
} else if (!new_size) {
|
|
Release(ptr, old_size);
|
|
ptr = nullptr;
|
|
} else {
|
|
Bucket *bucket = PointerToBucket(ptr);
|
|
bool single = (bucket->next == bucket);
|
|
|
|
bucket = (Bucket *)ResizeRaw(allocator, bucket, K_SIZE(Bucket) + old_size,
|
|
K_SIZE(Bucket) + new_size, flags);
|
|
|
|
list = bucket;
|
|
|
|
if (single) {
|
|
bucket->prev = bucket;
|
|
bucket->next = bucket;
|
|
} else {
|
|
bucket->prev->next = bucket;
|
|
bucket->next->prev = bucket;
|
|
}
|
|
|
|
ptr = (void *)bucket->data;
|
|
}
|
|
|
|
return ptr;
|
|
}
|
|
|
|
void LinkedAllocator::Release(const void *ptr, Size size)
|
|
{
|
|
if (!ptr)
|
|
return;
|
|
|
|
Bucket *bucket = PointerToBucket((void *)ptr);
|
|
bool single = (bucket->next == bucket);
|
|
|
|
list = single ? nullptr : bucket->next;
|
|
|
|
bucket->prev->next = bucket->next;
|
|
bucket->next->prev = bucket->prev;
|
|
|
|
ReleaseRaw(allocator, bucket, K_SIZE(Bucket) + size);
|
|
}
|
|
|
|
void LinkedAllocator::GiveTo(LinkedAllocator *alloc)
|
|
{
|
|
Bucket *other = alloc->list;
|
|
|
|
if (other && list) {
|
|
other->prev->next = list;
|
|
list->prev = other->prev;
|
|
list->next = other;
|
|
other->prev = list;
|
|
} else if (list) {
|
|
K_ASSERT(!alloc->list);
|
|
alloc->list = list;
|
|
}
|
|
|
|
list = nullptr;
|
|
}
|
|
|
|
LinkedAllocator::Bucket *LinkedAllocator::PointerToBucket(void *ptr)
|
|
{
|
|
uint8_t *data = (uint8_t *)ptr;
|
|
return (Bucket *)(data - offsetof(Bucket, data));
|
|
}
|
|
|
|
BlockAllocator& BlockAllocator::operator=(BlockAllocator &&other)
|
|
{
|
|
allocator.operator=(std::move(other.allocator));
|
|
|
|
block_size = other.block_size;
|
|
current_bucket = other.current_bucket;
|
|
last_alloc = other.last_alloc;
|
|
|
|
other.current_bucket = nullptr;
|
|
other.last_alloc = nullptr;
|
|
|
|
return *this;
|
|
}
|
|
|
|
void BlockAllocator::Reset()
|
|
{
|
|
last_alloc = nullptr;
|
|
|
|
if (current_bucket) {
|
|
current_bucket->used = 0;
|
|
allocator.ReleaseAllExcept(current_bucket);
|
|
} else {
|
|
allocator.ReleaseAll();
|
|
}
|
|
}
|
|
|
|
void BlockAllocator::ReleaseAll()
|
|
{
|
|
current_bucket = nullptr;
|
|
last_alloc = nullptr;
|
|
|
|
allocator.ReleaseAll();
|
|
}
|
|
|
|
void *BlockAllocator::Allocate(Size size, unsigned int flags)
|
|
{
|
|
K_ASSERT(size >= 0);
|
|
|
|
// Keep alignement requirements
|
|
Size aligned_size = AlignLen(size, 8);
|
|
|
|
if (AllocateSeparately(aligned_size)) {
|
|
uint8_t *ptr = (uint8_t *)AllocateRaw(&allocator, size, flags);
|
|
return ptr;
|
|
} else {
|
|
if (!current_bucket || (current_bucket->used + aligned_size) > block_size) {
|
|
current_bucket = (Bucket *)AllocateRaw(&allocator, K_SIZE(Bucket) + block_size,
|
|
flags & ~(int)AllocFlag::Zero);
|
|
current_bucket->used = 0;
|
|
}
|
|
|
|
uint8_t *ptr = current_bucket->data + current_bucket->used;
|
|
current_bucket->used += aligned_size;
|
|
|
|
if (flags & (int)AllocFlag::Zero) {
|
|
MemSet(ptr, 0, size);
|
|
}
|
|
|
|
last_alloc = ptr;
|
|
return ptr;
|
|
}
|
|
}
|
|
|
|
void *BlockAllocator::Resize(void *ptr, Size old_size, Size new_size, unsigned int flags)
|
|
{
|
|
K_ASSERT(old_size >= 0);
|
|
K_ASSERT(new_size >= 0);
|
|
|
|
if (!new_size) {
|
|
Release(ptr, old_size);
|
|
ptr = nullptr;
|
|
} else {
|
|
if (!ptr) {
|
|
old_size = 0;
|
|
}
|
|
|
|
Size aligned_old_size = AlignLen(old_size, 8);
|
|
Size aligned_new_size = AlignLen(new_size, 8);
|
|
Size aligned_delta = aligned_new_size - aligned_old_size;
|
|
|
|
// Try fast path
|
|
if (ptr && ptr == last_alloc &&
|
|
(current_bucket->used + aligned_delta) <= block_size &&
|
|
!AllocateSeparately(aligned_new_size)) {
|
|
current_bucket->used += aligned_delta;
|
|
|
|
if ((flags & (int)AllocFlag::Zero) && new_size > old_size) {
|
|
MemSet((uint8_t *)ptr + old_size, 0, new_size - old_size);
|
|
}
|
|
} else if (AllocateSeparately(aligned_old_size)) {
|
|
ptr = ResizeRaw(&allocator, ptr, old_size, new_size, flags);
|
|
} else {
|
|
void *new_ptr = Allocate(new_size, flags & ~(int)AllocFlag::Zero);
|
|
|
|
if (new_size > old_size) {
|
|
MemCpy(new_ptr, ptr, old_size);
|
|
|
|
if (flags & (int)AllocFlag::Zero) {
|
|
MemSet((uint8_t *)ptr + old_size, 0, new_size - old_size);
|
|
}
|
|
} else {
|
|
MemCpy(new_ptr, ptr, new_size);
|
|
}
|
|
|
|
ptr = new_ptr;
|
|
}
|
|
}
|
|
|
|
return ptr;
|
|
}
|
|
|
|
void BlockAllocator::Release(const void *ptr, Size size)
|
|
{
|
|
K_ASSERT(size >= 0);
|
|
|
|
if (ptr) {
|
|
Size aligned_size = AlignLen(size, 8);
|
|
|
|
if (ptr == last_alloc) {
|
|
current_bucket->used -= aligned_size;
|
|
|
|
if (!current_bucket->used) {
|
|
ReleaseRaw(&allocator, current_bucket, K_SIZE(Bucket) + block_size);
|
|
current_bucket = nullptr;
|
|
}
|
|
|
|
last_alloc = nullptr;
|
|
} else if (AllocateSeparately(aligned_size)) {
|
|
ReleaseRaw(&allocator, ptr, size);
|
|
}
|
|
}
|
|
}
|
|
|
|
void BlockAllocator::GiveTo(LinkedAllocator *alloc)
|
|
{
|
|
current_bucket = nullptr;
|
|
last_alloc = nullptr;
|
|
|
|
allocator.GiveTo(alloc);
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
|
|
void *AllocateSafe(Size len)
|
|
{
|
|
void *ptr = VirtualAlloc(nullptr, (SIZE_T)len, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
|
|
if (!ptr) {
|
|
LogError("Failed to allocate %1 of memory: %2", FmtMemSize(len), GetWin32ErrorString());
|
|
abort();
|
|
}
|
|
|
|
if (!VirtualLock(ptr, (SIZE_T)len)) {
|
|
LogError("Failed to lock memory (%1): %2", FmtMemSize(len), GetWin32ErrorString());
|
|
abort();
|
|
}
|
|
|
|
ZeroSafe(ptr, len);
|
|
|
|
return ptr;
|
|
}
|
|
|
|
void ReleaseSafe(void *ptr, Size len)
|
|
{
|
|
if (!ptr)
|
|
return;
|
|
|
|
ZeroSafe(ptr, len);
|
|
VirtualFree(ptr, 0, MEM_RELEASE);
|
|
}
|
|
|
|
void ZeroSafe(void *ptr, Size len)
|
|
{
|
|
SecureZeroMemory(ptr, (SIZE_T)len);
|
|
}
|
|
|
|
#elif !defined(__wasi__)
|
|
|
|
static int GetPageSize()
|
|
{
|
|
static Size pagesize = sysconf(_SC_PAGESIZE);
|
|
return pagesize;
|
|
}
|
|
|
|
void *AllocateSafe(Size len)
|
|
{
|
|
Size aligned = AlignLen(len, GetPageSize());
|
|
int flags = MAP_PRIVATE | MAP_ANONYMOUS;
|
|
|
|
#if defined(MAP_CONCEAL)
|
|
flags |= MAP_CONCEAL;
|
|
#endif
|
|
|
|
void *ptr = mmap(nullptr, (size_t)aligned, PROT_READ | PROT_WRITE, flags, -1, 0);
|
|
if (ptr == MAP_FAILED) {
|
|
LogError("Failed to allocate %1 of memory: %2", FmtMemSize(len), strerror(errno));
|
|
abort();
|
|
}
|
|
|
|
if (mlock(ptr, (size_t)aligned) < 0) {
|
|
LogError("Failed to lock memory (%1): %2", FmtMemSize(len), strerror(errno));
|
|
abort();
|
|
}
|
|
|
|
#if defined(MADV_DONTDUMP)
|
|
(void)madvise(ptr, (size_t)aligned, MADV_DONTDUMP);
|
|
#elif defined(MADV_NOCORE)
|
|
(void)madvise(ptr, (size_t)aligned, MADV_NOCORE);
|
|
#endif
|
|
|
|
ZeroSafe(ptr, len);
|
|
|
|
return ptr;
|
|
}
|
|
|
|
void ReleaseSafe(void *ptr, Size len)
|
|
{
|
|
if (!ptr)
|
|
return;
|
|
|
|
ZeroSafe(ptr, len);
|
|
|
|
Size aligned = AlignLen(len, GetPageSize());
|
|
munmap(ptr, aligned);
|
|
}
|
|
|
|
void ZeroSafe(void *ptr, Size len)
|
|
{
|
|
MemSet(ptr, 0, len);
|
|
__asm__ __volatile__("" : : "r"(ptr) : "memory");
|
|
}
|
|
|
|
#endif
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Date
|
|
// ------------------------------------------------------------------------
|
|
|
|
LocalDate LocalDate::FromJulianDays(int days)
|
|
{
|
|
K_ASSERT(days >= 0);
|
|
|
|
// Algorithm from Richards, copied from Wikipedia:
|
|
// https://en.wikipedia.org/w/index.php?title=Julian_day&oldid=792497863
|
|
|
|
LocalDate date;
|
|
{
|
|
int f = days + 1401 + (((4 * days + 274277) / 146097) * 3) / 4 - 38;
|
|
int e = 4 * f + 3;
|
|
int g = e % 1461 / 4;
|
|
int h = 5 * g + 2;
|
|
date.st.day = (int8_t)(h % 153 / 5 + 1);
|
|
date.st.month = (int8_t)((h / 153 + 2) % 12 + 1);
|
|
date.st.year = (int16_t)((e / 1461) - 4716 + (date.st.month < 3));
|
|
}
|
|
|
|
return date;
|
|
}
|
|
|
|
int LocalDate::ToJulianDays() const
|
|
{
|
|
K_ASSERT(IsValid());
|
|
|
|
// Straight from the Web:
|
|
// http://www.cs.utsa.edu/~cs1063/projects/Spring2011/Project1/jdn-explanation.html
|
|
|
|
int julian_days;
|
|
{
|
|
bool adjust = st.month < 3;
|
|
int year = st.year + 4800 - adjust;
|
|
int month = st.month + 12 * adjust - 3;
|
|
|
|
julian_days = st.day + (153 * month + 2) / 5 + 365 * year - 32045 +
|
|
year / 4 - year / 100 + year / 400;
|
|
}
|
|
|
|
return julian_days;
|
|
}
|
|
|
|
int LocalDate::GetWeekDay() const
|
|
{
|
|
K_ASSERT(IsValid());
|
|
|
|
// Zeller's congruence:
|
|
// https://en.wikipedia.org/wiki/Zeller%27s_congruence
|
|
|
|
int week_day;
|
|
{
|
|
int year = st.year;
|
|
int month = st.month;
|
|
if (month < 3) {
|
|
year--;
|
|
month += 12;
|
|
}
|
|
|
|
int century = year / 100;
|
|
year %= 100;
|
|
|
|
week_day = (st.day + (13 * (month + 1) / 5) + year + year / 4 + century / 4 + 5 * century + 5) % 7;
|
|
}
|
|
|
|
return week_day;
|
|
}
|
|
|
|
LocalDate &LocalDate::operator++()
|
|
{
|
|
K_ASSERT(IsValid());
|
|
|
|
if (st.day < DaysInMonth(st.year, st.month)) {
|
|
st.day++;
|
|
} else if (st.month < 12) {
|
|
st.month++;
|
|
st.day = 1;
|
|
} else {
|
|
st.year++;
|
|
st.month = 1;
|
|
st.day = 1;
|
|
}
|
|
|
|
return *this;
|
|
}
|
|
|
|
LocalDate &LocalDate::operator--()
|
|
{
|
|
K_ASSERT(IsValid());
|
|
|
|
if (st.day > 1) {
|
|
st.day--;
|
|
} else if (st.month > 1) {
|
|
st.month--;
|
|
st.day = DaysInMonth(st.year, st.month);
|
|
} else {
|
|
st.year--;
|
|
st.month = 12;
|
|
st.day = DaysInMonth(st.year, st.month);
|
|
}
|
|
|
|
return *this;
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Time
|
|
// ------------------------------------------------------------------------
|
|
|
|
#if defined(_WIN32)
|
|
|
|
static int64_t FileTimeToUnixTime(FILETIME ft)
|
|
{
|
|
int64_t time = ((int64_t)ft.dwHighDateTime << 32) | ft.dwLowDateTime;
|
|
return time / 10000 - 11644473600000ll;
|
|
}
|
|
|
|
static FILETIME UnixTimeToFileTime(int64_t time)
|
|
{
|
|
time = (time + 11644473600000ll) * 10000;
|
|
|
|
FILETIME ft;
|
|
ft.dwHighDateTime = (DWORD)(time >> 32);
|
|
ft.dwLowDateTime = (DWORD)time;
|
|
|
|
return ft;
|
|
}
|
|
|
|
#endif
|
|
|
|
int64_t GetUnixTime()
|
|
{
|
|
#if defined(_WIN32)
|
|
FILETIME ft;
|
|
GetSystemTimeAsFileTime(&ft);
|
|
|
|
return FileTimeToUnixTime(ft);
|
|
#elif defined(__EMSCRIPTEN__)
|
|
return (int64_t)emscripten_get_now();
|
|
#elif defined(__linux__)
|
|
struct timespec ts;
|
|
K_CRITICAL(clock_gettime(CLOCK_REALTIME_COARSE, &ts) == 0, "clock_gettime(CLOCK_REALTIME_COARSE) failed: %1", strerror(errno));
|
|
|
|
int64_t time = (int64_t)ts.tv_sec * 1000 + (int64_t)ts.tv_nsec / 1000000;
|
|
return time;
|
|
#else
|
|
struct timespec ts;
|
|
K_CRITICAL(clock_gettime(CLOCK_REALTIME, &ts) == 0, "clock_gettime(CLOCK_REALTIME) failed: %1", strerror(errno));
|
|
|
|
int64_t time = (int64_t)ts.tv_sec * 1000 + (int64_t)ts.tv_nsec / 1000000;
|
|
return time;
|
|
#endif
|
|
}
|
|
|
|
TimeSpec DecomposeTimeUTC(int64_t time)
|
|
{
|
|
TimeSpec spec = {};
|
|
|
|
#if defined(_WIN32)
|
|
__time64_t time64 = time / 1000;
|
|
|
|
struct tm ti = {};
|
|
_gmtime64_s(&ti, &time64);
|
|
#else
|
|
time_t time64 = time / 1000;
|
|
|
|
struct tm ti = {};
|
|
gmtime_r(&time64, &ti);
|
|
#endif
|
|
|
|
spec.year = (int16_t)(1900 + ti.tm_year);
|
|
spec.month = (int8_t)ti.tm_mon + 1; // Whose idea was it to use 0-11? ...
|
|
spec.day = (int8_t)ti.tm_mday;
|
|
spec.week_day = (int8_t)(ti.tm_wday ? (ti.tm_wday + 1) : 7);
|
|
spec.hour = (int8_t)ti.tm_hour;
|
|
spec.min = (int8_t)ti.tm_min;
|
|
spec.sec = (int8_t)ti.tm_sec;
|
|
spec.msec = time % 1000;
|
|
spec.offset = 0;
|
|
|
|
return spec;
|
|
}
|
|
|
|
TimeSpec DecomposeTimeLocal(int64_t time)
|
|
{
|
|
TimeSpec spec = {};
|
|
|
|
#if defined(_WIN32)
|
|
__time64_t time64 = time / 1000;
|
|
|
|
struct tm ti = {};
|
|
int offset = 0;
|
|
|
|
_localtime64_s(&ti, &time64);
|
|
|
|
struct tm utc = {};
|
|
_gmtime64_s(&utc, &time64);
|
|
|
|
offset = (int)(_mktime64(&ti) - _mktime64(&utc) + (3600 * ti.tm_isdst));
|
|
#else
|
|
time_t time64 = time / 1000;
|
|
|
|
struct tm ti = {};
|
|
int offset = 0;
|
|
|
|
localtime_r(&time64, &ti);
|
|
offset = ti.tm_gmtoff;
|
|
#endif
|
|
|
|
spec.year = (int16_t)(1900 + ti.tm_year);
|
|
spec.month = (int8_t)ti.tm_mon + 1; // Whose idea was it to use 0-11? ...
|
|
spec.day = (int8_t)ti.tm_mday;
|
|
spec.week_day = (int8_t)(ti.tm_wday ? (ti.tm_wday + 1) : 7);
|
|
spec.hour = (int8_t)ti.tm_hour;
|
|
spec.min = (int8_t)ti.tm_min;
|
|
spec.sec = (int8_t)ti.tm_sec;
|
|
spec.msec = time % 1000;
|
|
spec.offset = (int16_t)(offset / 60);
|
|
|
|
return spec;
|
|
}
|
|
|
|
int64_t ComposeTimeUTC(const TimeSpec &spec)
|
|
{
|
|
K_ASSERT(!spec.offset);
|
|
|
|
struct tm ti = {};
|
|
|
|
ti.tm_year = spec.year - 1900;
|
|
ti.tm_mon = spec.month - 1;
|
|
ti.tm_mday = spec.day;
|
|
ti.tm_hour = spec.hour;
|
|
ti.tm_min = spec.min;
|
|
ti.tm_sec = spec.sec;
|
|
|
|
#if defined(_WIN32)
|
|
int64_t time = (int64_t)_mkgmtime64(&ti);
|
|
#else
|
|
int64_t time = (int64_t)timegm(&ti);
|
|
#endif
|
|
|
|
time *= 1000;
|
|
time += spec.msec;
|
|
|
|
return time;
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Clock
|
|
// ------------------------------------------------------------------------
|
|
|
|
int64_t GetMonotonicClock()
|
|
{
|
|
static std::atomic_int64_t memory;
|
|
|
|
#if defined(_WIN32)
|
|
int64_t clock = (int64_t)GetTickCount64();
|
|
#elif defined(__EMSCRIPTEN__)
|
|
int64_t clock = emscripten_get_now();
|
|
#elif defined(CLOCK_MONOTONIC_COARSE)
|
|
struct timespec ts;
|
|
K_CRITICAL(clock_gettime(CLOCK_MONOTONIC_COARSE, &ts) == 0, "clock_gettime(CLOCK_MONOTONIC_COARSE) failed: %1", strerror(errno));
|
|
|
|
int64_t clock = (int64_t)ts.tv_sec * 1000 + (int64_t)ts.tv_nsec / 1000000;
|
|
#else
|
|
struct timespec ts;
|
|
K_CRITICAL(clock_gettime(CLOCK_MONOTONIC, &ts) == 0, "clock_gettime(CLOCK_MONOTONIC) failed: %1", strerror(errno));
|
|
|
|
int64_t clock = (int64_t)ts.tv_sec * 1000 + (int64_t)ts.tv_nsec / 1000000;
|
|
#endif
|
|
|
|
// Protect against clock going backwards
|
|
int64_t prev = memory.load(std::memory_order_relaxed);
|
|
if (clock < prev) [[unlikely]]
|
|
return prev;
|
|
memory.compare_exchange_weak(prev, clock, std::memory_order_relaxed, std::memory_order_relaxed);
|
|
|
|
return clock;
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Strings
|
|
// ------------------------------------------------------------------------
|
|
|
|
bool CopyString(const char *str, Span<char> buf)
|
|
{
|
|
#if defined(K_DEBUG)
|
|
K_ASSERT(buf.len > 0);
|
|
#else
|
|
if (!buf.len) [[unlikely]]
|
|
return false;
|
|
#endif
|
|
|
|
Size i = 0;
|
|
for (; str[i]; i++) {
|
|
if (i >= buf.len - 1) [[unlikely]] {
|
|
buf[buf.len - 1] = 0;
|
|
return false;
|
|
}
|
|
buf[i] = str[i];
|
|
}
|
|
buf[i] = 0;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CopyString(Span<const char> str, Span<char> buf)
|
|
{
|
|
#if defined(K_DEBUG)
|
|
K_ASSERT(buf.len > 0);
|
|
#else
|
|
if (!buf.len) [[unlikely]]
|
|
return false;
|
|
#endif
|
|
|
|
Size copy_len = std::min(str.len, buf.len - 1);
|
|
|
|
MemCpy(buf.ptr, str.ptr, copy_len);
|
|
buf[copy_len] = 0;
|
|
|
|
return (copy_len == str.len);
|
|
}
|
|
|
|
Span<char> DuplicateString(Span<const char> str, Allocator *alloc)
|
|
{
|
|
K_ASSERT(alloc);
|
|
|
|
char *new_str = (char *)AllocateRaw(alloc, str.len + 1);
|
|
MemCpy(new_str, str.ptr, str.len);
|
|
new_str[str.len] = 0;
|
|
return MakeSpan(new_str, str.len);
|
|
}
|
|
|
|
template <typename CompareFunc>
|
|
static inline int NaturalCmp(Span<const char> str1, Span<const char> str2, CompareFunc cmp)
|
|
{
|
|
Size i = 0;
|
|
Size j = 0;
|
|
|
|
while (i < str1.len && j < str2.len) {
|
|
int delta = cmp(str1[i], str2[j]);
|
|
|
|
if (delta) {
|
|
if (IsAsciiDigit(str1[i]) && IsAsciiDigit(str2[i])) {
|
|
while (i < str1.len && str1[i] == '0') {
|
|
i++;
|
|
}
|
|
while (j < str2.len && str2[j] == '0') {
|
|
j++;
|
|
}
|
|
|
|
bool digit1 = false;
|
|
bool digit2 = false;
|
|
int bias = 0;
|
|
|
|
for (;;) {
|
|
digit1 = (i < str1.len) && IsAsciiDigit(str1[i]);
|
|
digit2 = (j < str2.len) && IsAsciiDigit(str2[j]);
|
|
|
|
if (!digit1 || !digit2)
|
|
break;
|
|
|
|
bias = bias ? bias : cmp(str1[i], str2[j]);
|
|
i++;
|
|
j++;
|
|
}
|
|
|
|
if (!digit1 && !digit2 && bias) {
|
|
return bias;
|
|
} else if (digit1 || digit2) {
|
|
return digit1 ? 1 : -1;
|
|
}
|
|
} else {
|
|
return delta;
|
|
}
|
|
} else {
|
|
i++;
|
|
j++;
|
|
}
|
|
}
|
|
|
|
if (i == str1.len && j < str2.len) {
|
|
return -1;
|
|
} else if (i < str1.len) {
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
int CmpNatural(Span<const char> str1, Span<const char> str2)
|
|
{
|
|
auto cmp = [](int a, int b) { return a - b; };
|
|
return NaturalCmp(str1, str2, cmp);
|
|
}
|
|
|
|
int CmpNaturalI(Span<const char> str1, Span<const char> str2)
|
|
{
|
|
auto cmp = [](int a, int b) { return LowerAscii(a) - LowerAscii(b); };
|
|
return NaturalCmp(str1, str2, cmp);
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Format
|
|
// ------------------------------------------------------------------------
|
|
|
|
static const char DigitPairs[201] = "00010203040506070809101112131415161718192021222324"
|
|
"25262728293031323334353637383940414243444546474849"
|
|
"50515253545556575859606162636465666768697071727374"
|
|
"75767778798081828384858687888990919293949596979899";
|
|
static const char BigHexLiterals[] = "0123456789ABCDEF";
|
|
static const char SmallHexLiterals[] = "0123456789abcdef";
|
|
|
|
static Span<char> FormatUnsignedToDecimal(uint64_t value, char out_buf[32])
|
|
{
|
|
Size offset = 32;
|
|
{
|
|
int pair_idx;
|
|
do {
|
|
pair_idx = (int)((value % 100) * 2);
|
|
value /= 100;
|
|
offset -= 2;
|
|
MemCpy(out_buf + offset, DigitPairs + pair_idx, 2);
|
|
} while (value);
|
|
offset += (pair_idx < 20);
|
|
}
|
|
|
|
return MakeSpan(out_buf + offset, 32 - offset);
|
|
}
|
|
|
|
static Span<char> FormatUnsignedToBinary(uint64_t value, char out_buf[64])
|
|
{
|
|
Size msb = 64 - (Size)CountLeadingZeros(value);
|
|
if (!msb) {
|
|
msb = 1;
|
|
}
|
|
|
|
for (Size i = 0; i < msb; i++) {
|
|
bool bit = (value >> (msb - i - 1)) & 0x1;
|
|
out_buf[i] = bit ? '1' : '0';
|
|
}
|
|
|
|
return MakeSpan(out_buf, msb);
|
|
}
|
|
|
|
static Span<char> FormatUnsignedToOctal(uint64_t value, char out_buf[64])
|
|
{
|
|
Size offset = 64;
|
|
do {
|
|
uint64_t digit = value & 0x7;
|
|
value >>= 3;
|
|
out_buf[--offset] = BigHexLiterals[digit];
|
|
} while (value);
|
|
|
|
return MakeSpan(out_buf + offset, 64 - offset);
|
|
}
|
|
|
|
static Span<char> FormatUnsignedToBigHex(uint64_t value, char out_buf[32])
|
|
{
|
|
Size offset = 32;
|
|
do {
|
|
uint64_t digit = value & 0xF;
|
|
value >>= 4;
|
|
out_buf[--offset] = BigHexLiterals[digit];
|
|
} while (value);
|
|
|
|
return MakeSpan(out_buf + offset, 32 - offset);
|
|
}
|
|
|
|
static Span<char> FormatUnsignedToSmallHex(uint64_t value, char out_buf[32])
|
|
{
|
|
Size offset = 32;
|
|
do {
|
|
uint64_t digit = value & 0xF;
|
|
value >>= 4;
|
|
out_buf[--offset] = SmallHexLiterals[digit];
|
|
} while (value);
|
|
|
|
return MakeSpan(out_buf + offset, 32 - offset);
|
|
}
|
|
|
|
#if defined(JKJ_HEADER_DRAGONBOX)
|
|
static Size FakeFloatPrecision(Span<char> buf, int K, int min_prec, int max_prec, int *out_K)
|
|
{
|
|
K_ASSERT(min_prec >= 0);
|
|
|
|
if (-K < min_prec) {
|
|
int delta = min_prec + K;
|
|
MemSet(buf.end(), '0', delta);
|
|
|
|
*out_K -= delta;
|
|
return buf.len + delta;
|
|
} else if (-K > max_prec) {
|
|
if (-K <= buf.len) {
|
|
int offset = (int)buf.len + K;
|
|
int truncate = offset + max_prec;
|
|
int scale = offset + max_prec;
|
|
|
|
if (buf[truncate] >= '5') {
|
|
buf[truncate] = '0';
|
|
|
|
for (int i = truncate - 1; i >= 0; i--) {
|
|
if (buf[i] == '9') {
|
|
buf[i] = '0' + !i;
|
|
truncate += !i;
|
|
} else {
|
|
buf[i]++;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
*out_K -= (int)(scale - buf.len);
|
|
return truncate;
|
|
} else {
|
|
buf[0] = '0' + (-K == buf.len + 1 && buf[0] >= '5');
|
|
|
|
if (min_prec) {
|
|
MemSet(buf.ptr + 1, '0', min_prec - 1);
|
|
*out_K = -min_prec;
|
|
return min_prec;
|
|
} else {
|
|
*out_K = 0;
|
|
return 1;
|
|
}
|
|
}
|
|
} else {
|
|
return buf.len;
|
|
}
|
|
}
|
|
|
|
static Span<char> PrettifyFloat(Span<char> buf, int K, int min_prec, int max_prec)
|
|
{
|
|
// Apply precision settings after conversion
|
|
buf.len = FakeFloatPrecision(buf, K, min_prec, max_prec, &K);
|
|
|
|
int KK = (int)buf.len + K;
|
|
|
|
if (K >= 0) {
|
|
// 1234e7 -> 12340000000
|
|
|
|
if (!buf.len && !K) {
|
|
K = 1;
|
|
}
|
|
|
|
MemSet(buf.end(), '0', K);
|
|
buf.len += K;
|
|
} else if (KK > 0) {
|
|
// 1234e-2 -> 12.34
|
|
|
|
MemMove(buf.ptr + KK + 1, buf.ptr + KK, buf.len - KK);
|
|
buf.ptr[KK] = '.';
|
|
buf.len++;
|
|
} else {
|
|
// 1234e-6 -> 0.001234
|
|
|
|
int offset = 2 - KK;
|
|
MemMove(buf.ptr + offset, buf.ptr, buf.len);
|
|
MemSet(buf.ptr, '0', offset);
|
|
buf.ptr[1] = '.';
|
|
buf.len += offset;
|
|
}
|
|
|
|
return buf;
|
|
}
|
|
|
|
static Span<char> ExponentiateFloat(Span<char> buf, int K, int min_prec, int max_prec)
|
|
{
|
|
// Apply precision settings after conversion
|
|
buf.len = FakeFloatPrecision(buf, (int)(1 - buf.len), min_prec, max_prec, &K);
|
|
|
|
int exponent = (int)buf.len + K - 1;
|
|
|
|
if (buf.len > 1) {
|
|
MemMove(buf.ptr + 2, buf.ptr + 1, buf.len - 1);
|
|
buf.ptr[1] = '.';
|
|
buf.ptr[buf.len + 1] = 'e';
|
|
buf.len += 2;
|
|
} else {
|
|
buf.ptr[1] = 'e';
|
|
buf.len = 2;
|
|
}
|
|
|
|
if (exponent > 0) {
|
|
buf.ptr[buf.len++] = '+';
|
|
} else {
|
|
buf.ptr[buf.len++] = '-';
|
|
exponent = -exponent;
|
|
}
|
|
|
|
if (exponent >= 100) {
|
|
buf.ptr[buf.len++] = (char)('0' + exponent / 100);
|
|
exponent %= 100;
|
|
|
|
int pair_idx = (int)(exponent * 2);
|
|
MemCpy(buf.end(), DigitPairs + pair_idx, 2);
|
|
buf.len += 2;
|
|
} else if (exponent >= 10) {
|
|
int pair_idx = (int)(exponent * 2);
|
|
MemCpy(buf.end(), DigitPairs + pair_idx, 2);
|
|
buf.len += 2;
|
|
} else {
|
|
buf.ptr[buf.len++] = (char)('0' + exponent);
|
|
}
|
|
|
|
return buf;
|
|
}
|
|
#endif
|
|
|
|
// NaN and Inf are handled by caller
|
|
template <typename T>
|
|
Span<const char> FormatFloatingPoint(T value, bool non_zero, int min_prec, int max_prec, char out_buf[128])
|
|
{
|
|
#if defined(JKJ_HEADER_DRAGONBOX)
|
|
if (non_zero) {
|
|
auto v = jkj::dragonbox::to_decimal(value, jkj::dragonbox::policy::sign::ignore);
|
|
|
|
Span<char> buf = FormatUnsignedToDecimal(v.significand, out_buf);
|
|
int KK = (int)buf.len + v.exponent;
|
|
|
|
if (KK > -6 && KK <= 21) {
|
|
return PrettifyFloat(buf, v.exponent, min_prec, max_prec);
|
|
} else {
|
|
return ExponentiateFloat(buf, v.exponent, min_prec, max_prec);
|
|
}
|
|
} else {
|
|
Span<char> buf = MakeSpan(out_buf, 128);
|
|
|
|
buf[0] = '0';
|
|
if (min_prec) {
|
|
buf.ptr[1] = '.';
|
|
MemSet(buf.ptr + 2, '0', min_prec);
|
|
buf.len = 2 + min_prec;
|
|
} else {
|
|
buf.len = 1;
|
|
}
|
|
|
|
return buf;
|
|
}
|
|
#else
|
|
#if defined(_MSC_VER)
|
|
#pragma message("Cannot format floating point values correctly without Dragonbox")
|
|
#else
|
|
#warning Cannot format floating point values correctly without Dragonbox
|
|
#endif
|
|
|
|
int ret = snprintf(out_buf, 128, "%g", value);
|
|
return MakeSpan(out_buf, std::min(ret, 128));
|
|
#endif
|
|
}
|
|
|
|
template <typename AppendFunc>
|
|
static inline void AppendPad(Size pad, char padding, AppendFunc append)
|
|
{
|
|
for (Size i = 0; i < pad; i++) {
|
|
append(padding);
|
|
}
|
|
}
|
|
|
|
template <typename AppendFunc>
|
|
static inline void AppendSafe(char c, AppendFunc append)
|
|
{
|
|
if (IsAsciiControl(c))
|
|
return;
|
|
|
|
append(c);
|
|
}
|
|
|
|
template <typename AppendFunc>
|
|
static inline void ProcessArg(const FmtArg &arg, AppendFunc append)
|
|
{
|
|
switch (arg.type) {
|
|
case FmtType::Str: { append(arg.u.str); } break;
|
|
|
|
case FmtType::PadStr: {
|
|
append(arg.u.str);
|
|
AppendPad(arg.pad - arg.u.str.len, arg.padding, append);
|
|
} break;
|
|
case FmtType::RepeatStr: {
|
|
Span<const char> str = arg.u.repeat.str;
|
|
|
|
for (int i = 0; i < arg.u.repeat.count; i++) {
|
|
append(str);
|
|
}
|
|
} break;
|
|
|
|
case FmtType::Char: { append(MakeSpan(&arg.u.ch, 1)); } break;
|
|
case FmtType::Buffer: {
|
|
Span<const char> str = arg.u.buf;
|
|
append(str);
|
|
} break;
|
|
case FmtType::Custom: { arg.u.custom.Format(append); } break;
|
|
|
|
case FmtType::Bool: { append(arg.u.b ? "true" : "false"); } break;
|
|
|
|
case FmtType::Integer: {
|
|
if (arg.u.i < 0) {
|
|
char buf[128];
|
|
Span<const char> str = FormatUnsignedToDecimal((uint64_t)-arg.u.i, buf);
|
|
|
|
if (arg.pad) {
|
|
if (arg.padding == '0') {
|
|
append('-');
|
|
AppendPad((Size)arg.pad - str.len - 1, arg.padding, append);
|
|
} else {
|
|
AppendPad((Size)arg.pad - str.len - 1, arg.padding, append);
|
|
append('-');
|
|
}
|
|
} else {
|
|
append('-');
|
|
}
|
|
|
|
append(str);
|
|
} else {
|
|
char buf[128];
|
|
Span<const char> str = FormatUnsignedToDecimal((uint64_t)arg.u.i, buf);
|
|
|
|
AppendPad((Size)arg.pad - str.len, arg.padding, append);
|
|
append(str);
|
|
}
|
|
} break;
|
|
case FmtType::Unsigned: {
|
|
char buf[128];
|
|
Span<const char> str = FormatUnsignedToDecimal(arg.u.u, buf);
|
|
|
|
AppendPad((Size)arg.pad - str.len, arg.padding, append);
|
|
append(str);
|
|
} break;
|
|
|
|
case FmtType::Float: {
|
|
static const uint32_t ExponentMask = 0x7f800000u;
|
|
static const uint32_t MantissaMask = 0x007fffffu;
|
|
static const uint32_t SignMask = 0x80000000u;
|
|
|
|
union { float f; uint32_t u32; } u;
|
|
u.f = arg.u.f.value;
|
|
|
|
if ((u.u32 & ExponentMask) == ExponentMask) {
|
|
uint32_t mantissa = u.u32 & MantissaMask;
|
|
|
|
if (mantissa) {
|
|
append("NaN");
|
|
} else {
|
|
append((u.u32 & SignMask) ? "-Inf" : "Inf");
|
|
}
|
|
} else {
|
|
char buf[128];
|
|
|
|
if (u.u32 & SignMask) {
|
|
append('-');
|
|
append(FormatFloatingPoint(-u.f, true, arg.u.f.min_prec, arg.u.f.max_prec, buf));
|
|
} else {
|
|
append(FormatFloatingPoint(u.f, u.u32, arg.u.f.min_prec, arg.u.f.max_prec, buf));
|
|
}
|
|
}
|
|
} break;
|
|
case FmtType::Double: {
|
|
static const uint64_t ExponentMask = 0x7FF0000000000000ull;
|
|
static const uint64_t MantissaMask = 0x000FFFFFFFFFFFFFull;
|
|
static const uint64_t SignMask = 0x8000000000000000ull;
|
|
|
|
union { double d; uint64_t u64; } u;
|
|
u.d = arg.u.d.value;
|
|
|
|
if ((u.u64 & ExponentMask) == ExponentMask) {
|
|
uint64_t mantissa = u.u64 & MantissaMask;
|
|
|
|
if (mantissa) {
|
|
append("NaN");
|
|
} else {
|
|
append((u.u64 & SignMask) ? "-Inf" : "Inf");
|
|
}
|
|
} else {
|
|
char buf[128];
|
|
|
|
if (u.u64 & SignMask) {
|
|
append('-');
|
|
append(FormatFloatingPoint(-u.d, true, arg.u.d.min_prec, arg.u.d.max_prec, buf));
|
|
} else {
|
|
append(FormatFloatingPoint(u.d, u.u64, arg.u.d.min_prec, arg.u.d.max_prec, buf));
|
|
}
|
|
}
|
|
} break;
|
|
|
|
case FmtType::Binary: {
|
|
char buf[128];
|
|
Span<const char> str = FormatUnsignedToBinary(arg.u.u, buf);
|
|
|
|
AppendPad((Size)arg.pad - str.len, arg.padding, append);
|
|
append(str);
|
|
} break;
|
|
case FmtType::Octal: {
|
|
char buf[128];
|
|
Span<const char> str = FormatUnsignedToOctal(arg.u.u, buf);
|
|
|
|
AppendPad((Size)arg.pad - str.len, arg.padding, append);
|
|
append(str);
|
|
} break;
|
|
case FmtType::BigHex: {
|
|
char buf[128];
|
|
Span<const char> str = FormatUnsignedToBigHex(arg.u.u, buf);
|
|
|
|
AppendPad((Size)arg.pad - str.len, arg.padding, append);
|
|
append(str);
|
|
} break;
|
|
case FmtType::SmallHex: {
|
|
char buf[128];
|
|
Span<const char> str = FormatUnsignedToSmallHex(arg.u.u, buf);
|
|
|
|
AppendPad((Size)arg.pad - str.len, arg.padding, append);
|
|
append(str);
|
|
} break;
|
|
|
|
case FmtType::BigBytes: {
|
|
for (uint8_t c: arg.u.hex) {
|
|
char encoded[2];
|
|
|
|
encoded[0] = BigHexLiterals[((uint8_t)c >> 4) & 0xF];
|
|
encoded[1] = BigHexLiterals[((uint8_t)c >> 0) & 0xF];
|
|
|
|
Span<const char> buf = MakeSpan(encoded, 2);
|
|
append(buf);
|
|
}
|
|
} break;
|
|
case FmtType::SmallBytes: {
|
|
for (uint8_t c: arg.u.hex) {
|
|
char encoded[2];
|
|
|
|
encoded[0] = SmallHexLiterals[((uint8_t)c >> 4) & 0xF];
|
|
encoded[1] = SmallHexLiterals[((uint8_t)c >> 0) & 0xF];
|
|
|
|
Span<const char> buf = MakeSpan(encoded, 2);
|
|
append(buf);
|
|
}
|
|
} break;
|
|
|
|
case FmtType::MemorySize: {
|
|
char buf[128];
|
|
|
|
double size;
|
|
if (arg.u.i < 0) {
|
|
append('-');
|
|
size = (double)-arg.u.i;
|
|
} else {
|
|
size = (double)arg.u.i;
|
|
}
|
|
|
|
if (size >= 1073688137.0) {
|
|
size /= 1073741824.0;
|
|
|
|
int prec = 1 + (size < 9.9995) + (size < 99.995);
|
|
append(FormatFloatingPoint(size, true, prec, prec, buf));
|
|
append(" GiB");
|
|
} else if (size >= 1048524.0) {
|
|
size /= 1048576.0;
|
|
|
|
int prec = 1 + (size < 9.9995) + (size < 99.995);
|
|
append(FormatFloatingPoint(size, true, prec, prec, buf));
|
|
append(" MiB");
|
|
} else if (size >= 1023.95) {
|
|
size /= 1024.0;
|
|
|
|
int prec = 1 + (size < 9.9995) + (size < 99.995);
|
|
append(FormatFloatingPoint(size, true, prec, prec, buf));
|
|
append(" kiB");
|
|
} else {
|
|
append(FormatFloatingPoint(size, arg.u.i, 0, 0, buf));
|
|
append(" B");
|
|
}
|
|
} break;
|
|
case FmtType::DiskSize: {
|
|
char buf[128];
|
|
|
|
double size;
|
|
if (arg.u.i < 0) {
|
|
append('-');
|
|
size = (double)-arg.u.i;
|
|
} else {
|
|
size = (double)arg.u.i;
|
|
}
|
|
|
|
if (size >= 999950000.0) {
|
|
size /= 1000000000.0;
|
|
|
|
int prec = 1 + (size < 9.9995) + (size < 99.995);
|
|
append(FormatFloatingPoint(size, true, prec, prec, buf));
|
|
append(" GB");
|
|
} else if (size >= 999950.0) {
|
|
size /= 1000000.0;
|
|
|
|
int prec = 1 + (size < 9.9995) + (size < 99.995);
|
|
append(FormatFloatingPoint(size, true, prec, prec, buf));
|
|
append(" MB");
|
|
} else if (size >= 999.95) {
|
|
size /= 1000.0;
|
|
|
|
int prec = 1 + (size < 9.9995) + (size < 99.995);
|
|
append(FormatFloatingPoint(size, true, prec, prec, buf));
|
|
append(" kB");
|
|
} else {
|
|
append(FormatFloatingPoint(size, arg.u.i, 0, 0, buf));
|
|
append(" B");
|
|
}
|
|
} break;
|
|
|
|
case FmtType::Date: {
|
|
K_ASSERT(!arg.u.date.value || arg.u.date.IsValid());
|
|
|
|
char buf[128];
|
|
|
|
int year = arg.u.date.st.year;
|
|
if (year < 0) {
|
|
append('-');
|
|
year = -year;
|
|
}
|
|
if (year < 10) {
|
|
append("000");
|
|
} else if (year < 100) {
|
|
append("00");
|
|
} else if (year < 1000) {
|
|
append('0');
|
|
}
|
|
append(FormatUnsignedToDecimal((uint64_t)year, buf));
|
|
append('-');
|
|
if (arg.u.date.st.month < 10) {
|
|
append('0');
|
|
}
|
|
append(FormatUnsignedToDecimal((uint64_t)arg.u.date.st.month, buf));
|
|
append('-');
|
|
if (arg.u.date.st.day < 10) {
|
|
append('0');
|
|
}
|
|
append(FormatUnsignedToDecimal((uint64_t)arg.u.date.st.day, buf));
|
|
} break;
|
|
|
|
case FmtType::TimeISO: {
|
|
const TimeSpec &spec = arg.u.time.spec;
|
|
|
|
LocalArray<char, 128> buf;
|
|
|
|
if (spec.offset && arg.u.time.ms) {
|
|
int offset_h = spec.offset / 60;
|
|
int offset_m = spec.offset % 60;
|
|
|
|
buf.len = Fmt(buf.data, "%1%2%3T%4%5%6.%7%8%9%10",
|
|
FmtInt(spec.year, 2), FmtInt(spec.month, 2),
|
|
FmtInt(spec.day, 2), FmtInt(spec.hour, 2),
|
|
FmtInt(spec.min, 2), FmtInt(spec.sec, 2), FmtInt(spec.msec, 3),
|
|
offset_h >= 0 ? "+" : "", FmtInt(offset_h, 2), FmtInt(offset_m, 2)).len;
|
|
} else if (spec.offset) {
|
|
int offset_h = spec.offset / 60;
|
|
int offset_m = spec.offset % 60;
|
|
|
|
buf.len = Fmt(buf.data, "%1%2%3T%4%5%6%7%8%9",
|
|
FmtInt(spec.year, 2), FmtInt(spec.month, 2),
|
|
FmtInt(spec.day, 2), FmtInt(spec.hour, 2),
|
|
FmtInt(spec.min, 2), FmtInt(spec.sec, 2),
|
|
offset_h >= 0 ? "+" : "", FmtInt(offset_h, 2), FmtInt(offset_m, 2)).len;
|
|
} else if (arg.u.time.ms) {
|
|
buf.len = Fmt(buf.data, "%1%2%3T%4%5%6.%7Z",
|
|
FmtInt(spec.year, 2), FmtInt(spec.month, 2),
|
|
FmtInt(spec.day, 2), FmtInt(spec.hour, 2),
|
|
FmtInt(spec.min, 2), FmtInt(spec.sec, 2), FmtInt(spec.msec, 3)).len;
|
|
} else {
|
|
buf.len = Fmt(buf.data, "%1%2%3T%4%5%6Z",
|
|
FmtInt(spec.year, 2), FmtInt(spec.month, 2),
|
|
FmtInt(spec.day, 2), FmtInt(spec.hour, 2),
|
|
FmtInt(spec.min, 2), FmtInt(spec.sec, 2)).len;
|
|
}
|
|
|
|
append(buf);
|
|
} break;
|
|
case FmtType::TimeNice: {
|
|
const TimeSpec &spec = arg.u.time.spec;
|
|
|
|
LocalArray<char, 128> buf;
|
|
|
|
if (arg.u.time.ms) {
|
|
int offset_h = spec.offset / 60;
|
|
int offset_m = spec.offset % 60;
|
|
|
|
buf.len = Fmt(buf.data, "%1-%2-%3 %4:%5:%6.%7 %8%9%10",
|
|
FmtInt(spec.year, 2), FmtInt(spec.month, 2),
|
|
FmtInt(spec.day, 2), FmtInt(spec.hour, 2),
|
|
FmtInt(spec.min, 2), FmtInt(spec.sec, 2), FmtInt(spec.msec, 3),
|
|
offset_h >= 0 ? "+" : "", FmtInt(offset_h, 2), FmtInt(offset_m, 2)).len;
|
|
} else {
|
|
int offset_h = spec.offset / 60;
|
|
int offset_m = spec.offset % 60;
|
|
|
|
buf.len = Fmt(buf.data, "%1-%2-%3 %4:%5:%6 %7%8%9",
|
|
FmtInt(spec.year, 2), FmtInt(spec.month, 2),
|
|
FmtInt(spec.day, 2), FmtInt(spec.hour, 2),
|
|
FmtInt(spec.min, 2), FmtInt(spec.sec, 2),
|
|
offset_h >= 0 ? "+" : "", FmtInt(offset_h, 2), FmtInt(offset_m, 2)).len;
|
|
}
|
|
|
|
append(buf);
|
|
} break;
|
|
|
|
case FmtType::List: {
|
|
Span<const char> separator = arg.u.list.separator;
|
|
|
|
if (arg.u.list.u.names.len) {
|
|
append(arg.u.list.u.names[0]);
|
|
|
|
for (Size i = 1; i < arg.u.list.u.names.len; i++) {
|
|
append(separator);
|
|
append(arg.u.list.u.names[i]);
|
|
}
|
|
} else {
|
|
append(T("None"));
|
|
}
|
|
} break;
|
|
case FmtType::FlagNames: {
|
|
uint64_t flags = arg.u.list.flags;
|
|
Span<const char> separator = arg.u.list.separator;
|
|
|
|
if (flags) {
|
|
for (;;) {
|
|
int idx = CountTrailingZeros(flags);
|
|
flags &= ~(1ull << idx);
|
|
|
|
append(arg.u.list.u.names[idx]);
|
|
if (!flags)
|
|
break;
|
|
append(separator);
|
|
}
|
|
} else {
|
|
append(T("None"));
|
|
}
|
|
} break;
|
|
case FmtType::FlagOptions: {
|
|
uint64_t flags = arg.u.list.flags;
|
|
Span<const char> separator = arg.u.list.separator;
|
|
|
|
if (arg.u.list.flags) {
|
|
for (;;) {
|
|
int idx = CountTrailingZeros(flags);
|
|
flags &= ~(1ull << idx);
|
|
|
|
append(arg.u.list.u.options[idx].name);
|
|
if (!flags)
|
|
break;
|
|
append(separator);
|
|
}
|
|
} else {
|
|
append(T("None"));
|
|
}
|
|
} break;
|
|
|
|
case FmtType::Random: {
|
|
LocalArray<char, 512> buf;
|
|
|
|
static const char *const DefaultChars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
Span<const char> chars = arg.u.random.chars ? arg.u.random.chars : DefaultChars;
|
|
|
|
K_ASSERT(arg.u.random.len <= K_SIZE(buf.data));
|
|
buf.len = arg.u.random.len;
|
|
|
|
for (Size j = 0; j < arg.u.random.len; j++) {
|
|
int rnd = GetRandomInt(0, (int)chars.len);
|
|
buf[j] = chars[rnd];
|
|
}
|
|
|
|
append(buf);
|
|
} break;
|
|
|
|
case FmtType::SafeStr: {
|
|
for (char c: arg.u.str) {
|
|
AppendSafe(c, append);
|
|
}
|
|
} break;
|
|
case FmtType::SafeChar: { AppendSafe(arg.u.ch, append); } break;
|
|
}
|
|
}
|
|
|
|
template <typename AppendFunc>
|
|
static inline Size ProcessAnsiSpecifier(const char *spec, bool vt100, AppendFunc append)
|
|
{
|
|
Size idx = 0;
|
|
|
|
LocalArray<char, 32> buf;
|
|
bool valid = true;
|
|
|
|
buf.Append("\x1B[");
|
|
|
|
// Foreground color
|
|
switch (spec[++idx]) {
|
|
case 'd': { buf.Append("30"); } break;
|
|
case 'r': { buf.Append("31"); } break;
|
|
case 'g': { buf.Append("32"); } break;
|
|
case 'y': { buf.Append("33"); } break;
|
|
case 'b': { buf.Append("34"); } break;
|
|
case 'm': { buf.Append("35"); } break;
|
|
case 'c': { buf.Append("36"); } break;
|
|
case 'w': { buf.Append("37"); } break;
|
|
case 'D': { buf.Append("90"); } break;
|
|
case 'R': { buf.Append("91"); } break;
|
|
case 'G': { buf.Append("92"); } break;
|
|
case 'Y': { buf.Append("93"); } break;
|
|
case 'B': { buf.Append("94"); } break;
|
|
case 'M': { buf.Append("95"); } break;
|
|
case 'C': { buf.Append("96"); } break;
|
|
case 'W': { buf.Append("97"); } break;
|
|
case '.': { buf.Append("39"); } break;
|
|
case '0': {
|
|
buf.Append("0");
|
|
goto end;
|
|
} break;
|
|
case 0: {
|
|
valid = false;
|
|
goto end;
|
|
} break;
|
|
default: { valid = false; } break;
|
|
}
|
|
|
|
// Background color
|
|
switch (spec[++idx]) {
|
|
case 'd': { buf.Append(";40"); } break;
|
|
case 'r': { buf.Append(";41"); } break;
|
|
case 'g': { buf.Append(";42"); } break;
|
|
case 'y': { buf.Append(";43"); } break;
|
|
case 'b': { buf.Append(";44"); } break;
|
|
case 'm': { buf.Append(";45"); } break;
|
|
case 'c': { buf.Append(";46"); } break;
|
|
case 'w': { buf.Append(";47"); } break;
|
|
case 'D': { buf.Append(";100"); } break;
|
|
case 'R': { buf.Append(";101"); } break;
|
|
case 'G': { buf.Append(";102"); } break;
|
|
case 'Y': { buf.Append(";103"); } break;
|
|
case 'B': { buf.Append(";104"); } break;
|
|
case 'M': { buf.Append(";105"); } break;
|
|
case 'C': { buf.Append(";106"); } break;
|
|
case 'W': { buf.Append(";107"); } break;
|
|
case '.': { buf.Append(";49"); } break;
|
|
case 0: {
|
|
valid = false;
|
|
goto end;
|
|
} break;
|
|
default: { valid = false; } break;
|
|
}
|
|
|
|
// Bold/dim/underline/invert
|
|
switch (spec[++idx]) {
|
|
case '+': { buf.Append(";1"); } break;
|
|
case '-': { buf.Append(";2"); } break;
|
|
case '_': { buf.Append(";4"); } break;
|
|
case '^': { buf.Append(";7"); } break;
|
|
case '.': {} break;
|
|
case 0: {
|
|
valid = false;
|
|
goto end;
|
|
} break;
|
|
default: { valid = false; } break;
|
|
}
|
|
|
|
end:
|
|
if (!valid) {
|
|
#if defined(K_DEBUG)
|
|
LogDebug("Format string contains invalid ANSI specifier");
|
|
#endif
|
|
return idx;
|
|
}
|
|
|
|
if (vt100) {
|
|
buf.Append("m");
|
|
append(buf);
|
|
}
|
|
|
|
return idx;
|
|
}
|
|
|
|
template <typename AppendFunc>
|
|
static inline void DoFormat(const char *fmt, Span<const FmtArg> args, bool vt100, AppendFunc append)
|
|
{
|
|
#if defined(K_DEBUG)
|
|
bool invalid_marker = false;
|
|
uint32_t unused_arguments = ((uint32_t)1 << args.len) - 1;
|
|
#endif
|
|
|
|
const char *fmt_ptr = fmt;
|
|
for (;;) {
|
|
// Find the next marker (or the end of string) and write everything before it
|
|
const char *marker_ptr = fmt_ptr;
|
|
while (marker_ptr[0] && marker_ptr[0] != '%') {
|
|
marker_ptr++;
|
|
}
|
|
append(MakeSpan(fmt_ptr, (Size)(marker_ptr - fmt_ptr)));
|
|
if (!marker_ptr[0])
|
|
break;
|
|
|
|
// Try to interpret this marker as a number
|
|
Size idx = 0;
|
|
Size idx_end = 1;
|
|
for (;;) {
|
|
// Unsigned cast makes the test below quicker, don't remove it or it'll break
|
|
unsigned int digit = (unsigned int)marker_ptr[idx_end] - '0';
|
|
if (digit > 9)
|
|
break;
|
|
idx = (Size)(idx * 10) + (Size)digit;
|
|
idx_end++;
|
|
}
|
|
|
|
// That was indeed a number
|
|
if (idx_end > 1) {
|
|
idx--;
|
|
if (idx < args.len) {
|
|
ProcessArg<AppendFunc>(args[idx], append);
|
|
#if defined(K_DEBUG)
|
|
unused_arguments &= ~((uint32_t)1 << idx);
|
|
} else {
|
|
invalid_marker = true;
|
|
#endif
|
|
}
|
|
fmt_ptr = marker_ptr + idx_end;
|
|
} else if (marker_ptr[1] == '%') {
|
|
append('%');
|
|
fmt_ptr = marker_ptr + 2;
|
|
} else if (marker_ptr[1] == '/') {
|
|
append(*K_PATH_SEPARATORS);
|
|
fmt_ptr = marker_ptr + 2;
|
|
} else if (marker_ptr[1] == '!') {
|
|
fmt_ptr = marker_ptr + 2 + ProcessAnsiSpecifier(marker_ptr + 1, vt100, append);
|
|
} else if (marker_ptr[1]) {
|
|
append(marker_ptr[0]);
|
|
fmt_ptr = marker_ptr + 1;
|
|
#if defined(K_DEBUG)
|
|
invalid_marker = true;
|
|
#endif
|
|
} else {
|
|
#if defined(K_DEBUG)
|
|
invalid_marker = true;
|
|
#endif
|
|
break;
|
|
}
|
|
}
|
|
|
|
#if defined(K_DEBUG)
|
|
if (invalid_marker && unused_arguments) {
|
|
PrintLn(StdErr, "\nLog format string '%1' has invalid markers and unused arguments", fmt);
|
|
} else if (unused_arguments) {
|
|
PrintLn(StdErr, "\nLog format string '%1' has unused arguments", fmt);
|
|
} else if (invalid_marker) {
|
|
PrintLn(StdErr, "\nLog format string '%1' has invalid markers", fmt);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
Span<char> FmtFmt(const char *fmt, Span<const FmtArg> args, bool vt100, Span<char> out_buf)
|
|
{
|
|
K_ASSERT(out_buf.len >= 0);
|
|
|
|
if (!out_buf.len)
|
|
return {};
|
|
out_buf.len--;
|
|
|
|
Size available_len = out_buf.len;
|
|
|
|
DoFormat(fmt, args, vt100, [&](Span<const char> frag) {
|
|
Size copy_len = std::min(frag.len, available_len);
|
|
|
|
MemCpy(out_buf.end() - available_len, frag.ptr, copy_len);
|
|
available_len -= copy_len;
|
|
});
|
|
|
|
out_buf.len -= available_len;
|
|
out_buf.ptr[out_buf.len] = 0;
|
|
|
|
return out_buf;
|
|
}
|
|
|
|
Span<char> FmtFmt(const char *fmt, Span<const FmtArg> args, bool vt100, HeapArray<char> *out_buf)
|
|
{
|
|
Size start_len = out_buf->len;
|
|
|
|
out_buf->Grow(K_FMT_STRING_BASE_CAPACITY);
|
|
DoFormat(fmt, args, vt100, [&](Span<const char> frag) {
|
|
out_buf->Grow(frag.len + 1);
|
|
MemCpy(out_buf->end(), frag.ptr, frag.len);
|
|
out_buf->len += frag.len;
|
|
});
|
|
out_buf->ptr[out_buf->len] = 0;
|
|
|
|
return out_buf->Take(start_len, out_buf->len - start_len);
|
|
}
|
|
|
|
Span<char> FmtFmt(const char *fmt, Span<const FmtArg> args, bool vt100, Allocator *alloc)
|
|
{
|
|
K_ASSERT(alloc);
|
|
|
|
HeapArray<char> buf(alloc);
|
|
FmtFmt(fmt, args, vt100, &buf);
|
|
|
|
return buf.TrimAndLeak(1);
|
|
}
|
|
|
|
void FmtFmt(const char *fmt, Span<const FmtArg> args, bool vt100, FunctionRef<void(Span<const char>)> append)
|
|
{
|
|
// This one dos not null terminate! Be careful!
|
|
DoFormat(fmt, args, vt100, append);
|
|
}
|
|
|
|
void PrintFmt(const char *fmt, Span<const FmtArg> args, StreamWriter *st)
|
|
{
|
|
LocalArray<char, K_FMT_STRING_PRINT_BUFFER_SIZE> buf;
|
|
DoFormat(fmt, args, st->IsVt100(), [&](Span<const char> frag) {
|
|
if (frag.len > K_LEN(buf.data) - buf.len) {
|
|
st->Write(buf);
|
|
buf.len = 0;
|
|
}
|
|
if (frag.len >= K_LEN(buf.data)) {
|
|
st->Write(frag);
|
|
} else {
|
|
MemCpy(buf.data + buf.len, frag.ptr, frag.len);
|
|
buf.len += frag.len;
|
|
}
|
|
});
|
|
st->Write(buf);
|
|
}
|
|
|
|
void PrintLnFmt(const char *fmt, Span<const FmtArg> args, StreamWriter *st)
|
|
{
|
|
PrintFmt(fmt, args, st);
|
|
st->Write('\n');
|
|
}
|
|
|
|
// PrintLn variants without format strings
|
|
void PrintLn(StreamWriter *out_st)
|
|
{
|
|
out_st->Write('\n');
|
|
}
|
|
void PrintLn()
|
|
{
|
|
StdOut->Write('\n');
|
|
}
|
|
|
|
void FmtUpperAscii::Format(FunctionRef<void(Span<const char>)> append) const
|
|
{
|
|
for (char c: str) {
|
|
c = UpperAscii(c);
|
|
append((char)c);
|
|
}
|
|
}
|
|
|
|
void FmtLowerAscii::Format(FunctionRef<void(Span<const char>)> append) const
|
|
{
|
|
for (char c: str) {
|
|
c = LowerAscii(c);
|
|
append((char)c);
|
|
}
|
|
}
|
|
|
|
void FmtUrlSafe::Format(FunctionRef<void(Span<const char>)> append) const
|
|
{
|
|
for (char c: str) {
|
|
if (IsAsciiAlphaOrDigit(c) || strchr(passthrough, c)) {
|
|
append((char)c);
|
|
} else {
|
|
char encoded[3];
|
|
|
|
encoded[0] = '%';
|
|
encoded[1] = BigHexLiterals[((uint8_t)c >> 4) & 0xF];
|
|
encoded[2] = BigHexLiterals[((uint8_t)c >> 0) & 0xF];
|
|
|
|
Span<const char> buf = MakeSpan(encoded, 3);
|
|
append(buf);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FmtHtmlSafe::Format(FunctionRef<void(Span<const char>)> append) const
|
|
{
|
|
for (char c: str) {
|
|
switch (c) {
|
|
case '<': { append("<"); } break;
|
|
case '>': { append(">"); } break;
|
|
case '"': { append("""); } break;
|
|
case '\'': { append("'"); } break;
|
|
case '&': { append("&"); } break;
|
|
|
|
default: { append(c); } break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void FmtEscape::Format(FunctionRef<void(Span<const char>)> append) const
|
|
{
|
|
for (char c: str) {
|
|
if (c == '\r') {
|
|
append("\\r");
|
|
} else if (c == '\n') {
|
|
append("\\n");
|
|
} else if (c == '\\') {
|
|
append("\\\\");
|
|
} else if ((unsigned int)c < 32) {
|
|
char encoded[4];
|
|
|
|
encoded[0] = '\\';
|
|
encoded[1] = '0' + (((uint8_t)c >> 6) & 7);
|
|
encoded[2] = '0' + (((uint8_t)c >> 3) & 7);
|
|
encoded[3] = '0' + (((uint8_t)c >> 0) & 7);
|
|
|
|
Span<const char> buf = MakeSpan(encoded, 4);
|
|
append(buf);
|
|
} else if (c == quote) {
|
|
append('\\');
|
|
append(quote);
|
|
} else {
|
|
append(c);
|
|
}
|
|
}
|
|
}
|
|
|
|
FmtArg FmtVersion(int64_t version, int parts, int by)
|
|
{
|
|
K_ASSERT(version >= 0);
|
|
K_ASSERT(parts > 0);
|
|
|
|
FmtArg arg = {};
|
|
arg.type = FmtType::Buffer;
|
|
|
|
Span<char> buf = arg.u.buf;
|
|
int64_t divisor = 1;
|
|
|
|
for (int i = 1; i < parts; i++) {
|
|
divisor *= by;
|
|
}
|
|
|
|
for (int i = 0; i < parts; i++) {
|
|
int64_t component = (version / divisor) % by;
|
|
Size len = Fmt(buf, "%1.", component).len;
|
|
|
|
buf.ptr += len;
|
|
buf.len -= len;
|
|
|
|
divisor /= by;
|
|
}
|
|
|
|
// Remove trailing dot
|
|
buf.ptr[-1] = 0;
|
|
|
|
return arg;
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Debug and errors
|
|
// ------------------------------------------------------------------------
|
|
|
|
static int64_t start_clock = GetMonotonicClock();
|
|
|
|
static std::function<LogFunc> log_handler = DefaultLogHandler;
|
|
static bool log_vt100 = FileIsVt100(STDERR_FILENO);
|
|
|
|
// thread_local is broken on MinGW when destructors are involved.
|
|
// So heap allocation it is, at least for now.
|
|
static thread_local std::function<LogFilterFunc> *log_filters[16];
|
|
static thread_local Size log_filters_len;
|
|
|
|
const char *GetEnv(const char *name)
|
|
{
|
|
#if defined(__EMSCRIPTEN__)
|
|
// Each accessed environment variable is kept in memory and thus leaked once
|
|
static HashMap<const char *, const char *> values;
|
|
|
|
bool inserted;
|
|
auto bucket = values.InsertOrGetDefault(name, &inserted);
|
|
|
|
if (inserted) {
|
|
const char *str = (const char *)EM_ASM_INT({
|
|
try {
|
|
var name = UTF8ToString($0);
|
|
var str = process.env[name];
|
|
|
|
if (str == null)
|
|
return 0;
|
|
|
|
var bytes = lengthBytesUTF8(str) + 1;
|
|
var utf8 = _malloc(bytes);
|
|
stringToUTF8(str, utf8, bytes);
|
|
|
|
return utf8;
|
|
} catch (error) {
|
|
return 0;
|
|
}
|
|
}, name);
|
|
|
|
bucket->key = DuplicateString(name, GetDefaultAllocator()).ptr;
|
|
bucket->value = str;
|
|
}
|
|
|
|
return bucket->value;
|
|
#else
|
|
return getenv(name);
|
|
#endif
|
|
}
|
|
|
|
bool GetDebugFlag(const char *name)
|
|
{
|
|
const char *debug = GetEnv(name);
|
|
|
|
if (debug) {
|
|
bool ret = false;
|
|
if (!ParseBool(debug, &ret, K_DEFAULT_PARSE_FLAGS & ~(int)ParseFlag::Log)) {
|
|
LogError("Environment variable '%1' is not a boolean", name);
|
|
}
|
|
return ret;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static void RunLogFilter(Size idx, LogLevel level, const char *ctx, const char *msg)
|
|
{
|
|
const std::function<LogFilterFunc> &func = *log_filters[idx];
|
|
|
|
func(level, ctx, msg, [&](LogLevel level, const char *ctx, const char *msg) {
|
|
if (idx > 0) {
|
|
RunLogFilter(idx - 1, level, ctx, msg);
|
|
} else {
|
|
log_handler(level, ctx, msg);
|
|
}
|
|
});
|
|
}
|
|
|
|
void LogFmt(LogLevel level, const char *ctx, const char *fmt, Span<const FmtArg> args)
|
|
{
|
|
static thread_local bool skip = false;
|
|
|
|
static bool init = false;
|
|
static bool log_times;
|
|
|
|
// Avoid deadlock if a log filter or the handler tries to log something while handling a previous call
|
|
if (skip)
|
|
return;
|
|
skip = true;
|
|
K_DEFER { skip = false; };
|
|
|
|
if (!init) {
|
|
// Do this first... GetDebugFlag() might log an error or something, in which
|
|
// case we don't want to recurse forever and crash!
|
|
init = true;
|
|
|
|
log_times = GetDebugFlag("LOG_TIMES");
|
|
}
|
|
|
|
char ctx_buf[512];
|
|
if (log_times) {
|
|
double time = (double)(GetMonotonicClock() - start_clock) / 1000;
|
|
Fmt(ctx_buf, "[%1] %2", FmtDouble(time, 3, 8), ctx ? ctx : "");
|
|
|
|
ctx = ctx_buf;
|
|
}
|
|
|
|
char msg_buf[2048];
|
|
{
|
|
Size len = FmtFmt(T(fmt), args, log_vt100, msg_buf).len;
|
|
|
|
if (len == K_SIZE(msg_buf) - 1) {
|
|
strncpy(msg_buf + K_SIZE(msg_buf) - 32, "... [truncated]", 32);
|
|
msg_buf[K_SIZE(msg_buf) - 1] = 0;
|
|
}
|
|
}
|
|
|
|
if (log_filters_len) {
|
|
RunLogFilter(log_filters_len - 1, level, ctx, msg_buf);
|
|
} else {
|
|
log_handler(level, ctx, msg_buf);
|
|
}
|
|
}
|
|
|
|
void SetLogHandler(const std::function<LogFunc> &func, bool vt100)
|
|
{
|
|
log_handler = func;
|
|
log_vt100 = vt100;
|
|
}
|
|
|
|
void DefaultLogHandler(LogLevel level, const char *ctx, const char *msg)
|
|
{
|
|
switch (level) {
|
|
case LogLevel::Debug:
|
|
case LogLevel::Info: { Print(StdErr, "%!D..%1%!0%2\n", ctx ? ctx : "", msg); } break;
|
|
case LogLevel::Warning: { Print(StdErr, "%!M..%1%!0%2\n", ctx ? ctx : "", msg); } break;
|
|
case LogLevel::Error: { Print(StdErr, "%!R..%1%!0%2\n", ctx ? ctx : "", msg); } break;
|
|
}
|
|
}
|
|
|
|
void PushLogFilter(const std::function<LogFilterFunc> &func)
|
|
{
|
|
K_ASSERT(log_filters_len < K_LEN(log_filters));
|
|
log_filters[log_filters_len++] = new std::function<LogFilterFunc>(func);
|
|
}
|
|
|
|
void PopLogFilter()
|
|
{
|
|
K_ASSERT(log_filters_len > 0);
|
|
delete log_filters[--log_filters_len];
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
bool RedirectLogToWindowsEvents(const char *name)
|
|
{
|
|
static HANDLE log = nullptr;
|
|
K_ASSERT(!log);
|
|
|
|
log = OpenEventLogA(nullptr, name);
|
|
if (!log) {
|
|
LogError("Failed to register event provider: %1", GetWin32ErrorString());
|
|
return false;
|
|
}
|
|
atexit([]() { CloseEventLog(log); });
|
|
|
|
SetLogHandler([](LogLevel level, const char *ctx, const char *msg) {
|
|
WORD type = 0;
|
|
LocalArray<wchar_t, 8192> buf_w;
|
|
|
|
switch (level) {
|
|
case LogLevel::Debug:
|
|
case LogLevel::Info: { type = EVENTLOG_INFORMATION_TYPE; } break;
|
|
case LogLevel::Warning: { type = EVENTLOG_WARNING_TYPE; } break;
|
|
case LogLevel::Error: { type = EVENTLOG_ERROR_TYPE; } break;
|
|
}
|
|
|
|
// Append context
|
|
if (ctx) {
|
|
Size len = ConvertUtf8ToWin32Wide(ctx, buf_w.TakeAvailable());
|
|
if (len < 0)
|
|
return;
|
|
buf_w.len += len;
|
|
}
|
|
|
|
// Append message
|
|
{
|
|
Size len = ConvertUtf8ToWin32Wide(msg, buf_w.TakeAvailable());
|
|
if (len < 0)
|
|
return;
|
|
buf_w.len += len;
|
|
}
|
|
|
|
const wchar_t *ptr = buf_w.data;
|
|
ReportEventW(log, type, 0, 0, nullptr, 1, 0, &ptr, nullptr);
|
|
}, false);
|
|
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Progress
|
|
// ------------------------------------------------------------------------
|
|
|
|
#if !defined(__wasi__)
|
|
|
|
struct ProgressState {
|
|
char text[K_PROGRESS_TEXT_SIZE];
|
|
|
|
int64_t value;
|
|
int64_t min;
|
|
int64_t max;
|
|
|
|
bool determinate;
|
|
bool valid;
|
|
};
|
|
|
|
struct ProgressNode {
|
|
std::atomic_bool used;
|
|
|
|
std::mutex mutex;
|
|
ProgressState front;
|
|
ProgressState back;
|
|
};
|
|
|
|
static std::function<ProgressFunc> pg_handler = DefaultProgressHandler;
|
|
|
|
static std::atomic_int pg_count;
|
|
static ProgressNode pg_nodes[K_PROGRESS_MAX_NODES];
|
|
|
|
static std::mutex pg_mutex;
|
|
static bool pg_run = false;
|
|
|
|
static void RunProgressThread()
|
|
{
|
|
// Reuse for performance
|
|
HeapArray<ProgressInfo> bars;
|
|
|
|
int delay = StdErr->IsVt100() ? 400 : 4000;
|
|
|
|
for (;;) {
|
|
// Need to run still?
|
|
{
|
|
std::lock_guard<std::mutex> lock(pg_mutex);
|
|
|
|
if (!pg_count) {
|
|
pg_run = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
bars.RemoveFrom(0);
|
|
|
|
for (ProgressNode &node: pg_nodes) {
|
|
ProgressInfo bar = {};
|
|
|
|
// Copy state atomically or bail
|
|
{
|
|
std::unique_lock<std::mutex> lock(node.mutex, std::try_to_lock);
|
|
|
|
if (lock.owns_lock()) {
|
|
node.back = node.front;
|
|
lock.unlock();
|
|
}
|
|
|
|
if (!node.back.valid)
|
|
continue;
|
|
}
|
|
|
|
bar.text = node.back.text;
|
|
bar.value = node.back.value;
|
|
bar.min = node.back.min;
|
|
bar.max = node.back.max;
|
|
bar.determinate = node.back.determinate;
|
|
|
|
bars.Append(bar);
|
|
}
|
|
|
|
pg_handler(bars);
|
|
|
|
WaitDelay(delay);
|
|
}
|
|
}
|
|
|
|
ProgressHandle::~ProgressHandle()
|
|
{
|
|
ProgressNode *node = this->node.load();
|
|
|
|
if (node) {
|
|
std::lock_guard<std::mutex> lock(node->mutex);
|
|
|
|
node->front.valid = false;
|
|
node->used = false;
|
|
|
|
if (!--pg_count) {
|
|
StdErr->Flush();
|
|
}
|
|
}
|
|
}
|
|
|
|
void ProgressHandle::Set(int64_t value, int64_t min, int64_t max)
|
|
{
|
|
ProgressNode *node = AcquireNode();
|
|
|
|
if (!node) [[unlikely]]
|
|
return;
|
|
|
|
std::unique_lock<std::mutex> lock(node->mutex, std::try_to_lock);
|
|
|
|
if (!lock.owns_lock())
|
|
return;
|
|
|
|
node->front.value = value;
|
|
node->front.min = min;
|
|
node->front.max = max;
|
|
node->front.determinate = (max > min);
|
|
node->front.valid = true;
|
|
}
|
|
|
|
void ProgressHandle::Set(int64_t value, int64_t min, int64_t max, Span<const char> text)
|
|
{
|
|
ProgressNode *node = AcquireNode();
|
|
|
|
if (!node) [[unlikely]]
|
|
return;
|
|
|
|
std::unique_lock<std::mutex> lock(node->mutex, std::try_to_lock);
|
|
|
|
if (!lock.owns_lock())
|
|
return;
|
|
|
|
CopyText(text, node->front.text);
|
|
node->front.value = value;
|
|
node->front.min = min;
|
|
node->front.max = max;
|
|
node->front.determinate = (max > min);
|
|
node->front.valid = true;
|
|
}
|
|
|
|
ProgressNode *ProgressHandle::AcquireNode()
|
|
{
|
|
// Fast path
|
|
{
|
|
ProgressNode *node = this->node.load(std::memory_order_relaxed);
|
|
|
|
if (node)
|
|
return node;
|
|
}
|
|
|
|
int count = pg_count++;
|
|
|
|
if (!count) {
|
|
std::lock_guard lock(pg_mutex);
|
|
|
|
if (!pg_run) {
|
|
std::thread thread(RunProgressThread);
|
|
thread.detach();
|
|
|
|
pg_run = true;
|
|
}
|
|
} else if (count > K_PROGRESS_USED_NODES) {
|
|
pg_count--;
|
|
return nullptr;
|
|
}
|
|
|
|
int base = GetRandomInt(0, K_LEN(pg_nodes));
|
|
|
|
for (int i = 0; i < K_LEN(pg_nodes); i++) {
|
|
int idx = (base + i) % K_LEN(pg_nodes);
|
|
|
|
ProgressNode *node = &pg_nodes[idx];
|
|
bool used = node->used.exchange(true);
|
|
|
|
if (!used) {
|
|
static_assert(K_SIZE(text) == K_SIZE(node->front.text));
|
|
MemCpy(node->front.text, text, K_SIZE(text));
|
|
|
|
ProgressNode *prev = nullptr;
|
|
bool set = this->node.compare_exchange_strong(prev, node);
|
|
|
|
if (set) {
|
|
return node;
|
|
} else {
|
|
node->used = false;
|
|
pg_count--;
|
|
|
|
return prev;
|
|
}
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void ProgressHandle::CopyText(Span<const char> text, char out[K_PROGRESS_TEXT_SIZE])
|
|
{
|
|
Span<char> buf = MakeSpan(out, K_PROGRESS_TEXT_SIZE);
|
|
bool complete = CopyString(text, buf);
|
|
|
|
if (!complete) [[unlikely]] {
|
|
out[K_PROGRESS_TEXT_SIZE - 4] = '.';
|
|
out[K_PROGRESS_TEXT_SIZE - 3] = '.';
|
|
out[K_PROGRESS_TEXT_SIZE - 2] = '.';
|
|
out[K_PROGRESS_TEXT_SIZE - 1] = 0;
|
|
}
|
|
}
|
|
|
|
void SetProgressHandler(const std::function<ProgressFunc> &func)
|
|
{
|
|
pg_handler = func;
|
|
}
|
|
|
|
void DefaultProgressHandler(Span<const ProgressInfo> bars)
|
|
{
|
|
static uint64_t frame = 0;
|
|
|
|
if (!bars.len) {
|
|
StdErr->Flush();
|
|
return;
|
|
}
|
|
|
|
Size count = bars.len;
|
|
Size rows = std::min((Size)20, bars.len);
|
|
|
|
bars = bars.Take(0, rows);
|
|
|
|
if (StdErr->IsVt100()) {
|
|
// Don't blow up stack size
|
|
static LocalArray<char, 65536> buf;
|
|
buf.Clear();
|
|
|
|
for (const ProgressInfo &bar: bars) {
|
|
if (bar.determinate) {
|
|
int64_t range = bar.max - bar.min;
|
|
int64_t delta = bar.value - bar.min;
|
|
|
|
int progress = (int)(100 * delta / range);
|
|
int size = progress / 4;
|
|
|
|
buf.len += Fmt(buf.TakeAvailable(), true, "%!..+[%1%2]%!0 %3\n", FmtRepeat("=", size), FmtRepeat(" ", 25 - size), bar.text).len;
|
|
} else {
|
|
int progress = (int)(frame % 44);
|
|
int before = (progress > 22) ? (44 - progress) : progress;
|
|
int after = std::max(22 - before, 0);
|
|
|
|
buf.len += Fmt(buf.TakeAvailable(), true, "%!..+[%1===%2]%!0 %3\n", FmtRepeat(" ", before), FmtRepeat(" ", after), bar.text).len;
|
|
}
|
|
}
|
|
|
|
if (count > bars.len) {
|
|
buf.len += Fmt(buf.TakeAvailable(), true, "%!D..... and %1 more tasks%!0\n", count - bars.len).len;
|
|
rows++;
|
|
}
|
|
buf.len--;
|
|
|
|
StdErr->Write(buf);
|
|
StdErr->Flush();
|
|
|
|
if (rows > 1) {
|
|
Print(StdErr, "\r\x1B[%1F\x1B[%2M", rows - 1, rows);
|
|
} else {
|
|
Print(StdErr, "\r\x1B[%1M", rows);
|
|
}
|
|
} else {
|
|
for (const ProgressInfo &bar: bars) {
|
|
PrintLn(StdErr, "%1", bar.text);
|
|
}
|
|
}
|
|
|
|
frame++;
|
|
}
|
|
|
|
#endif
|
|
|
|
// ------------------------------------------------------------------------
|
|
// System
|
|
// ------------------------------------------------------------------------
|
|
|
|
#if defined(_WIN32)
|
|
|
|
static bool win32_utf8 = (GetACP() == CP_UTF8);
|
|
|
|
bool IsWin32Utf8()
|
|
{
|
|
return win32_utf8;
|
|
}
|
|
|
|
Size ConvertUtf8ToWin32Wide(Span<const char> str, Span<wchar_t> out_str_w)
|
|
{
|
|
if (!out_str_w.len) {
|
|
LogError("Output buffer is too small");
|
|
return -1;
|
|
}
|
|
|
|
if (!str.len) {
|
|
out_str_w[0] = 0;
|
|
return 0;
|
|
} else if (out_str_w.len == 1) {
|
|
LogError("Output buffer is too small");
|
|
return -1;
|
|
}
|
|
|
|
int len = MultiByteToWideChar(CP_UTF8, 0, str.ptr, (int)str.len, out_str_w.ptr, (int)out_str_w.len - 1);
|
|
if (!len) {
|
|
switch (GetLastError()) {
|
|
case ERROR_INSUFFICIENT_BUFFER: { LogError("String '%1' is too large", str); } break;
|
|
case ERROR_NO_UNICODE_TRANSLATION: { LogError("String '%1' is not valid UTF-8", str); } break;
|
|
default: { LogError("MultiByteToWideChar() failed: %1", GetWin32ErrorString()); } break;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// MultiByteToWideChar() does not NUL terminate when passed in explicit string length
|
|
out_str_w.ptr[len] = 0;
|
|
|
|
return (Size)len;
|
|
}
|
|
|
|
Size ConvertWin32WideToUtf8(LPCWSTR str_w, Span<char> out_str)
|
|
{
|
|
if (!out_str.len) {
|
|
LogError("Output buffer is too small");
|
|
return -1;
|
|
}
|
|
|
|
int len = WideCharToMultiByte(CP_UTF8, 0, str_w, -1, out_str.ptr, (int)out_str.len - 1, nullptr, nullptr);
|
|
if (!len) {
|
|
switch (GetLastError()) {
|
|
case ERROR_INSUFFICIENT_BUFFER: { LogError("Cannot convert UTF-16 string to UTF-8: too large"); } break;
|
|
case ERROR_NO_UNICODE_TRANSLATION: { LogError("Cannot convert invalid UTF-16 string to UTF-8"); } break;
|
|
default: { LogError("WideCharToMultiByte() failed: %1", GetWin32ErrorString()); } break;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
return (Size)len - 1;
|
|
}
|
|
|
|
char *GetWin32ErrorString(uint32_t error_code)
|
|
{
|
|
static thread_local char str_buf[512];
|
|
|
|
if (error_code == UINT32_MAX) {
|
|
error_code = GetLastError();
|
|
}
|
|
|
|
if (win32_utf8) {
|
|
if (!FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
|
nullptr, error_code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
|
|
str_buf, K_SIZE(str_buf), nullptr))
|
|
goto fail;
|
|
} else {
|
|
wchar_t buf_w[256];
|
|
if (!FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
|
nullptr, error_code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
|
|
buf_w, K_SIZE(buf_w), nullptr))
|
|
goto fail;
|
|
|
|
if (!WideCharToMultiByte(CP_UTF8, 0, buf_w, -1, str_buf, K_SIZE(str_buf), nullptr, nullptr))
|
|
goto fail;
|
|
}
|
|
|
|
// Truncate newlines
|
|
{
|
|
char *str_end = str_buf + strlen(str_buf);
|
|
while (str_end > str_buf && (str_end[-1] == '\n' || str_end[-1] == '\r'))
|
|
str_end--;
|
|
*str_end = 0;
|
|
}
|
|
|
|
return str_buf;
|
|
|
|
fail:
|
|
sprintf(str_buf, "Win32 error 0x%x", error_code);
|
|
return str_buf;
|
|
}
|
|
|
|
static inline FileType FileAttributesToType(uint32_t attr)
|
|
{
|
|
if (attr & FILE_ATTRIBUTE_DIRECTORY) {
|
|
return FileType::Directory;
|
|
} else if (attr & FILE_ATTRIBUTE_DEVICE) {
|
|
return FileType::Device;
|
|
} else {
|
|
return FileType::File;
|
|
}
|
|
}
|
|
|
|
static StatResult StatHandle(HANDLE h, const char *filename, FileInfo *out_info)
|
|
{
|
|
BY_HANDLE_FILE_INFORMATION attr;
|
|
if (!GetFileInformationByHandle(h, &attr)) {
|
|
LogError("Cannot stat file '%1': %2", filename, GetWin32ErrorString());
|
|
return StatResult::OtherError;
|
|
}
|
|
|
|
out_info->type = FileAttributesToType(attr.dwFileAttributes);
|
|
out_info->size = ((uint64_t)attr.nFileSizeHigh << 32) | attr.nFileSizeLow;
|
|
out_info->mtime = FileTimeToUnixTime(attr.ftLastWriteTime);
|
|
out_info->ctime = FileTimeToUnixTime(attr.ftCreationTime);
|
|
out_info->atime = FileTimeToUnixTime(attr.ftLastAccessTime);
|
|
out_info->btime = out_info->ctime;
|
|
out_info->mode = (out_info->type == FileType::Directory) ? 0755 : 0644;
|
|
out_info->uid = 0;
|
|
out_info->gid = 0;
|
|
|
|
return StatResult::Success;
|
|
}
|
|
|
|
StatResult StatFile(int fd, const char *filename, unsigned int flags, FileInfo *out_info)
|
|
{
|
|
// We don't detect symbolic links, but since they are much less of a hazard
|
|
// than on POSIX systems we care a lot less about them.
|
|
|
|
if (fd < 0) {
|
|
HANDLE h;
|
|
if (win32_utf8) {
|
|
h = CreateFileA(filename, 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
|
nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr);
|
|
} else {
|
|
wchar_t filename_w[4096];
|
|
if (ConvertUtf8ToWin32Wide(filename, filename_w) < 0)
|
|
return StatResult::OtherError;
|
|
|
|
h = CreateFileW(filename_w, 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
|
nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr);
|
|
}
|
|
if (h == INVALID_HANDLE_VALUE) {
|
|
DWORD err = GetLastError();
|
|
|
|
switch (err) {
|
|
case ERROR_FILE_NOT_FOUND:
|
|
case ERROR_PATH_NOT_FOUND: {
|
|
if (!(flags & (int)StatFlag::SilentMissing)) {
|
|
LogError("Cannot stat file '%1': %2", filename, GetWin32ErrorString(err));
|
|
}
|
|
return StatResult::MissingPath;
|
|
} break;
|
|
case ERROR_ACCESS_DENIED: {
|
|
LogError("Cannot stat file '%1': %2", filename, GetWin32ErrorString(err));
|
|
return StatResult::AccessDenied;
|
|
}
|
|
default: {
|
|
LogError("Cannot stat file '%1': %2", filename, GetWin32ErrorString(err));
|
|
return StatResult::OtherError;
|
|
} break;
|
|
}
|
|
}
|
|
K_DEFER { CloseHandle(h); };
|
|
|
|
return StatHandle(h, filename, out_info);
|
|
} else {
|
|
HANDLE h = (HANDLE)_get_osfhandle(fd);
|
|
return StatHandle(h, filename, out_info);
|
|
}
|
|
}
|
|
|
|
RenameResult RenameFile(const char *src_filename, const char *dest_filename, unsigned int silent, unsigned int flags)
|
|
{
|
|
K_ASSERT(!(silent & ((int)RenameResult::Success | (int)RenameResult::OtherError)));
|
|
|
|
DWORD move_flags = (flags & (int)RenameFlag::Overwrite) ? MOVEFILE_REPLACE_EXISTING : 0;
|
|
DWORD err = ERROR_SUCCESS;
|
|
|
|
for (int i = 0; i < 10; i++) {
|
|
if (win32_utf8) {
|
|
if (MoveFileExA(src_filename, dest_filename, move_flags))
|
|
return RenameResult::Success;
|
|
} else {
|
|
wchar_t src_filename_w[4096];
|
|
wchar_t dest_filename_w[4096];
|
|
if (ConvertUtf8ToWin32Wide(src_filename, src_filename_w) < 0)
|
|
return RenameResult::OtherError;
|
|
if (ConvertUtf8ToWin32Wide(dest_filename, dest_filename_w) < 0)
|
|
return RenameResult::OtherError;
|
|
|
|
if (MoveFileExW(src_filename_w, dest_filename_w, move_flags))
|
|
return RenameResult::Success;
|
|
}
|
|
|
|
err = GetLastError();
|
|
|
|
// If two threads are trying to rename to the same destination or the FS is
|
|
// very busy, we get spurious ERROR_ACCESS_DENIED errors. Wait a bit and retry :)
|
|
if (err != ERROR_ACCESS_DENIED)
|
|
break;
|
|
|
|
Sleep(1);
|
|
}
|
|
|
|
if (err == ERROR_ALREADY_EXISTS) {
|
|
if (!(silent & (int)RenameResult::AlreadyExists)) {
|
|
LogError("Failed to rename '%1' to '%2': file already exists", src_filename, dest_filename);
|
|
}
|
|
return RenameResult::AlreadyExists;
|
|
} else {
|
|
LogError("Failed to rename '%1' to '%2': %3", src_filename, dest_filename, GetWin32ErrorString(err));
|
|
return RenameResult::OtherError;
|
|
}
|
|
}
|
|
|
|
bool ResizeFile(int fd, const char *filename, int64_t len)
|
|
{
|
|
HANDLE h = (HANDLE)_get_osfhandle(fd);
|
|
|
|
LARGE_INTEGER prev_pos = {};
|
|
if (!SetFilePointerEx(h, prev_pos, &prev_pos, FILE_CURRENT)) {
|
|
LogError("Failed to resize file '%1': %2", filename, GetWin32ErrorString());
|
|
return false;
|
|
}
|
|
K_DEFER { SetFilePointerEx(h, prev_pos, nullptr, FILE_BEGIN); };
|
|
|
|
if (!SetFilePointerEx(h, { .QuadPart = len }, nullptr, FILE_BEGIN)) {
|
|
LogError("Failed to resize file '%1': %2", filename, GetWin32ErrorString());
|
|
return false;
|
|
}
|
|
if (!SetEndOfFile(h)) {
|
|
LogError("Failed to resize file '%1': %2", filename, GetWin32ErrorString());
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SetFileTimes(int fd, const char *filename, int64_t mtime, int64_t btime)
|
|
{
|
|
HANDLE h = (HANDLE)_get_osfhandle(fd);
|
|
|
|
FILETIME mft = UnixTimeToFileTime(mtime);
|
|
FILETIME bft = UnixTimeToFileTime(btime);
|
|
|
|
if (!SetFileTime(h, &bft, nullptr, &mft)) {
|
|
LogError("Failed to set modification time of '%1': %2", filename, GetWin32ErrorString());
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool GetVolumeInfo(const char *dirname, VolumeInfo *out_volume)
|
|
{
|
|
ULARGE_INTEGER available;
|
|
ULARGE_INTEGER total;
|
|
|
|
if (win32_utf8) {
|
|
if (!GetDiskFreeSpaceExA(dirname, &available, &total, nullptr)) {
|
|
LogError("Cannot get volume information for '%1': %2", dirname, GetWin32ErrorString());
|
|
return false;
|
|
}
|
|
} else {
|
|
wchar_t dirname_w[4096];
|
|
if (ConvertUtf8ToWin32Wide(dirname, dirname_w) < 0)
|
|
return false;
|
|
|
|
if (!GetDiskFreeSpaceExW(dirname_w, &available, &total, nullptr)) {
|
|
LogError("Cannot get volume information for '%1': %2", dirname, GetWin32ErrorString());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
out_volume->total = (int64_t)total.QuadPart;
|
|
out_volume->available = (int64_t)available.QuadPart;
|
|
|
|
return true;
|
|
}
|
|
|
|
EnumResult EnumerateDirectory(const char *dirname, const char *filter, Size max_files,
|
|
FunctionRef<bool(const char *, FileType)> func)
|
|
{
|
|
EnumResult ret = EnumerateDirectory(dirname, filter, max_files,
|
|
[&](const char *basename, const FileInfo &file_info) {
|
|
return func(basename, file_info.type);
|
|
});
|
|
|
|
return ret;
|
|
}
|
|
|
|
EnumResult EnumerateDirectory(const char *dirname, const char *filter, Size max_files,
|
|
FunctionRef<bool(const char *, const FileInfo &)> func)
|
|
{
|
|
if (filter) {
|
|
K_ASSERT(!strpbrk(filter, K_PATH_SEPARATORS));
|
|
} else {
|
|
filter = "*";
|
|
}
|
|
|
|
wchar_t find_filter_w[4096];
|
|
{
|
|
char find_filter[4096];
|
|
if (snprintf(find_filter, K_SIZE(find_filter), "%s\\%s", dirname, filter) >= K_SIZE(find_filter)) {
|
|
LogError("Cannot enumerate directory '%1': Path too long", dirname);
|
|
return EnumResult::OtherError;
|
|
}
|
|
|
|
if (ConvertUtf8ToWin32Wide(find_filter, find_filter_w) < 0)
|
|
return EnumResult::OtherError;
|
|
}
|
|
|
|
WIN32_FIND_DATAW attr;
|
|
HANDLE handle = FindFirstFileExW(find_filter_w, FindExInfoBasic, &attr,
|
|
FindExSearchNameMatch, nullptr, FIND_FIRST_EX_LARGE_FETCH);
|
|
if (handle == INVALID_HANDLE_VALUE) {
|
|
DWORD err = GetLastError();
|
|
|
|
if (err == ERROR_FILE_NOT_FOUND) {
|
|
// Erase the filter part from the buffer, we are about to exit anyway.
|
|
// And no, I don't want to include wchar.h
|
|
Size len = 0;
|
|
while (find_filter_w[len++]);
|
|
while (len > 0 && find_filter_w[--len] != L'\\');
|
|
find_filter_w[len] = 0;
|
|
|
|
DWORD attrib = GetFileAttributesW(find_filter_w);
|
|
if (attrib != INVALID_FILE_ATTRIBUTES && (attrib & FILE_ATTRIBUTE_DIRECTORY))
|
|
return EnumResult::Success;
|
|
}
|
|
|
|
LogError("Cannot enumerate directory '%1': %2", dirname, GetWin32ErrorString());
|
|
|
|
switch (err) {
|
|
case ERROR_FILE_NOT_FOUND:
|
|
case ERROR_PATH_NOT_FOUND: return EnumResult::MissingPath;
|
|
case ERROR_ACCESS_DENIED: return EnumResult::AccessDenied;
|
|
default: return EnumResult::OtherError;
|
|
}
|
|
}
|
|
K_DEFER { FindClose(handle); };
|
|
|
|
Size count = 0;
|
|
do {
|
|
if ((attr.cFileName[0] == '.' && !attr.cFileName[1]) ||
|
|
(attr.cFileName[0] == '.' && attr.cFileName[1] == '.' && !attr.cFileName[2]))
|
|
continue;
|
|
|
|
if (count++ >= max_files && max_files >= 0) [[unlikely]] {
|
|
LogError("Partial enumation of directory '%1'", dirname);
|
|
return EnumResult::PartialEnum;
|
|
}
|
|
|
|
char filename[512];
|
|
if (ConvertWin32WideToUtf8(attr.cFileName, filename) < 0)
|
|
return EnumResult::OtherError;
|
|
|
|
FileInfo file_info = {};
|
|
|
|
file_info.type = FileAttributesToType(attr.dwFileAttributes);
|
|
file_info.size = ((uint64_t)attr.nFileSizeHigh << 32) | attr.nFileSizeLow;
|
|
file_info.mtime = FileTimeToUnixTime(attr.ftLastWriteTime);
|
|
file_info.btime = FileTimeToUnixTime(attr.ftCreationTime);
|
|
file_info.mode = (file_info.type == FileType::Directory) ? 0755 : 0644;
|
|
file_info.uid = 0;
|
|
file_info.gid = 0;
|
|
|
|
if (!func(filename, file_info))
|
|
return EnumResult::CallbackFail;
|
|
} while (FindNextFileW(handle, &attr));
|
|
|
|
if (GetLastError() != ERROR_NO_MORE_FILES) {
|
|
LogError("Error while enumerating directory '%1': %2", dirname,
|
|
GetWin32ErrorString());
|
|
return EnumResult::OtherError;
|
|
}
|
|
|
|
return EnumResult::Success;
|
|
}
|
|
|
|
#else
|
|
|
|
static FileType FileModeToType(mode_t mode)
|
|
{
|
|
if (S_ISDIR(mode)) {
|
|
return FileType::Directory;
|
|
} else if (S_ISREG(mode)) {
|
|
return FileType::File;
|
|
} else if (S_ISBLK(mode) || S_ISCHR(mode)) {
|
|
return FileType::Device;
|
|
} else if (S_ISLNK(mode)) {
|
|
return FileType::Link;
|
|
} else if (S_ISFIFO(mode)) {
|
|
return FileType::Pipe;
|
|
} else if (S_ISSOCK(mode)) {
|
|
return FileType::Socket;
|
|
} else {
|
|
// This... should not happen. But who knows?
|
|
return FileType::File;
|
|
}
|
|
}
|
|
|
|
static StatResult StatAt(int fd, bool fd_is_directory, const char *filename, unsigned int flags, FileInfo *out_info)
|
|
{
|
|
#if defined(__linux__) && defined(STATX_TYPE) && !defined(CORE_NO_STATX)
|
|
const char *pathname = filename;
|
|
int stat_flags = (flags & (int)StatFlag::FollowSymlink) ? 0 : AT_SYMLINK_NOFOLLOW;
|
|
int stat_mask = STATX_TYPE | STATX_MODE | STATX_MTIME | STATX_BTIME | STATX_SIZE;
|
|
|
|
if (!fd_is_directory) {
|
|
if (fd >= 0) {
|
|
pathname = "";
|
|
stat_flags |= AT_EMPTY_PATH;
|
|
} else {
|
|
fd = AT_FDCWD;
|
|
}
|
|
}
|
|
|
|
struct statx sxb;
|
|
if (statx(fd, pathname, stat_flags, stat_mask, &sxb) < 0) {
|
|
switch (errno) {
|
|
case ENOENT: {
|
|
if (!(flags & (int)StatFlag::SilentMissing)) {
|
|
LogError("Cannot stat '%1': %2", filename, strerror(errno));
|
|
}
|
|
return StatResult::MissingPath;
|
|
} break;
|
|
case EACCES: {
|
|
LogError("Cannot stat '%1': %2", filename, strerror(errno));
|
|
return StatResult::AccessDenied;
|
|
} break;
|
|
case ENOTDIR: {
|
|
LogError("Cannot stat '%1': Component is not a directory", filename);
|
|
return StatResult::OtherError;
|
|
} break;
|
|
default: {
|
|
LogError("Cannot stat '%1': %2", filename, strerror(errno));
|
|
return StatResult::OtherError;
|
|
} break;
|
|
}
|
|
}
|
|
|
|
out_info->type = FileModeToType(sxb.stx_mode);
|
|
out_info->size = (int64_t)sxb.stx_size;
|
|
out_info->mtime = (int64_t)sxb.stx_mtime.tv_sec * 1000 +
|
|
(int64_t)sxb.stx_mtime.tv_nsec / 1000000;
|
|
out_info->ctime = (int64_t)sxb.stx_ctime.tv_sec * 1000 +
|
|
(int64_t)sxb.stx_ctime.tv_nsec / 1000000;
|
|
out_info->atime = (int64_t)sxb.stx_atime.tv_sec * 1000 +
|
|
(int64_t)sxb.stx_atime.tv_nsec / 1000000;
|
|
if (sxb.stx_mask & STATX_BTIME) {
|
|
out_info->btime = (int64_t)sxb.stx_btime.tv_sec * 1000 +
|
|
(int64_t)sxb.stx_btime.tv_nsec / 1000000;
|
|
} else {
|
|
out_info->btime = out_info->mtime;
|
|
}
|
|
out_info->mode = (unsigned int)sxb.stx_mode & ~S_IFMT;
|
|
out_info->uid = sxb.stx_uid;
|
|
out_info->gid = sxb.stx_gid;
|
|
#else
|
|
if (fd < 0) {
|
|
fd_is_directory = true;
|
|
fd = AT_FDCWD;
|
|
}
|
|
|
|
struct stat sb;
|
|
int ret = 0;
|
|
|
|
if (fd_is_directory) {
|
|
int stat_flags = (flags & (int)StatFlag::FollowSymlink) ? 0 : AT_SYMLINK_NOFOLLOW;
|
|
ret = fstatat(fd, filename, &sb, stat_flags);
|
|
} else {
|
|
ret = fstat(fd, &sb);
|
|
}
|
|
|
|
if (ret < 0) {
|
|
switch (errno) {
|
|
case ENOENT: {
|
|
if (!(flags & (int)StatFlag::SilentMissing)) {
|
|
LogError("Cannot stat '%1': %2", filename, strerror(errno));
|
|
}
|
|
return StatResult::MissingPath;
|
|
} break;
|
|
case EACCES: {
|
|
LogError("Cannot stat '%1': %2", filename, strerror(errno));
|
|
return StatResult::AccessDenied;
|
|
} break;
|
|
case ENOTDIR: {
|
|
LogError("Cannot stat '%1': Component is not a directory", filename);
|
|
return StatResult::OtherError;
|
|
} break;
|
|
default: {
|
|
LogError("Cannot stat '%1': %2", filename, strerror(errno));
|
|
return StatResult::OtherError;
|
|
} break;
|
|
}
|
|
}
|
|
|
|
out_info->type = FileModeToType(sb.st_mode);
|
|
out_info->size = (int64_t)sb.st_size;
|
|
#if defined(__linux__)
|
|
out_info->mtime = (int64_t)sb.st_mtim.tv_sec * 1000 +
|
|
(int64_t)sb.st_mtim.tv_nsec / 1000000;
|
|
out_info->ctime = (int64_t)sb.st_ctim.tv_sec * 1000 +
|
|
(int64_t)sb.st_ctim.tv_nsec / 1000000;
|
|
out_info->atime = (int64_t)sb.st_atim.tv_sec * 1000 +
|
|
(int64_t)sb.st_atim.tv_nsec / 1000000;
|
|
out_info->btime = out_info->mtime;
|
|
#elif defined(__APPLE__)
|
|
out_info->mtime = (int64_t)sb.st_mtimespec.tv_sec * 1000 +
|
|
(int64_t)sb.st_mtimespec.tv_nsec / 1000000;
|
|
out_info->ctime = (int64_t)sb.st_ctimespec.tv_sec * 1000 +
|
|
(int64_t)sb.st_ctimespec.tv_nsec / 1000000;
|
|
out_info->atime = (int64_t)sb.st_atimespec.tv_sec * 1000 +
|
|
(int64_t)sb.st_atimespec.tv_nsec / 1000000;
|
|
out_info->btime = (int64_t)sb.st_birthtimespec.tv_sec * 1000 +
|
|
(int64_t)sb.st_birthtimespec.tv_nsec / 1000000;
|
|
#elif defined(__OpenBSD__)
|
|
out_info->mtime = (int64_t)sb.st_mtim.tv_sec * 1000 +
|
|
(int64_t)sb.st_mtim.tv_nsec / 1000000;
|
|
out_info->ctime = (int64_t)sb.st_ctim.tv_sec * 1000 +
|
|
(int64_t)sb.st_ctim.tv_nsec / 1000000;
|
|
out_info->atime = (int64_t)sb.st_atim.tv_sec * 1000 +
|
|
(int64_t)sb.st_atim.tv_nsec / 1000000;
|
|
out_info->btime = (int64_t)sb.__st_birthtim.tv_sec * 1000 +
|
|
(int64_t)sb.__st_birthtim.tv_nsec / 1000000;
|
|
#elif defined(__FreeBSD__)
|
|
out_info->mtime = (int64_t)sb.st_mtim.tv_sec * 1000 +
|
|
(int64_t)sb.st_mtim.tv_nsec / 1000000;
|
|
out_info->ctime = (int64_t)sb.st_ctim.tv_sec * 1000 +
|
|
(int64_t)sb.st_ctim.tv_nsec / 1000000;
|
|
out_info->atime = (int64_t)sb.st_atim.tv_sec * 1000 +
|
|
(int64_t)sb.st_atim.tv_nsec / 1000000;
|
|
out_info->btime = (int64_t)sb.st_birthtim.tv_sec * 1000 +
|
|
(int64_t)sb.st_birthtim.tv_nsec / 1000000;
|
|
#else
|
|
out_info->mtime = (int64_t)sb.st_mtim.tv_sec * 1000 +
|
|
(int64_t)sb.st_mtim.tv_nsec / 1000000;
|
|
out_info->ctime = (int64_t)sb.st_ctim.tv_sec * 1000 +
|
|
(int64_t)sb.st_ctim.tv_nsec / 1000000;
|
|
out_info->atime = (int64_t)sb.st_atim.tv_sec * 1000 +
|
|
(int64_t)sb.st_atim.tv_nsec / 1000000;
|
|
out_info->btime = out_info->mtime;
|
|
#endif
|
|
out_info->mode = (unsigned int)sb.st_mode;
|
|
out_info->uid = (uint32_t)sb.st_uid;
|
|
out_info->gid = (uint32_t)sb.st_gid;
|
|
#endif
|
|
|
|
return StatResult::Success;
|
|
}
|
|
|
|
StatResult StatFile(int fd, const char *path, unsigned int flags, FileInfo *out_info)
|
|
{
|
|
return StatAt(fd, false, path, flags, out_info);
|
|
}
|
|
|
|
static bool SyncDirectory(Span<const char> directory)
|
|
{
|
|
char directory0[4096];
|
|
if (directory.len >= K_SIZE(directory0)) {
|
|
LogError("Failed to sync directory '%1': path too long", directory);
|
|
return false;
|
|
}
|
|
MemCpy(directory0, directory.ptr, directory.len);
|
|
directory0[directory.len] = 0;
|
|
|
|
int dirfd = K_RESTART_EINTR(open(directory0, O_RDONLY | O_CLOEXEC), < 0);
|
|
if (dirfd < 0) {
|
|
LogError("Failed to sync directory '%1': %2", directory, strerror(errno));
|
|
return false;
|
|
}
|
|
K_DEFER { CloseDescriptor(dirfd); };
|
|
|
|
if (fsync(dirfd) < 0) {
|
|
LogError("Failed to sync directory '%1': %2", directory, strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static inline bool IsErrnoNotSupported(int err)
|
|
{
|
|
bool unsupported = (err == ENOSYS || err == ENOTSUP || err == EOPNOTSUPP);
|
|
return unsupported;
|
|
}
|
|
|
|
RenameResult RenameFile(const char *src_filename, const char *dest_filename, unsigned int silent, unsigned int flags)
|
|
{
|
|
K_ASSERT(!(silent & ((int)RenameResult::Success | (int)RenameResult::OtherError)));
|
|
|
|
if (flags & (int)RenameFlag::Overwrite) {
|
|
if (rename(src_filename, dest_filename) < 0)
|
|
goto error;
|
|
} else {
|
|
#if defined(RENAME_NOREPLACE)
|
|
if (!renameat2(AT_FDCWD, src_filename, AT_FDCWD, dest_filename, RENAME_NOREPLACE))
|
|
goto sync;
|
|
if (!IsErrnoNotSupported(errno) && errno != EINVAL)
|
|
goto error;
|
|
#elif defined(SYS_renameat2)
|
|
{
|
|
int dirfd = AT_FDCWD;
|
|
int rflags = 1; // RENAME_NOREPLACE
|
|
|
|
if (!syscall(SYS_renameat2, dirfd, src_filename, dirfd, dest_filename, rflags))
|
|
goto sync;
|
|
if (!IsErrnoNotSupported(errno) && errno != EINVAL)
|
|
goto error;
|
|
}
|
|
#elif defined(RENAME_EXCL)
|
|
if (!renamex_np(src_filename, dest_filename, RENAME_EXCL))
|
|
goto sync;
|
|
if (!IsErrnoNotSupported(errno) && errno != EINVAL)
|
|
goto error;
|
|
#endif
|
|
|
|
// Not atomic, but not racy
|
|
if (!link(src_filename, dest_filename)) {
|
|
if (unlink(src_filename) < 0) {
|
|
unlink(dest_filename);
|
|
goto error;
|
|
}
|
|
goto sync;
|
|
}
|
|
#if defined(__linux__)
|
|
if (!IsErrnoNotSupported(errno) && errno != EINVAL && errno != EPERM)
|
|
goto error;
|
|
#else
|
|
if (!IsErrnoNotSupported(errno) && errno != EINVAL)
|
|
goto error;
|
|
#endif
|
|
|
|
// Fall back to racy way...
|
|
if (!faccessat(AT_FDCWD, dest_filename, F_OK, AT_SYMLINK_NOFOLLOW)) {
|
|
errno = EEXIST;
|
|
goto error;
|
|
}
|
|
if (errno != ENOENT)
|
|
goto error;
|
|
if (rename(src_filename, dest_filename) < 0)
|
|
goto error;
|
|
}
|
|
|
|
sync:
|
|
if (flags & (int)RenameFlag::Sync) {
|
|
Span<const char> src_directory = GetPathDirectory(src_filename);
|
|
Span<const char> dest_directory = GetPathDirectory(dest_filename);
|
|
|
|
// Not much we can do if fsync fails (I think), so ignore errors.
|
|
// Hope for the best: that's the spirit behind the POSIX filesystem API ;)
|
|
SyncDirectory(src_directory);
|
|
if (dest_directory != src_directory) {
|
|
SyncDirectory(dest_directory);
|
|
}
|
|
}
|
|
|
|
return RenameResult::Success;
|
|
|
|
error:
|
|
if (errno == EEXIST) {
|
|
if (!(silent & (int)RenameResult::AlreadyExists)) {
|
|
LogError("Failed to rename '%1' to '%2': file already exists", src_filename, dest_filename);
|
|
}
|
|
return RenameResult::AlreadyExists;
|
|
}
|
|
|
|
LogError("Failed to rename '%1' to '%2': %3", src_filename, dest_filename, strerror(errno));
|
|
return RenameResult::OtherError;
|
|
}
|
|
|
|
bool ResizeFile(int fd, const char *filename, int64_t len)
|
|
{
|
|
if (ftruncate(fd, len) < 0) {
|
|
if (errno == EINVAL) {
|
|
// Only write() calls seem to return ENOSPC, ftruncate() seems to fail with EINVAL
|
|
LogError("Failed to reserve file '%1': not enough space", filename);
|
|
} else {
|
|
LogError("Failed to reserve file '%1': %2", filename, strerror(errno));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SetFileMode(int fd, const char *filename, uint32_t mode)
|
|
{
|
|
if (fd >= 0) {
|
|
if (fchmod(fd, (mode_t)mode) < 0) {
|
|
LogError("Failed to set permissions of '%1': %2", filename, strerror(errno));
|
|
return false;
|
|
}
|
|
} else {
|
|
if (fchmodat(AT_FDCWD, filename, (mode_t)mode, AT_SYMLINK_NOFOLLOW) < 0) {
|
|
LogError("Failed to set permissions of '%1': %2", filename, strerror(errno));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SetFileOwner(int fd, const char *filename, uint32_t uid, uint32_t gid)
|
|
{
|
|
if (fd >= 0) {
|
|
if (fchown(fd, (uid_t)uid, (gid_t)gid) < 0) {
|
|
LogError("Failed to change owner of '%1': %2", filename, strerror(errno));
|
|
return false;
|
|
}
|
|
} else {
|
|
if (lchown(filename, (uid_t)uid, (gid_t)gid) < 0) {
|
|
LogError("Failed to change owner of '%1': %2", filename, strerror(errno));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SetFileTimes(int fd, const char *filename, int64_t mtime, int64_t)
|
|
{
|
|
struct timespec times[2] = {};
|
|
|
|
times[0].tv_nsec = UTIME_OMIT;
|
|
times[1].tv_sec = mtime / 1000;
|
|
times[1].tv_nsec = (mtime % 1000) * 1000000;
|
|
|
|
if (fd >= 0) {
|
|
if (futimens(fd, times) < 0) {
|
|
LogError("Failed to set modification time of '%1': %2", filename, strerror(errno));
|
|
return false;
|
|
}
|
|
} else {
|
|
if (utimensat(AT_FDCWD, filename, times, AT_SYMLINK_NOFOLLOW) < 0) {
|
|
LogError("Failed to set modification time of '%1': %2", filename, strerror(errno));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#if !defined(__wasm__)
|
|
|
|
bool GetVolumeInfo(const char *dirname, VolumeInfo *out_volume)
|
|
{
|
|
struct statvfs vfs;
|
|
if (statvfs(dirname, &vfs) < 0) {
|
|
LogError("Cannot get volume information for '%1': %2", dirname, strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
out_volume->total = (int64_t)vfs.f_blocks * vfs.f_frsize;
|
|
out_volume->available = (int64_t)vfs.f_bavail * vfs.f_frsize;
|
|
|
|
return true;
|
|
}
|
|
|
|
#endif
|
|
|
|
static EnumResult ReadDirectory(DIR *dirp, const char *dirname, const char *filter, Size max_files,
|
|
FunctionRef<bool(const char *, FileType)> func)
|
|
{
|
|
// Avoid random failure in empty directories
|
|
errno = 0;
|
|
|
|
Size count = 0;
|
|
dirent *dent;
|
|
while ((dent = readdir(dirp))) {
|
|
if ((dent->d_name[0] == '.' && !dent->d_name[1]) ||
|
|
(dent->d_name[0] == '.' && dent->d_name[1] == '.' && !dent->d_name[2]))
|
|
continue;
|
|
|
|
if (!filter || !fnmatch(filter, dent->d_name, FNM_PERIOD)) {
|
|
if (count++ >= max_files && max_files >= 0) [[unlikely]] {
|
|
LogError("Partial enumation of directory '%1'", dirname);
|
|
return EnumResult::PartialEnum;
|
|
}
|
|
|
|
FileType file_type;
|
|
#if defined(_DIRENT_HAVE_D_TYPE)
|
|
if (dent->d_type != DT_UNKNOWN) {
|
|
switch (dent->d_type) {
|
|
case DT_DIR: { file_type = FileType::Directory; } break;
|
|
case DT_REG: { file_type = FileType::File; } break;
|
|
case DT_LNK: { file_type = FileType::Link; } break;
|
|
case DT_BLK:
|
|
case DT_CHR: { file_type = FileType::Device; } break;
|
|
case DT_FIFO: { file_type = FileType::Pipe; } break;
|
|
#if !defined(__wasi__)
|
|
case DT_SOCK: { file_type = FileType::Socket; } break;
|
|
#endif
|
|
|
|
default: {
|
|
// This... should not happen. But who knows?
|
|
file_type = FileType::File;
|
|
} break;
|
|
}
|
|
} else
|
|
#endif
|
|
{
|
|
struct stat sb;
|
|
if (fstatat(dirfd(dirp), dent->d_name, &sb, AT_SYMLINK_NOFOLLOW) < 0) {
|
|
LogError("Ignoring file '%1' in '%2' (stat failed)", dent->d_name, dirname);
|
|
continue;
|
|
}
|
|
|
|
file_type = FileModeToType(sb.st_mode);
|
|
}
|
|
|
|
if (!func(dent->d_name, file_type))
|
|
return EnumResult::CallbackFail;
|
|
}
|
|
|
|
errno = 0;
|
|
}
|
|
|
|
if (errno) {
|
|
LogError("Error while enumerating directory '%1': %2", dirname, strerror(errno));
|
|
return EnumResult::OtherError;
|
|
}
|
|
|
|
return EnumResult::Success;
|
|
}
|
|
|
|
static EnumResult ReadDirectory(DIR *dirp, const char *dirname, const char *filter, Size max_files,
|
|
FunctionRef<bool(const char *, const FileInfo &)> func)
|
|
{
|
|
// Avoid random failure in empty directories
|
|
errno = 0;
|
|
|
|
Size count = 0;
|
|
dirent *dent;
|
|
while ((dent = readdir(dirp))) {
|
|
if ((dent->d_name[0] == '.' && !dent->d_name[1]) ||
|
|
(dent->d_name[0] == '.' && dent->d_name[1] == '.' && !dent->d_name[2]))
|
|
continue;
|
|
|
|
if (!filter || !fnmatch(filter, dent->d_name, FNM_PERIOD)) {
|
|
if (count++ >= max_files && max_files >= 0) [[unlikely]] {
|
|
LogError("Partial enumation of directory '%1'", dirname);
|
|
return EnumResult::PartialEnum;
|
|
}
|
|
|
|
FileInfo file_info;
|
|
StatResult ret = StatAt(dirfd(dirp), true, dent->d_name, (int)StatFlag::SilentMissing, &file_info);
|
|
|
|
if (ret == StatResult::Success && !func(dent->d_name, file_info))
|
|
return EnumResult::CallbackFail;
|
|
}
|
|
|
|
errno = 0;
|
|
}
|
|
|
|
if (errno) {
|
|
LogError("Error while enumerating directory '%1': %2", dirname, strerror(errno));
|
|
return EnumResult::OtherError;
|
|
}
|
|
|
|
return EnumResult::Success;
|
|
}
|
|
|
|
EnumResult EnumerateDirectory(const char *dirname, const char *filter, Size max_files,
|
|
FunctionRef<bool(const char *, FileType)> func)
|
|
{
|
|
DIR *dirp = K_RESTART_EINTR(opendir(dirname), == nullptr);
|
|
if (!dirp) {
|
|
LogError("Cannot enumerate directory '%1': %2", dirname, strerror(errno));
|
|
|
|
switch (errno) {
|
|
case ENOENT: return EnumResult::MissingPath;
|
|
case EACCES: return EnumResult::AccessDenied;
|
|
default: return EnumResult::OtherError;
|
|
}
|
|
}
|
|
K_DEFER { closedir(dirp); };
|
|
|
|
return ReadDirectory(dirp, dirname, filter, max_files, func);
|
|
}
|
|
|
|
EnumResult EnumerateDirectory(const char *dirname, const char *filter, Size max_files,
|
|
FunctionRef<bool(const char *, const FileInfo &)> func)
|
|
{
|
|
DIR *dirp = K_RESTART_EINTR(opendir(dirname), == nullptr);
|
|
if (!dirp) {
|
|
LogError("Cannot enumerate directory '%1': %2", dirname, strerror(errno));
|
|
|
|
switch (errno) {
|
|
case ENOENT: return EnumResult::MissingPath;
|
|
case EACCES: return EnumResult::AccessDenied;
|
|
default: return EnumResult::OtherError;
|
|
}
|
|
}
|
|
K_DEFER { closedir(dirp); };
|
|
|
|
return ReadDirectory(dirp, dirname, filter, max_files, func);
|
|
}
|
|
|
|
#if !defined(__APPLE__)
|
|
|
|
EnumResult EnumerateDirectory(int fd, const char *dirname, const char *filter, Size max_files,
|
|
FunctionRef<bool(const char *, FileType)> func)
|
|
{
|
|
DIR *dirp = fdopendir(fd);
|
|
if (!dirp) {
|
|
CloseDescriptor(fd);
|
|
|
|
LogError("Cannot enumerate directory '%1': %2", dirname, strerror(errno));
|
|
return EnumResult::OtherError;
|
|
}
|
|
K_DEFER { closedir(dirp); };
|
|
|
|
return ReadDirectory(dirp, dirname, filter, max_files, func);
|
|
}
|
|
|
|
EnumResult EnumerateDirectory(int fd, const char *dirname, const char *filter, Size max_files,
|
|
FunctionRef<bool(const char *, const FileInfo &)> func)
|
|
{
|
|
DIR *dirp = fdopendir(fd);
|
|
if (!dirp) {
|
|
CloseDescriptor(fd);
|
|
|
|
LogError("Cannot enumerate directory '%1': %2", dirname, strerror(errno));
|
|
return EnumResult::OtherError;
|
|
}
|
|
K_DEFER { closedir(dirp); };
|
|
|
|
return ReadDirectory(dirp, dirname, filter, max_files, func);
|
|
}
|
|
|
|
#endif
|
|
|
|
#endif
|
|
|
|
bool EnumerateFiles(const char *dirname, const char *filter, Size max_depth, Size max_files,
|
|
Allocator *str_alloc, HeapArray<const char *> *out_files)
|
|
{
|
|
K_DEFER_NC(out_guard, len = out_files->len) { out_files->RemoveFrom(len); };
|
|
|
|
EnumResult ret = EnumerateDirectory(dirname, nullptr, max_files,
|
|
[&](const char *basename, FileType file_type) {
|
|
switch (file_type) {
|
|
case FileType::Directory: {
|
|
if (max_depth) {
|
|
const char *sub_directory = Fmt(str_alloc, "%1%/%2", dirname, basename).ptr;
|
|
return EnumerateFiles(sub_directory, filter, std::max((Size)-1, max_depth - 1),
|
|
max_files, str_alloc, out_files);
|
|
}
|
|
} break;
|
|
|
|
case FileType::File:
|
|
case FileType::Link: {
|
|
if (!filter || MatchPathName(basename, filter)) {
|
|
const char *filename = Fmt(str_alloc, "%1%/%2", dirname, basename).ptr;
|
|
out_files->Append(filename);
|
|
}
|
|
} break;
|
|
|
|
case FileType::Device:
|
|
case FileType::Pipe:
|
|
case FileType::Socket: {} break;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
if (ret != EnumResult::Success && ret != EnumResult::PartialEnum)
|
|
return false;
|
|
|
|
out_guard.Disable();
|
|
return true;
|
|
}
|
|
|
|
bool IsDirectoryEmpty(const char *dirname)
|
|
{
|
|
EnumResult ret = EnumerateDirectory(dirname, nullptr, -1, [](const char *, FileType) { return false; });
|
|
|
|
bool empty = (ret == EnumResult::Success);
|
|
return empty;
|
|
}
|
|
|
|
bool TestFile(const char *filename)
|
|
{
|
|
FileInfo file_info;
|
|
StatResult ret = StatFile(filename, (int)StatFlag::SilentMissing, &file_info);
|
|
|
|
bool exists = (ret == StatResult::Success);
|
|
return exists;
|
|
}
|
|
|
|
bool TestFile(const char *filename, FileType type)
|
|
{
|
|
FileInfo file_info;
|
|
if (StatFile(filename, (int)StatFlag::SilentMissing, &file_info) != StatResult::Success)
|
|
return false;
|
|
|
|
// Don't follow, but don't warn if we just wanted a file
|
|
if (type != FileType::Link && file_info.type == FileType::Link) {
|
|
file_info.type = FileType::File;
|
|
}
|
|
|
|
if (type != file_info.type) {
|
|
switch (type) {
|
|
case FileType::Directory: { LogError("Path '%1' is not a directory", filename); } break;
|
|
case FileType::File: { LogError("Path '%1' is not a file", filename); } break;
|
|
case FileType::Device: { LogError("Path '%1' is not a device", filename); } break;
|
|
case FileType::Pipe: { LogError("Path '%1' is not a pipe", filename); } break;
|
|
case FileType::Socket: { LogError("Path '%1' is not a socket", filename); } break;
|
|
|
|
case FileType::Link: { K_UNREACHABLE(); } break;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool IsDirectory(const char *filename)
|
|
{
|
|
FileInfo file_info;
|
|
if (StatFile(filename, (int)StatFlag::SilentMissing, &file_info) != StatResult::Success)
|
|
return false;
|
|
return file_info.type == FileType::Directory;
|
|
}
|
|
|
|
static Size MatchPathItem(const char *path, const char *spec)
|
|
{
|
|
Size i = 0;
|
|
|
|
while (spec[i] && spec[i] != '*') {
|
|
switch (spec[i]) {
|
|
case '?': {
|
|
if (!path[i] || IsPathSeparator(path[i]))
|
|
return -1;
|
|
} break;
|
|
|
|
#if defined(_WIN32)
|
|
case '\\':
|
|
#endif
|
|
case '/': {
|
|
if (!IsPathSeparator(path[i]))
|
|
return -1;
|
|
} break;
|
|
|
|
default: {
|
|
if (path[i] != spec[i])
|
|
return -1;
|
|
} break;
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
return i;
|
|
}
|
|
|
|
static Size MatchPathItemI(const char *path, const char *spec)
|
|
{
|
|
Size i = 0;
|
|
|
|
while (spec[i] && spec[i] != '*') {
|
|
switch (spec[i]) {
|
|
case '?': {
|
|
if (!path[i] || IsPathSeparator(path[i]))
|
|
return -1;
|
|
} break;
|
|
|
|
#if defined(_WIN32)
|
|
case '\\':
|
|
#endif
|
|
case '/': {
|
|
if (!IsPathSeparator(path[i]))
|
|
return -1;
|
|
} break;
|
|
|
|
default: {
|
|
// XXX: Use proper Unicode/locale case-folding? Or is this enough?
|
|
|
|
if (LowerAscii(path[i]) != LowerAscii(spec[i]))
|
|
return -1;
|
|
} break;
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
return i;
|
|
}
|
|
|
|
bool MatchPathName(const char *path, const char *spec, bool case_sensitive)
|
|
{
|
|
auto match = case_sensitive ? MatchPathItem : MatchPathItemI;
|
|
|
|
// Match head
|
|
{
|
|
Size match_len = match(path, spec);
|
|
|
|
if (match_len < 0) {
|
|
return false;
|
|
} else {
|
|
// Fast path (no wildcard)
|
|
if (!spec[match_len])
|
|
return !path[match_len];
|
|
|
|
path += match_len;
|
|
spec += match_len;
|
|
}
|
|
}
|
|
|
|
// Find tail
|
|
const char *tail = strrchr(spec, '*') + 1;
|
|
|
|
// Match remaining items
|
|
while (spec[0] == '*') {
|
|
bool superstar = (spec[1] == '*');
|
|
while (spec[0] == '*') {
|
|
spec++;
|
|
}
|
|
|
|
for (;;) {
|
|
Size match_len = match(path, spec);
|
|
|
|
// We need to be greedy for the last wildcard, or we may not reach the tail
|
|
if (match_len < 0 || (spec == tail && path[match_len])) {
|
|
if (!path[0])
|
|
return false;
|
|
if (!superstar && IsPathSeparator(path[0]))
|
|
return false;
|
|
path++;
|
|
} else {
|
|
path += match_len;
|
|
spec += match_len;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool MatchPathSpec(const char *path, const char *spec, bool case_sensitive)
|
|
{
|
|
Span<const char> path2 = path;
|
|
|
|
do {
|
|
const char *it = SplitStrReverseAny(path2, K_PATH_SEPARATORS, &path2).ptr;
|
|
|
|
if (MatchPathName(it, spec, case_sensitive))
|
|
return true;
|
|
} while (path2.len);
|
|
|
|
return false;
|
|
}
|
|
|
|
bool FindExecutableInPath(Span<const char> paths, const char *name, Allocator *alloc, const char **out_path)
|
|
{
|
|
K_ASSERT(alloc || !out_path);
|
|
|
|
// Fast path
|
|
if (strpbrk(name, K_PATH_SEPARATORS)) {
|
|
if (!TestFile(name, FileType::File))
|
|
return false;
|
|
|
|
if (out_path) {
|
|
*out_path = DuplicateString(name, alloc).ptr;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
while (paths.len) {
|
|
Span<const char> path = SplitStr(paths, K_PATH_DELIMITER, &paths);
|
|
|
|
LocalArray<char, 4096> buf;
|
|
buf.len = Fmt(buf.data, "%1%/%2", path, name).len;
|
|
|
|
#if defined(_WIN32)
|
|
static const Span<const char> extensions[] = { ".com", ".exe", ".bat", ".cmd" };
|
|
|
|
for (Span<const char> ext: extensions) {
|
|
if (ext.len < buf.Available() - 1) [[likely]] {
|
|
MemCpy(buf.end(), ext.ptr, ext.len + 1);
|
|
|
|
if (TestFile(buf.data)) {
|
|
if (out_path) {
|
|
*out_path = DuplicateString(buf.data, alloc).ptr;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
#else
|
|
if (buf.len < K_SIZE(buf.data) - 1 && TestFile(buf.data)) {
|
|
if (out_path) {
|
|
*out_path = DuplicateString(buf.data, alloc).ptr;
|
|
}
|
|
return true;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool FindExecutableInPath(const char *name, Allocator *alloc, const char **out_path)
|
|
{
|
|
K_ASSERT(alloc || !out_path);
|
|
|
|
// Fast path
|
|
if (strpbrk(name, K_PATH_SEPARATORS)) {
|
|
if (!TestFile(name, FileType::File))
|
|
return false;
|
|
|
|
if (out_path) {
|
|
*out_path = DuplicateString(name, alloc).ptr;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
LocalArray<char, 16384> env_buf;
|
|
Span<const char> paths;
|
|
if (win32_utf8) {
|
|
paths = GetEnv("PATH");
|
|
} else {
|
|
wchar_t buf_w[K_SIZE(env_buf.data)];
|
|
DWORD len = GetEnvironmentVariableW(L"PATH", buf_w, K_LEN(buf_w));
|
|
|
|
if (!len && GetLastError() != ERROR_ENVVAR_NOT_FOUND) {
|
|
LogError("Failed to get PATH environment variable: %1", GetWin32ErrorString());
|
|
return false;
|
|
} else if (len >= K_LEN(buf_w)) {
|
|
LogError("Failed to get PATH environment variable: buffer to small");
|
|
return false;
|
|
}
|
|
buf_w[len] = 0;
|
|
|
|
env_buf.len = ConvertWin32WideToUtf8(buf_w, env_buf.data);
|
|
if (env_buf.len < 0)
|
|
return false;
|
|
|
|
paths = env_buf;
|
|
}
|
|
#else
|
|
Span<const char> paths = GetEnv("PATH");
|
|
#endif
|
|
|
|
return FindExecutableInPath(paths, name, alloc, out_path);
|
|
}
|
|
|
|
bool SetWorkingDirectory(const char *directory)
|
|
{
|
|
#if defined(_WIN32)
|
|
if (!win32_utf8) {
|
|
wchar_t directory_w[4096];
|
|
if (ConvertUtf8ToWin32Wide(directory, directory_w) < 0)
|
|
return false;
|
|
|
|
if (!SetCurrentDirectoryW(directory_w)) {
|
|
LogError("Failed to set current directory to '%1': %2", directory, GetWin32ErrorString());
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
if (chdir(directory) < 0) {
|
|
LogError("Failed to set current directory to '%1': %2", directory, strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
const char *GetWorkingDirectory()
|
|
{
|
|
static thread_local char buf[4096];
|
|
|
|
#if defined(_WIN32)
|
|
if (!win32_utf8) {
|
|
wchar_t buf_w[K_SIZE(buf)];
|
|
DWORD ret = GetCurrentDirectoryW(K_SIZE(buf_w), buf_w);
|
|
K_ASSERT(ret && ret <= K_SIZE(buf_w));
|
|
|
|
Size str_len = ConvertWin32WideToUtf8(buf_w, buf);
|
|
K_ASSERT(str_len >= 0);
|
|
|
|
return buf;
|
|
}
|
|
#endif
|
|
|
|
const char *ptr = getcwd(buf, K_SIZE(buf));
|
|
K_ASSERT(ptr);
|
|
|
|
return buf;
|
|
}
|
|
|
|
const char *GetApplicationExecutable()
|
|
{
|
|
#if defined(_WIN32)
|
|
static char executable_path[4096];
|
|
|
|
if (!executable_path[0]) {
|
|
if (win32_utf8) {
|
|
Size path_len = (Size)GetModuleFileNameA(nullptr, executable_path, K_SIZE(executable_path));
|
|
K_ASSERT(path_len && path_len < K_SIZE(executable_path));
|
|
} else {
|
|
wchar_t path_w[K_SIZE(executable_path)];
|
|
Size path_len = (Size)GetModuleFileNameW(nullptr, path_w, K_SIZE(path_w));
|
|
K_ASSERT(path_len && path_len < K_LEN(path_w));
|
|
|
|
Size str_len = ConvertWin32WideToUtf8(path_w, executable_path);
|
|
K_ASSERT(str_len >= 0);
|
|
}
|
|
}
|
|
|
|
return executable_path;
|
|
#elif defined(__APPLE__)
|
|
static char executable_path[4096];
|
|
|
|
if (!executable_path[0]) {
|
|
uint32_t buffer_size = K_SIZE(executable_path);
|
|
int ret = _NSGetExecutablePath(executable_path, &buffer_size);
|
|
K_ASSERT(!ret);
|
|
|
|
char *path_buf = realpath(executable_path, nullptr);
|
|
K_ASSERT(path_buf);
|
|
K_ASSERT(strlen(path_buf) < K_SIZE(executable_path));
|
|
|
|
CopyString(path_buf, executable_path);
|
|
free(path_buf);
|
|
}
|
|
|
|
return executable_path;
|
|
#elif defined(__linux__)
|
|
static char executable_path[4096];
|
|
|
|
if (!executable_path[0]) {
|
|
ssize_t ret = readlink("/proc/self/exe", executable_path, K_SIZE(executable_path));
|
|
K_ASSERT(ret > 0 && ret < K_SIZE(executable_path));
|
|
}
|
|
|
|
return executable_path;
|
|
#elif defined(__OpenBSD__)
|
|
static char executable_path[4096];
|
|
|
|
if (!executable_path[0]) {
|
|
int name[4] = { CTL_KERN, KERN_PROC_ARGS, getpid(), KERN_PROC_ARGV };
|
|
|
|
size_t argc;
|
|
{
|
|
int ret = sysctl(name, K_LEN(name), nullptr, &argc, nullptr, 0);
|
|
K_ASSERT(ret >= 0);
|
|
K_ASSERT(argc >= 1);
|
|
}
|
|
|
|
HeapArray<char *> argv;
|
|
{
|
|
argv.AppendDefault(argc);
|
|
int ret = sysctl(name, K_LEN(name), argv.ptr, &argc, nullptr, 0);
|
|
K_ASSERT(ret >= 0);
|
|
}
|
|
|
|
if (PathIsAbsolute(argv[0])) {
|
|
K_ASSERT(strlen(argv[0]) < K_SIZE(executable_path));
|
|
|
|
CopyString(argv[0], executable_path);
|
|
} else {
|
|
const char *path;
|
|
bool success = FindExecutableInPath(argv[0], GetDefaultAllocator(), &path);
|
|
K_ASSERT(success);
|
|
K_ASSERT(strlen(path) < K_SIZE(executable_path));
|
|
|
|
CopyString(path, executable_path);
|
|
ReleaseRaw(nullptr, (void *)path, -1);
|
|
}
|
|
}
|
|
|
|
return executable_path;
|
|
#elif defined(__FreeBSD__)
|
|
static char executable_path[4096];
|
|
|
|
if (!executable_path[0]) {
|
|
int name[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1 };
|
|
size_t len = sizeof(executable_path);
|
|
|
|
int ret = sysctl(name, K_LEN(name), executable_path, &len, nullptr, 0);
|
|
K_ASSERT(ret >= 0);
|
|
K_ASSERT(len < K_SIZE(executable_path));
|
|
}
|
|
|
|
return executable_path;
|
|
#elif defined(__wasm__)
|
|
return nullptr;
|
|
#else
|
|
#error GetApplicationExecutable() not implemented for this platform
|
|
#endif
|
|
}
|
|
|
|
const char *GetApplicationDirectory()
|
|
{
|
|
static char executable_dir[4096];
|
|
|
|
if (!executable_dir[0]) {
|
|
const char *executable_path = GetApplicationExecutable();
|
|
Size dir_len = (Size)strlen(executable_path);
|
|
while (dir_len && !IsPathSeparator(executable_path[--dir_len]));
|
|
MemCpy(executable_dir, executable_path, dir_len);
|
|
executable_dir[dir_len] = 0;
|
|
}
|
|
|
|
return executable_dir;
|
|
}
|
|
|
|
Span<const char> GetPathDirectory(Span<const char> filename)
|
|
{
|
|
Span<const char> directory;
|
|
SplitStrReverseAny(filename, K_PATH_SEPARATORS, &directory);
|
|
|
|
return directory.len ? directory : ".";
|
|
}
|
|
|
|
// Names starting with a dot are not considered to be an extension (POSIX hidden files)
|
|
Span<const char> GetPathExtension(Span<const char> filename, CompressionType *out_compression_type)
|
|
{
|
|
filename = SplitStrReverseAny(filename, K_PATH_SEPARATORS);
|
|
|
|
Span<const char> extension = {};
|
|
const auto consume_next_extension = [&]() {
|
|
Span<const char> part = SplitStrReverse(filename, '.', &filename);
|
|
|
|
if (part.ptr > filename.ptr) {
|
|
extension = MakeSpan(part.ptr - 1, part.len + 1);
|
|
} else {
|
|
extension = MakeSpan(part.end(), 0);
|
|
}
|
|
};
|
|
|
|
consume_next_extension();
|
|
|
|
if (out_compression_type) {
|
|
const char *const *it = std::find_if(std::begin(CompressionTypeExtensions), std::end(CompressionTypeExtensions),
|
|
[&](const char *it) { return it && TestStr(it, extension); });
|
|
|
|
if (it != std::end(CompressionTypeExtensions)) {
|
|
*out_compression_type = (CompressionType)(it - CompressionTypeExtensions);
|
|
consume_next_extension();
|
|
} else {
|
|
*out_compression_type = CompressionType::None;
|
|
}
|
|
}
|
|
|
|
return extension;
|
|
}
|
|
|
|
Span<char> NormalizePath(Span<const char> path, Span<const char> root_directory, unsigned int flags, Allocator *alloc)
|
|
{
|
|
K_ASSERT(alloc);
|
|
|
|
if (!path.len && !root_directory.len)
|
|
return Fmt(alloc, "");
|
|
|
|
#if !defined(_WIN32)
|
|
if (!(flags & (int)NormalizeFlag::NoExpansion)) {
|
|
Span<const char> prefix = SplitStrAny(path, K_PATH_SEPARATORS);
|
|
|
|
if (prefix == "~") {
|
|
const char *home = GetEnv("HOME");
|
|
|
|
if (home) {
|
|
root_directory = home;
|
|
path = TrimStrLeft(path.Take(1, path.len - 1), K_PATH_SEPARATORS);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
HeapArray<char> buf(alloc);
|
|
Size parts_count = 0;
|
|
|
|
char separator = (flags & (int)NormalizeFlag::ForceSlash) ? '/' : *K_PATH_SEPARATORS;
|
|
|
|
const auto append_normalized_path = [&](Span<const char> path) {
|
|
if (!buf.len && PathIsAbsolute(path)) {
|
|
Span<const char> prefix = SplitStrAny(path, K_PATH_SEPARATORS, &path);
|
|
buf.Append(prefix);
|
|
buf.Append(separator);
|
|
}
|
|
|
|
while (path.len) {
|
|
Span<const char> part = SplitStrAny(path, K_PATH_SEPARATORS, &path);
|
|
|
|
if (part == "..") {
|
|
if (parts_count) {
|
|
while (--buf.len && !IsPathSeparator(buf.ptr[buf.len - 1]));
|
|
parts_count--;
|
|
} else {
|
|
buf.Append("..");
|
|
buf.Append(separator);
|
|
}
|
|
} else if (part == ".") {
|
|
// Skip
|
|
} else if (part.len) {
|
|
buf.Append(part);
|
|
buf.Append(separator);
|
|
parts_count++;
|
|
}
|
|
}
|
|
};
|
|
|
|
if (root_directory.len && !PathIsAbsolute(path)) {
|
|
append_normalized_path(root_directory);
|
|
}
|
|
append_normalized_path(path);
|
|
|
|
if (!buf.len) {
|
|
buf.Append('.');
|
|
|
|
if (flags & (int)NormalizeFlag::EndWithSeparator) {
|
|
buf.Append(separator);
|
|
}
|
|
} else if (buf.len == 1 && IsPathSeparator(buf[0])) {
|
|
// Root '/', keep as-is or almost
|
|
buf[0] = separator;
|
|
} else if (!(flags & (int)NormalizeFlag::EndWithSeparator)) {
|
|
// Strip last separator
|
|
buf.len--;
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
if (buf.len >= 2 && IsAsciiAlpha(buf[0]) && buf[1] == ':') {
|
|
buf[0] = UpperAscii(buf[0]);
|
|
}
|
|
#endif
|
|
|
|
// NUL terminator
|
|
buf.Trim(1);
|
|
buf.ptr[buf.len] = 0;
|
|
|
|
return buf.Leak();
|
|
}
|
|
|
|
bool PathIsAbsolute(const char *path)
|
|
{
|
|
#if defined(_WIN32)
|
|
if (IsAsciiAlpha(path[0]) && path[1] == ':')
|
|
return true;
|
|
#endif
|
|
|
|
return IsPathSeparator(path[0]);
|
|
}
|
|
bool PathIsAbsolute(Span<const char> path)
|
|
{
|
|
#if defined(_WIN32)
|
|
if (path.len >= 2 && IsAsciiAlpha(path[0]) && path[1] == ':')
|
|
return true;
|
|
#endif
|
|
|
|
return path.len && IsPathSeparator(path[0]);
|
|
}
|
|
|
|
bool PathContainsDotDot(const char *path)
|
|
{
|
|
const char *ptr = path;
|
|
|
|
while ((ptr = strstr(ptr, ".."))) {
|
|
if ((ptr == path || IsPathSeparator(ptr[-1])) && (IsPathSeparator(ptr[2]) || !ptr[2]))
|
|
return true;
|
|
ptr += 2;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool PathContainsDotDot(Span<const char> path)
|
|
{
|
|
const char *ptr = path.ptr;
|
|
const char *end = path.end();
|
|
|
|
while ((ptr = (const char *)MemMem(ptr, end - ptr, "..", 2))) {
|
|
if ((ptr == path.ptr || IsPathSeparator(ptr[-1])) && (ptr + 2 == end || IsPathSeparator(ptr[2])))
|
|
return true;
|
|
ptr += 2;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool CheckForDumbTerm()
|
|
{
|
|
static bool dumb = ([]() {
|
|
const char *term = GetEnv("TERM");
|
|
|
|
if (term && TestStr(term, "dumb"))
|
|
return true;
|
|
if (GetEnv("NO_COLOR"))
|
|
return true;
|
|
|
|
return false;
|
|
})();
|
|
|
|
return dumb;
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
|
|
OpenResult OpenFile(const char *filename, unsigned int flags, unsigned int silent, int *out_fd)
|
|
{
|
|
K_ASSERT(!(silent & ((int)OpenResult::Success | (int)OpenResult::OtherError)));
|
|
|
|
DWORD access = 0;
|
|
DWORD share = 0;
|
|
DWORD creation = 0;
|
|
DWORD attributes = 0;
|
|
int oflags = -1;
|
|
switch (flags & ((int)OpenFlag::Read |
|
|
(int)OpenFlag::Write |
|
|
(int)OpenFlag::Append)) {
|
|
case (int)OpenFlag::Read: {
|
|
access = GENERIC_READ;
|
|
share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
|
|
creation = OPEN_EXISTING;
|
|
attributes = FILE_ATTRIBUTE_NORMAL;
|
|
oflags = _O_RDONLY | _O_BINARY | _O_NOINHERIT;
|
|
} break;
|
|
case (int)OpenFlag::Write: {
|
|
access = GENERIC_WRITE;
|
|
share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
|
|
creation = CREATE_ALWAYS;
|
|
attributes = FILE_ATTRIBUTE_NORMAL;
|
|
oflags = _O_WRONLY | _O_CREAT | _O_TRUNC | _O_BINARY | _O_NOINHERIT;
|
|
} break;
|
|
case (int)OpenFlag::Read | (int)OpenFlag::Write: {
|
|
access = GENERIC_READ | GENERIC_WRITE;
|
|
share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
|
|
creation = CREATE_ALWAYS;
|
|
attributes = FILE_ATTRIBUTE_NORMAL;
|
|
oflags = _O_RDWR | _O_CREAT | _O_TRUNC | _O_BINARY | _O_NOINHERIT;
|
|
} break;
|
|
case (int)OpenFlag::Append: {
|
|
access = GENERIC_WRITE;
|
|
share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
|
|
creation = OPEN_ALWAYS;
|
|
attributes = FILE_ATTRIBUTE_NORMAL;
|
|
oflags = _O_WRONLY | _O_CREAT | _O_APPEND | _O_BINARY | _O_NOINHERIT;
|
|
} break;
|
|
}
|
|
K_ASSERT(oflags >= 0);
|
|
|
|
if (flags & (int)OpenFlag::Keep) {
|
|
if (creation == CREATE_ALWAYS) {
|
|
creation = OPEN_ALWAYS;
|
|
}
|
|
oflags &= ~_O_TRUNC;
|
|
}
|
|
if (flags & (int)OpenFlag::Directory) {
|
|
K_ASSERT(!(flags & (int)OpenFlag::Exclusive));
|
|
K_ASSERT(!(flags & (int)OpenFlag::Append));
|
|
|
|
creation = OPEN_EXISTING;
|
|
attributes = FILE_FLAG_BACKUP_SEMANTICS;
|
|
oflags &= ~(_O_CREAT | _O_TRUNC | _O_BINARY);
|
|
}
|
|
if (flags & (int)OpenFlag::Exists) {
|
|
K_ASSERT(!(flags & (int)OpenFlag::Exclusive));
|
|
|
|
creation = OPEN_EXISTING;
|
|
oflags &= ~_O_CREAT;
|
|
} else if (flags & (int)OpenFlag::Exclusive) {
|
|
K_ASSERT(creation == CREATE_ALWAYS);
|
|
|
|
creation = CREATE_NEW;
|
|
oflags |= (int)_O_EXCL;
|
|
}
|
|
|
|
HANDLE h = nullptr;
|
|
int fd = -1;
|
|
K_DEFER_N(err_guard) {
|
|
CloseDescriptor(fd);
|
|
if (h) {
|
|
CloseHandle(h);
|
|
}
|
|
};
|
|
|
|
if (win32_utf8) {
|
|
h = CreateFileA(filename, access, share, nullptr, creation, attributes, nullptr);
|
|
} else {
|
|
wchar_t filename_w[4096];
|
|
if (ConvertUtf8ToWin32Wide(filename, filename_w) < 0)
|
|
return OpenResult::OtherError;
|
|
|
|
h = CreateFileW(filename_w, access, share, nullptr, creation, attributes, nullptr);
|
|
}
|
|
if (h == INVALID_HANDLE_VALUE) {
|
|
DWORD err = GetLastError();
|
|
|
|
OpenResult ret;
|
|
switch (err) {
|
|
case ERROR_FILE_NOT_FOUND:
|
|
case ERROR_PATH_NOT_FOUND: { ret = OpenResult::MissingPath; } break;
|
|
case ERROR_FILE_EXISTS: { ret = OpenResult::FileExists; } break;
|
|
case ERROR_ACCESS_DENIED: { ret = OpenResult::AccessDenied; } break;
|
|
default: { ret = OpenResult::OtherError; } break;
|
|
}
|
|
|
|
if (!(silent & (int)ret)) {
|
|
LogError("Cannot open '%1': %2", filename, GetWin32ErrorString(err));
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
fd = _open_osfhandle((intptr_t)h, oflags);
|
|
if (fd < 0) {
|
|
LogError("Cannot open '%1': %2", filename, strerror(errno));
|
|
return OpenResult::OtherError;
|
|
}
|
|
|
|
if ((flags & (int)OpenFlag::Append) && _lseeki64(fd, 0, SEEK_END) < 0) {
|
|
LogError("Cannot move file pointer: %1", strerror(errno));
|
|
return OpenResult::OtherError;
|
|
}
|
|
|
|
err_guard.Disable();
|
|
*out_fd = fd;
|
|
|
|
return OpenResult::Success;
|
|
}
|
|
|
|
void CloseDescriptor(int fd)
|
|
{
|
|
if (fd < 0)
|
|
return;
|
|
|
|
_close(fd);
|
|
}
|
|
|
|
bool FlushFile(int fd, const char *filename)
|
|
{
|
|
K_ASSERT(filename);
|
|
|
|
HANDLE h = (HANDLE)_get_osfhandle(fd);
|
|
|
|
if (!FlushFileBuffers(h)) {
|
|
DWORD err = GetLastError();
|
|
|
|
if (err != ERROR_INVALID_HANDLE) {
|
|
LogError("Failed to sync '%1': %2", filename, GetWin32ErrorString(err));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SpliceFile(int src_fd, const char *src_filename, int64_t src_offset,
|
|
int dest_fd, const char *dest_filename, int64_t dest_offset, int64_t size,
|
|
FunctionRef<void(int64_t, int64_t)> progress)
|
|
{
|
|
static NtCopyFileChunkFunc *NtCopyFileChunk =
|
|
(NtCopyFileChunkFunc *)(void *)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtCopyFileChunk");
|
|
|
|
int64_t max = size;
|
|
progress(0, max);
|
|
|
|
// Try fast kernel-mode copy introduced in Windows 11
|
|
if (NtCopyFileChunk) {
|
|
HANDLE h1 = (HANDLE)_get_osfhandle(src_fd);
|
|
HANDLE h2 = (HANDLE)_get_osfhandle(dest_fd);
|
|
|
|
LARGE_INTEGER offset0 = {};
|
|
LARGE_INTEGER offset1 = {};
|
|
|
|
offset0.QuadPart = src_offset;
|
|
offset1.QuadPart = dest_offset;
|
|
|
|
while (size) {
|
|
unsigned long count = (unsigned long)std::min(size, (int64_t)Mebibytes(64));
|
|
|
|
IO_STATUS_BLOCK iob;
|
|
LONG status = NtCopyFileChunk(h1, h2, nullptr, &iob, count, &offset0, &offset1, nullptr, nullptr, 0);
|
|
|
|
if (status) {
|
|
static RtlNtStatusToDosErrorFunc *RtlNtStatusToDosError =
|
|
(RtlNtStatusToDosErrorFunc *)(void *)GetProcAddress(GetModuleHandleA("ntdll.dll"), "RtlNtStatusToDosError");
|
|
|
|
unsigned long err = RtlNtStatusToDosError(status);
|
|
LogError("Failed to copy '%1' to '%2': %3", src_filename, dest_filename, GetWin32ErrorString(err));
|
|
|
|
return false;
|
|
}
|
|
if (!iob.Information) {
|
|
LogError("Failed to copy '%1' to '%2': Truncated file", src_filename, dest_filename);
|
|
return false;
|
|
}
|
|
|
|
offset0.QuadPart += iob.Information;
|
|
offset1.QuadPart += iob.Information;
|
|
size -= iob.Information;
|
|
|
|
progress(max - size, max);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// User-mode fallback method
|
|
{
|
|
if (_lseeki64(src_fd, src_offset, SEEK_SET) < 0) {
|
|
LogError("Failed to seek to start of '%1': %2", src_filename, strerror(errno));
|
|
return false;
|
|
}
|
|
if (_lseeki64(dest_fd, dest_offset, SEEK_SET) < 0) {
|
|
LogError("Failed to seek to start of '%1': %2", dest_filename, strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
while (size) {
|
|
LocalArray<uint8_t, 655536> buf;
|
|
unsigned long count = (unsigned long)std::min(size, (int64_t)K_SIZE(buf.data));
|
|
|
|
buf.len = _read(src_fd, buf.data, count);
|
|
|
|
if (buf.len < 0) {
|
|
if (errno == EINTR)
|
|
continue;
|
|
|
|
LogError("Failed to copy '%1' to '%2': %3", src_filename, dest_filename, strerror(errno));
|
|
return false;
|
|
}
|
|
if (!buf.len) {
|
|
LogError("Failed to copy '%1' to '%2': Truncated file", src_filename, dest_filename);
|
|
return false;
|
|
}
|
|
|
|
Span<const uint8_t> remain = buf;
|
|
|
|
do {
|
|
int written = _write(dest_fd, remain.ptr, (unsigned int)remain.len);
|
|
|
|
if (written < 0) {
|
|
LogError("Failed to copy '%1' to '%2': %3", src_filename, dest_filename, strerror(errno));
|
|
return false;
|
|
}
|
|
if (!written) {
|
|
LogError("Failed to copy '%1' to '%2': Truncated file", src_filename, dest_filename);
|
|
return false;
|
|
}
|
|
|
|
remain.ptr += written;
|
|
remain.len -= written;
|
|
} while (remain.len);
|
|
|
|
size -= buf.len;
|
|
|
|
progress(max - size, max);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
K_UNREACHABLE();
|
|
}
|
|
|
|
bool FileIsVt100(int fd)
|
|
{
|
|
static thread_local int cache_fd = -1;
|
|
static thread_local bool cache_vt100;
|
|
|
|
if (CheckForDumbTerm())
|
|
return false;
|
|
|
|
// Fast path, for repeated calls (such as Print in a loop)
|
|
if (fd == cache_fd)
|
|
return cache_vt100;
|
|
|
|
if (fd == STDOUT_FILENO || fd == STDERR_FILENO) {
|
|
HANDLE h = (HANDLE)_get_osfhandle(fd);
|
|
|
|
DWORD console_mode;
|
|
if (GetConsoleMode(h, &console_mode)) {
|
|
static bool enable_emulation = [&]() {
|
|
bool emulation = console_mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING;
|
|
|
|
if (!emulation) {
|
|
// Enable VT100 escape sequences, introduced in Windows 10
|
|
DWORD new_mode = console_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING;
|
|
emulation = SetConsoleMode(h, new_mode);
|
|
|
|
if (emulation) {
|
|
static HANDLE exit_handle = h;
|
|
static DWORD exit_mode = console_mode;
|
|
|
|
atexit([]() { SetConsoleMode(exit_handle, exit_mode); });
|
|
} else {
|
|
// Try ConEmu ANSI support for Windows < 10
|
|
const char *conemuansi_str = GetEnv("ConEmuANSI");
|
|
emulation = conemuansi_str && TestStr(conemuansi_str, "ON");
|
|
}
|
|
}
|
|
|
|
return emulation;
|
|
}();
|
|
|
|
cache_vt100 = enable_emulation;
|
|
} else {
|
|
cache_vt100 = false;
|
|
}
|
|
} else {
|
|
cache_vt100 = false;
|
|
}
|
|
|
|
cache_fd = fd;
|
|
return cache_vt100;
|
|
}
|
|
|
|
bool MakeDirectory(const char *directory, bool error_if_exists)
|
|
{
|
|
if (win32_utf8) {
|
|
if (!CreateDirectoryA(directory, nullptr))
|
|
goto error;
|
|
} else {
|
|
wchar_t directory_w[4096];
|
|
if (ConvertUtf8ToWin32Wide(directory, directory_w) < 0)
|
|
return false;
|
|
|
|
if (!CreateDirectoryW(directory_w, nullptr))
|
|
goto error;
|
|
}
|
|
|
|
return true;
|
|
|
|
error:
|
|
DWORD err = GetLastError();
|
|
|
|
if (err != ERROR_ALREADY_EXISTS || error_if_exists) {
|
|
LogError("Cannot create directory '%1': %2", directory, GetWin32ErrorString(err));
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
bool MakeDirectoryRec(Span<const char> directory)
|
|
{
|
|
LocalArray<wchar_t, 4096> buf_w;
|
|
buf_w.len = ConvertUtf8ToWin32Wide(directory, buf_w.data);
|
|
if (buf_w.len < 0)
|
|
return false;
|
|
|
|
// Simple case: directory already exists or only last level was missing
|
|
if (!CreateDirectoryW(buf_w.data, nullptr)) {
|
|
DWORD err = GetLastError();
|
|
|
|
if (err == ERROR_ALREADY_EXISTS) {
|
|
return true;
|
|
} else if (err != ERROR_PATH_NOT_FOUND) {
|
|
LogError("Cannot create directory '%1': %2", directory, strerror(errno));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
for (Size offset = 1, parts = 0; offset <= buf_w.len; offset++) {
|
|
if (!buf_w.data[offset] || buf_w[offset] == L'\\' || buf_w[offset] == L'/') {
|
|
buf_w.data[offset] = 0;
|
|
parts++;
|
|
|
|
if (!CreateDirectoryW(buf_w.data, nullptr) && GetLastError() != ERROR_ALREADY_EXISTS) {
|
|
Size offset8 = 0;
|
|
while (offset8 < directory.len) {
|
|
parts -= IsPathSeparator(directory[offset8]);
|
|
if (!parts)
|
|
break;
|
|
offset8++;
|
|
}
|
|
|
|
LogError("Cannot create directory '%1': %2",
|
|
directory.Take(0, offset8), GetWin32ErrorString());
|
|
return false;
|
|
}
|
|
|
|
buf_w.data[offset] = L'\\';
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool UnlinkDirectory(const char *directory, bool error_if_missing)
|
|
{
|
|
if (win32_utf8) {
|
|
if (!RemoveDirectoryA(directory))
|
|
goto error;
|
|
} else {
|
|
wchar_t directory_w[4096];
|
|
if (ConvertUtf8ToWin32Wide(directory, directory_w) < 0)
|
|
return false;
|
|
|
|
if (!RemoveDirectoryW(directory_w))
|
|
goto error;
|
|
}
|
|
|
|
return true;
|
|
|
|
error:
|
|
DWORD err = GetLastError();
|
|
|
|
if (err != ERROR_FILE_NOT_FOUND || error_if_missing) {
|
|
LogError("Failed to remove directory '%1': %2", directory, GetWin32ErrorString(err));
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
bool UnlinkFile(const char *filename, bool error_if_missing)
|
|
{
|
|
if (win32_utf8) {
|
|
if (!DeleteFileA(filename))
|
|
goto error;
|
|
} else {
|
|
wchar_t filename_w[4096];
|
|
if (ConvertUtf8ToWin32Wide(filename, filename_w) < 0)
|
|
return false;
|
|
|
|
if (!DeleteFileW(filename_w))
|
|
goto error;
|
|
}
|
|
|
|
return true;
|
|
|
|
error:
|
|
DWORD err = GetLastError();
|
|
|
|
if (err != ERROR_FILE_NOT_FOUND || error_if_missing) {
|
|
LogError("Failed to remove file '%1': %2", filename, GetWin32ErrorString());
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
#else
|
|
|
|
OpenResult OpenFile(const char *filename, unsigned int flags, unsigned int silent, int *out_fd)
|
|
{
|
|
K_ASSERT(!(silent & ((int)OpenResult::Success | (int)OpenResult::OtherError)));
|
|
|
|
int oflags = -1;
|
|
switch (flags & ((int)OpenFlag::Read |
|
|
(int)OpenFlag::Write |
|
|
(int)OpenFlag::Append)) {
|
|
case (int)OpenFlag::Read: { oflags = O_RDONLY | O_CLOEXEC; } break;
|
|
case (int)OpenFlag::Write: { oflags = O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC; } break;
|
|
case (int)OpenFlag::Read | (int)OpenFlag::Write: { oflags = O_RDWR | O_CREAT | O_TRUNC | O_CLOEXEC; } break;
|
|
case (int)OpenFlag::Append: { oflags = O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC; } break;
|
|
}
|
|
K_ASSERT(oflags >= 0);
|
|
|
|
if (flags & (int)OpenFlag::Keep) {
|
|
oflags &= ~O_TRUNC;
|
|
}
|
|
if (flags & (int)OpenFlag::Directory) {
|
|
K_ASSERT(!(flags & (int)OpenFlag::Exclusive));
|
|
K_ASSERT(!(flags & (int)OpenFlag::Append));
|
|
|
|
oflags &= ~(O_CREAT | O_WRONLY | O_RDWR | O_TRUNC);
|
|
}
|
|
if (flags & (int)OpenFlag::Exists) {
|
|
K_ASSERT(!(flags & (int)OpenFlag::Exclusive));
|
|
oflags &= ~O_CREAT;
|
|
} else if (flags & (int)OpenFlag::Exclusive) {
|
|
K_ASSERT(oflags & O_CREAT);
|
|
oflags |= O_EXCL;
|
|
}
|
|
if (flags & (int)OpenFlag::NoFollow) {
|
|
oflags |= O_NOFOLLOW;
|
|
}
|
|
|
|
int fd = K_RESTART_EINTR(open(filename, oflags, 0644), < 0);
|
|
if (fd < 0) {
|
|
OpenResult ret;
|
|
switch (errno) {
|
|
case ENOENT: { ret = OpenResult::MissingPath; } break;
|
|
case EEXIST: { ret = OpenResult::FileExists; } break;
|
|
case EACCES: { ret = OpenResult::AccessDenied; } break;
|
|
default: { ret = OpenResult::OtherError; } break;
|
|
}
|
|
|
|
if (!(silent & (int)ret)) {
|
|
LogError("Cannot open '%1': %2", filename, strerror(errno));
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
*out_fd = fd;
|
|
return OpenResult::Success;
|
|
}
|
|
|
|
void CloseDescriptor(int fd)
|
|
{
|
|
// We could call close() anyway, it will fail with EINVAL,
|
|
// but that leads to debugger or valgrind noise.
|
|
if (fd < 0)
|
|
return;
|
|
|
|
close(fd);
|
|
}
|
|
|
|
bool FlushFile(int fd, const char *filename)
|
|
{
|
|
K_ASSERT(filename);
|
|
|
|
#if defined(__APPLE__)
|
|
if (fsync(fd) < 0 && errno != EINVAL && errno != ENOTSUP) {
|
|
#else
|
|
if (fsync(fd) < 0 && errno != EINVAL) {
|
|
#endif
|
|
LogError("Failed to sync '%1': %2", filename, strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SpliceFile(int src_fd, const char *src_filename, int64_t src_offset,
|
|
int dest_fd, const char *dest_filename, int64_t dest_offset, int64_t size,
|
|
FunctionRef<void(int64_t, int64_t)> progress)
|
|
{
|
|
static_assert(sizeof(off_t) == 8, "This code base requires large file offsets");
|
|
|
|
int64_t max = size;
|
|
progress(0, max);
|
|
|
|
// Try copy_file_range() if available
|
|
#if defined(SYS_copy_file_range)
|
|
{
|
|
bool first = true;
|
|
|
|
while (size) {
|
|
// glibc < 2.27 doesn't define copy_file_range
|
|
|
|
size_t count = (size_t)std::min(size, (int64_t)Mebibytes(64));
|
|
ssize_t ret = syscall(SYS_copy_file_range, src_fd, (off_t *)&src_offset, dest_fd, (off_t *)&dest_offset, count, 0u);
|
|
|
|
if (ret < 0) {
|
|
if (first && errno == EXDEV)
|
|
goto xdev;
|
|
if (errno == EINTR)
|
|
continue;
|
|
|
|
LogError("Failed to copy '%1' to '%2': %3", src_filename, dest_filename, strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
first = false;
|
|
size -= ret;
|
|
|
|
progress(max - size, max);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
xdev:
|
|
#elif defined(__FreeBSD__)
|
|
{
|
|
bool first = true;
|
|
|
|
while (size) {
|
|
size_t count = (size_t)std::min(size, (int64_t)Mebibytes(64));
|
|
ssize_t ret = copy_file_range(src_fd, (off_t *)&src_offset, dest_fd, (off_t *)&dest_offset, count, 0u);
|
|
|
|
if (ret < 0) {
|
|
if (first && errno == EXDEV)
|
|
goto xdev;
|
|
if (errno == EINTR)
|
|
continue;
|
|
|
|
LogError("Failed to copy '%1' to '%2': %3", src_filename, dest_filename, strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
first = false;
|
|
size -= ret;
|
|
|
|
progress(max - size, max);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
xdev:
|
|
#endif
|
|
|
|
#if defined(__linux__)
|
|
// Try sendfile() on Linux
|
|
{
|
|
bool first = true;
|
|
|
|
if (lseek(dest_fd, dest_offset, SEEK_SET) < 0) {
|
|
LogError("Failed to seek to start of '%1': %2", dest_filename, strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
while (size) {
|
|
size_t count = (size_t)std::min(size, (int64_t)Mebibytes(64));
|
|
ssize_t ret = sendfile(dest_fd, src_fd, (off_t *)&src_offset, count);
|
|
|
|
if (ret < 0) {
|
|
if (first && errno == EINVAL)
|
|
goto unsupported;
|
|
if (errno == EINTR)
|
|
continue;
|
|
|
|
LogError("Failed to copy '%1' to '%2': %3", src_filename, dest_filename, strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
first = false;
|
|
size -= ret;
|
|
|
|
progress(max - size, max);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
unsupported:
|
|
#endif
|
|
|
|
// User-mode fallback method
|
|
{
|
|
if (lseek(src_fd, src_offset, SEEK_SET) < 0) {
|
|
LogError("Failed to seek to start of '%1': %2", src_filename, strerror(errno));
|
|
return false;
|
|
}
|
|
if (lseek(dest_fd, dest_offset, SEEK_SET) < 0) {
|
|
LogError("Failed to seek to start of '%1': %2", dest_filename, strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
while (size) {
|
|
LocalArray<uint8_t, 655536> buf;
|
|
Size count = (Size)std::min(size, (int64_t)K_SIZE(buf.data));
|
|
|
|
buf.len = read(src_fd, buf.data, (size_t)count);
|
|
|
|
if (buf.len < 0) {
|
|
if (errno == EINTR)
|
|
continue;
|
|
|
|
LogError("Failed to copy '%1' to '%2': %3", src_filename, dest_filename, strerror(errno));
|
|
return false;
|
|
}
|
|
if (!buf.len) {
|
|
LogError("Failed to copy '%1' to '%2': Truncated file");
|
|
return false;
|
|
}
|
|
|
|
Span<const uint8_t> remain = buf;
|
|
|
|
do {
|
|
ssize_t written = write(dest_fd, remain.ptr, (size_t)remain.len);
|
|
|
|
if (written < 0) {
|
|
LogError("Failed to copy '%1' to '%2': %3", src_filename, dest_filename, strerror(errno));
|
|
return false;
|
|
}
|
|
if (!written) {
|
|
LogError("Failed to copy '%1' to '%2': Truncated file", src_filename, dest_filename);
|
|
return false;
|
|
}
|
|
|
|
remain.ptr += written;
|
|
remain.len -= written;
|
|
} while (remain.len);
|
|
|
|
size -= buf.len;
|
|
|
|
progress(max - size, max);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
K_UNREACHABLE();
|
|
}
|
|
|
|
bool FileIsVt100(int fd)
|
|
{
|
|
static thread_local int cache_fd = -1;
|
|
static thread_local bool cache_vt100;
|
|
|
|
if (CheckForDumbTerm())
|
|
return false;
|
|
|
|
#if defined(__EMSCRIPTEN__)
|
|
static bool win32 = ([]() {
|
|
int win32 = EM_ASM_INT({
|
|
try {
|
|
const os = require('os');
|
|
|
|
var win32 = (os.platform() === 'win32');
|
|
return win32 ? 1 : 0;
|
|
} catch (err) {
|
|
return 0;
|
|
}
|
|
});
|
|
|
|
return (bool)win32;
|
|
})();
|
|
|
|
if (win32)
|
|
return false;
|
|
#endif
|
|
|
|
// Fast path, for repeated calls (such as Print in a loop)
|
|
if (fd == cache_fd)
|
|
return cache_vt100;
|
|
|
|
cache_fd = fd;
|
|
cache_vt100 = isatty(fd);
|
|
|
|
return cache_vt100;
|
|
}
|
|
|
|
bool MakeDirectory(const char *directory, bool error_if_exists)
|
|
{
|
|
if (mkdir(directory, 0755) < 0 && (errno != EEXIST || error_if_exists)) {
|
|
LogError("Cannot create directory '%1': %2", directory, strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool MakeDirectoryRec(Span<const char> directory)
|
|
{
|
|
char buf[4096];
|
|
if (directory.len >= K_SIZE(buf)) [[unlikely]] {
|
|
LogError("Path '%1' is too large", directory);
|
|
return false;
|
|
}
|
|
MemCpy(buf, directory.ptr, directory.len);
|
|
buf[directory.len] = 0;
|
|
|
|
// Simple case: directory already exists or only last level was missing
|
|
if (mkdir(buf, 0755) < 0) {
|
|
if (errno == EEXIST) {
|
|
return true;
|
|
} else if (errno != ENOENT) {
|
|
LogError("Cannot create directory '%1': %2", buf, strerror(errno));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
for (Size offset = 1; offset <= directory.len; offset++) {
|
|
if (!buf[offset] || IsPathSeparator(buf[offset])) {
|
|
buf[offset] = 0;
|
|
|
|
if (mkdir(buf, 0755) < 0 && errno != EEXIST) {
|
|
LogError("Cannot create directory '%1': %2", buf, strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
buf[offset] = *K_PATH_SEPARATORS;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool UnlinkDirectory(const char *directory, bool error_if_missing)
|
|
{
|
|
if (rmdir(directory) < 0 && (errno != ENOENT || error_if_missing)) {
|
|
LogError("Failed to remove directory '%1': %2", directory, strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool UnlinkFile(const char *filename, bool error_if_missing)
|
|
{
|
|
if (unlink(filename) < 0 && (errno != ENOENT || error_if_missing)) {
|
|
LogError("Failed to remove file '%1': %2", filename, strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#endif
|
|
|
|
bool EnsureDirectoryExists(const char *filename)
|
|
{
|
|
Span<const char> directory = GetPathDirectory(filename);
|
|
return MakeDirectoryRec(directory);
|
|
}
|
|
|
|
#if !defined(__wasi__)
|
|
|
|
#if defined(_WIN32)
|
|
|
|
static const DWORD main_thread = GetCurrentThreadId();
|
|
static HANDLE console_ctrl_event = CreateEvent(nullptr, TRUE, FALSE, nullptr);
|
|
static bool ignore_ctrl_event = false;
|
|
|
|
static BOOL CALLBACK ConsoleCtrlHandler(DWORD)
|
|
{
|
|
SetEvent(console_ctrl_event);
|
|
return (BOOL)ignore_ctrl_event;
|
|
}
|
|
|
|
static bool InitConsoleCtrlHandler()
|
|
{
|
|
static std::once_flag flag;
|
|
|
|
static bool success;
|
|
std::call_once(flag, []() { success = SetConsoleCtrlHandler(ConsoleCtrlHandler, TRUE); });
|
|
|
|
if (!success) {
|
|
LogError("SetConsoleCtrlHandler() failed: %1", GetWin32ErrorString());
|
|
}
|
|
return success;
|
|
}
|
|
|
|
bool CreateOverlappedPipe(bool overlap0, bool overlap1, PipeMode mode, HANDLE out_handles[2])
|
|
{
|
|
static LONG pipe_idx;
|
|
|
|
HANDLE handles[2] = {};
|
|
K_DEFER_N(handle_guard) {
|
|
CloseHandleSafe(&handles[0]);
|
|
CloseHandleSafe(&handles[1]);
|
|
};
|
|
|
|
char pipe_name[128];
|
|
do {
|
|
Fmt(pipe_name, "\\\\.\\pipe\\kcc.%1.%2",
|
|
GetCurrentProcessId(), InterlockedIncrement(&pipe_idx));
|
|
|
|
DWORD open_mode = PIPE_ACCESS_INBOUND | FILE_FLAG_FIRST_PIPE_INSTANCE | (overlap0 ? FILE_FLAG_OVERLAPPED : 0);
|
|
DWORD pipe_mode = PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS;
|
|
switch (mode) {
|
|
case PipeMode::Byte: { pipe_mode |= PIPE_TYPE_BYTE; } break;
|
|
case PipeMode::Message: { pipe_mode |= PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE; } break;
|
|
}
|
|
|
|
handles[0] = CreateNamedPipeA(pipe_name, open_mode, pipe_mode, 1, 8192, 8192, 0, nullptr);
|
|
if (!handles[0] && GetLastError() != ERROR_ACCESS_DENIED) {
|
|
LogError("Failed to create pipe: %1", GetWin32ErrorString());
|
|
return false;
|
|
}
|
|
} while (!handles[0]);
|
|
|
|
handles[1] = CreateFileA(pipe_name, GENERIC_WRITE, 0, nullptr, OPEN_EXISTING,
|
|
FILE_ATTRIBUTE_NORMAL | (overlap1 ? FILE_FLAG_OVERLAPPED : 0), nullptr);
|
|
if (handles[1] == INVALID_HANDLE_VALUE) {
|
|
LogError("Failed to create pipe: %1", GetWin32ErrorString());
|
|
return false;
|
|
}
|
|
|
|
if (mode == PipeMode::Message) {
|
|
DWORD value = PIPE_READMODE_MESSAGE;
|
|
if (!SetNamedPipeHandleState(handles[1], &value, nullptr, nullptr)) {
|
|
LogError("Failed to switch pipe to message mode: %1", GetWin32ErrorString());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
handle_guard.Disable();
|
|
out_handles[0] = handles[0];
|
|
out_handles[1] = handles[1];
|
|
return true;
|
|
}
|
|
|
|
void CloseHandleSafe(HANDLE *handle_ptr)
|
|
{
|
|
HANDLE h = *handle_ptr;
|
|
|
|
if (h && h != INVALID_HANDLE_VALUE) {
|
|
CancelIo(h);
|
|
CloseHandle(h);
|
|
}
|
|
|
|
*handle_ptr = nullptr;
|
|
}
|
|
|
|
struct PendingIO {
|
|
OVERLAPPED ov = {}; // Keep first
|
|
|
|
bool pending = false;
|
|
DWORD err = 0;
|
|
Size len = -1;
|
|
|
|
static void CALLBACK CompletionHandler(DWORD err, DWORD len, OVERLAPPED *ov)
|
|
{
|
|
PendingIO *self = (PendingIO *)ov;
|
|
|
|
self->pending = false;
|
|
self->err = err;
|
|
self->len = err ? -1 : len;
|
|
}
|
|
};
|
|
|
|
bool ExecuteCommandLine(const char *cmd_line, const ExecuteInfo &info,
|
|
FunctionRef<Span<const uint8_t>()> in_func,
|
|
FunctionRef<void(Span<uint8_t> buf)> out_func, int *out_code)
|
|
{
|
|
STARTUPINFOW si = {};
|
|
|
|
BlockAllocator temp_alloc;
|
|
|
|
// Convert command line
|
|
Span<wchar_t> cmd_line_w = AllocateSpan<wchar_t>(&temp_alloc, 2 * strlen(cmd_line) + 1);
|
|
if (ConvertUtf8ToWin32Wide(cmd_line, cmd_line_w) < 0)
|
|
return false;
|
|
|
|
// Convert work directory
|
|
Span<wchar_t> work_dir_w;
|
|
if (info.work_dir) {
|
|
work_dir_w = AllocateSpan<wchar_t>(&temp_alloc, 2 * strlen(info.work_dir) + 1);
|
|
if (ConvertUtf8ToWin32Wide(info.work_dir, work_dir_w) < 0)
|
|
return false;
|
|
} else {
|
|
work_dir_w = {};
|
|
}
|
|
|
|
// Detect CTRL+C and CTRL+BREAK events
|
|
if (!InitConsoleCtrlHandler())
|
|
return false;
|
|
|
|
// Neither GenerateConsoleCtrlEvent() or TerminateProcess() manage to fully kill Clang on
|
|
// Windows (in highly-parallel builds) after CTRL-C, with occasionnal suspended child
|
|
// processes remaining alive. Furthermore, processes killed by GenerateConsoleCtrlEvent()
|
|
// can trigger "MessageBox" errors, unless SetErrorMode() is used.
|
|
//
|
|
// TerminateJobObject() is a bit brutal, but it takes care of these issues.
|
|
HANDLE job_handle = CreateJobObject(nullptr, nullptr);
|
|
if (!job_handle) {
|
|
LogError("Failed to create job object: %1", GetWin32ErrorString());
|
|
return false;
|
|
}
|
|
K_DEFER { CloseHandleSafe(&job_handle); };
|
|
|
|
// If I die, everyone dies!
|
|
{
|
|
JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits = {};
|
|
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
|
|
|
if (!SetInformationJobObject(job_handle, JobObjectExtendedLimitInformation, &limits, K_SIZE(limits))) {
|
|
LogError("SetInformationJobObject() failed: %1", GetWin32ErrorString());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Create read pipes
|
|
HANDLE in_pipe[2] = {};
|
|
K_DEFER {
|
|
CloseHandleSafe(&in_pipe[0]);
|
|
CloseHandleSafe(&in_pipe[1]);
|
|
};
|
|
if (in_func.IsValid() && !CreateOverlappedPipe(false, true, PipeMode::Byte, in_pipe))
|
|
return false;
|
|
|
|
// Create write pipes
|
|
HANDLE out_pipe[2] = {};
|
|
K_DEFER {
|
|
CloseHandleSafe(&out_pipe[0]);
|
|
CloseHandleSafe(&out_pipe[1]);
|
|
};
|
|
if (out_func.IsValid() && !CreateOverlappedPipe(true, false, PipeMode::Byte, out_pipe))
|
|
return false;
|
|
|
|
// Prepare environment (if needed)
|
|
HeapArray<wchar_t> new_env_w;
|
|
if (info.reset_env || info.env_variables.len) {
|
|
if (!info.reset_env) {
|
|
Span<wchar_t> current_env = MakeSpan(GetEnvironmentStringsW(), 0);
|
|
|
|
do {
|
|
Size len = (Size)wcslen(current_env.end());
|
|
current_env.len += len + 1;
|
|
} while (current_env.ptr[current_env.len]);
|
|
|
|
new_env_w.Append(current_env);
|
|
}
|
|
|
|
for (const ExecuteInfo::KeyValue &kv: info.env_variables) {
|
|
Span<const char> key = kv.key;
|
|
Span<const char> value = kv.value;
|
|
|
|
Size len = 2 * (key.len + value.len + 1) + 1;
|
|
new_env_w.Grow(len);
|
|
|
|
len = ConvertUtf8ToWin32Wide(key, new_env_w.TakeAvailable());
|
|
if (len < 0) [[unlikely]]
|
|
return false;
|
|
new_env_w.len += len;
|
|
|
|
new_env_w.Append(L'=');
|
|
|
|
len = ConvertUtf8ToWin32Wide(value, new_env_w.TakeAvailable());
|
|
if (len < 0) [[unlikely]]
|
|
return false;
|
|
new_env_w.len += len;
|
|
|
|
new_env_w.Append(0);
|
|
}
|
|
|
|
new_env_w.Append(0);
|
|
}
|
|
|
|
// Start process
|
|
HANDLE process_handle;
|
|
{
|
|
K_DEFER {
|
|
CloseHandleSafe(&si.hStdInput);
|
|
CloseHandleSafe(&si.hStdOutput);
|
|
CloseHandleSafe(&si.hStdError);
|
|
};
|
|
|
|
if (in_func.IsValid() || out_func.IsValid()) {
|
|
if (!DuplicateHandle(GetCurrentProcess(), in_func.IsValid() ? in_pipe[0] : GetStdHandle(STD_INPUT_HANDLE),
|
|
GetCurrentProcess(), &si.hStdInput, 0, TRUE, DUPLICATE_SAME_ACCESS)) {
|
|
LogError("Failed to duplicate handle: %1", GetWin32ErrorString());
|
|
return false;
|
|
}
|
|
if (!DuplicateHandle(GetCurrentProcess(), out_func.IsValid() ? out_pipe[1] : GetStdHandle(STD_OUTPUT_HANDLE),
|
|
GetCurrentProcess(), &si.hStdOutput, 0, TRUE, DUPLICATE_SAME_ACCESS) ||
|
|
!DuplicateHandle(GetCurrentProcess(), out_func.IsValid() ? out_pipe[1] : GetStdHandle(STD_ERROR_HANDLE),
|
|
GetCurrentProcess(), &si.hStdError, 0, TRUE, DUPLICATE_SAME_ACCESS)) {
|
|
LogError("Failed to duplicate handle: %1", GetWin32ErrorString());
|
|
return false;
|
|
}
|
|
si.dwFlags |= STARTF_USESTDHANDLES;
|
|
}
|
|
|
|
int flags = CREATE_NEW_PROCESS_GROUP | CREATE_UNICODE_ENVIRONMENT;
|
|
|
|
PROCESS_INFORMATION pi = {};
|
|
if (!CreateProcessW(nullptr, cmd_line_w.ptr, nullptr, nullptr, TRUE, flags,
|
|
new_env_w.ptr, work_dir_w.ptr, &si, &pi)) {
|
|
LogError("Failed to start process: %1", GetWin32ErrorString());
|
|
return false;
|
|
}
|
|
if (!AssignProcessToJobObject(job_handle, pi.hProcess)) {
|
|
CloseHandleSafe(&job_handle);
|
|
}
|
|
|
|
process_handle = pi.hProcess;
|
|
CloseHandle(pi.hThread);
|
|
|
|
CloseHandleSafe(&in_pipe[0]);
|
|
CloseHandleSafe(&out_pipe[1]);
|
|
}
|
|
K_DEFER { CloseHandleSafe(&process_handle); };
|
|
|
|
// Read and write standard process streams
|
|
{
|
|
bool running = true;
|
|
|
|
PendingIO proc_in;
|
|
Span<const uint8_t> write_buf = {};
|
|
PendingIO proc_out;
|
|
uint8_t read_buf[4096];
|
|
|
|
while (running) {
|
|
// Try to write
|
|
if (in_func.IsValid() && !proc_in.pending) {
|
|
if (!proc_in.err) {
|
|
if (proc_in.len >= 0) {
|
|
write_buf.ptr += proc_in.len;
|
|
write_buf.len -= proc_in.len;
|
|
}
|
|
|
|
if (!write_buf.len) {
|
|
write_buf = in_func();
|
|
K_ASSERT(write_buf.len >= 0);
|
|
}
|
|
|
|
if (write_buf.len) {
|
|
K_ASSERT(write_buf.len < UINT_MAX);
|
|
|
|
if (!WriteFileEx(in_pipe[1], write_buf.ptr, (DWORD)write_buf.len,
|
|
&proc_in.ov, PendingIO::CompletionHandler)) {
|
|
proc_in.err = GetLastError();
|
|
}
|
|
} else {
|
|
CloseHandleSafe(&in_pipe[1]);
|
|
}
|
|
}
|
|
|
|
if (proc_in.err && proc_in.err != ERROR_BROKEN_PIPE && proc_in.err != ERROR_NO_DATA) {
|
|
LogError("Failed to write to process: %1", GetWin32ErrorString(proc_in.err));
|
|
}
|
|
|
|
proc_in.pending = true;
|
|
}
|
|
|
|
// Try to read
|
|
if (out_func.IsValid() && !proc_out.pending) {
|
|
if (!proc_out.err) {
|
|
if (proc_out.len >= 0) {
|
|
out_func(MakeSpan(read_buf, proc_out.len));
|
|
proc_out.len = -1;
|
|
}
|
|
|
|
if (proc_out.len && !ReadFileEx(out_pipe[0], read_buf, K_SIZE(read_buf), &proc_out.ov, PendingIO::CompletionHandler)) {
|
|
proc_out.err = GetLastError();
|
|
}
|
|
}
|
|
|
|
if (proc_out.err) {
|
|
if (proc_out.err != ERROR_BROKEN_PIPE && proc_out.err != ERROR_NO_DATA) {
|
|
LogError("Failed to read process output: %1", GetWin32ErrorString(proc_out.err));
|
|
}
|
|
break;
|
|
}
|
|
|
|
proc_out.pending = true;
|
|
}
|
|
|
|
running = (WaitForSingleObjectEx(console_ctrl_event, INFINITE, TRUE) != WAIT_OBJECT_0);
|
|
}
|
|
}
|
|
|
|
// Terminate any remaining I/O
|
|
CloseHandleSafe(&in_pipe[1]);
|
|
CloseHandleSafe(&out_pipe[0]);
|
|
|
|
// Wait for process exit
|
|
{
|
|
HANDLE events[2] = {
|
|
process_handle,
|
|
console_ctrl_event
|
|
};
|
|
|
|
if (WaitForMultipleObjects(K_LEN(events), events, FALSE, INFINITE) == WAIT_FAILED) {
|
|
LogError("WaitForMultipleObjects() failed: %1", GetWin32ErrorString());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Get exit code
|
|
DWORD exit_code;
|
|
if (WaitForSingleObject(console_ctrl_event, 0) == WAIT_OBJECT_0) {
|
|
TerminateJobObject(job_handle, STATUS_CONTROL_C_EXIT);
|
|
exit_code = STATUS_CONTROL_C_EXIT;
|
|
} else if (!GetExitCodeProcess(process_handle, &exit_code)) {
|
|
LogError("GetExitCodeProcess() failed: %1", GetWin32ErrorString());
|
|
return false;
|
|
}
|
|
|
|
// Mimic POSIX SIGINT
|
|
if (exit_code == STATUS_CONTROL_C_EXIT) {
|
|
exit_code = 130;
|
|
}
|
|
|
|
*out_code = (int)exit_code;
|
|
return true;
|
|
}
|
|
|
|
#else
|
|
|
|
static const pthread_t main_thread = pthread_self();
|
|
|
|
static std::atomic_bool flag_signal { false };
|
|
static std::atomic_int explicit_signal { 0 };
|
|
static std::atomic_int interrupt_pfd[2] { -1, -1 };
|
|
|
|
void SetSignalHandler(int signal, void (*func)(int), struct sigaction *prev)
|
|
{
|
|
struct sigaction action = {};
|
|
|
|
action.sa_handler = func;
|
|
sigemptyset(&action.sa_mask);
|
|
action.sa_flags = 0;
|
|
|
|
sigaction(signal, &action, prev);
|
|
}
|
|
|
|
static void DefaultSignalHandler(int signal)
|
|
{
|
|
if (pthread_self() != main_thread) {
|
|
pthread_kill(main_thread, signal);
|
|
return;
|
|
}
|
|
|
|
pid_t pid = getpid();
|
|
K_ASSERT(pid > 1);
|
|
|
|
if (int fd = interrupt_pfd[1].load(); fd >= 0) {
|
|
char dummy = 0;
|
|
K_IGNORE write(fd, &dummy, 1);
|
|
}
|
|
|
|
if (flag_signal) {
|
|
explicit_signal = signal;
|
|
} else {
|
|
int code = (signal == SIGINT) ? 130 : 1;
|
|
exit(code);
|
|
}
|
|
}
|
|
|
|
bool CreatePipe(bool block, int out_pfd[2])
|
|
{
|
|
#if defined(__APPLE__)
|
|
if (pipe(out_pfd) < 0) {
|
|
LogError("Failed to create pipe: %1", strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
if (fcntl(out_pfd[0], F_SETFD, FD_CLOEXEC) < 0 || fcntl(out_pfd[1], F_SETFD, FD_CLOEXEC) < 0) {
|
|
LogError("Failed to set FD_CLOEXEC on pipe: %1", strerror(errno));
|
|
return false;
|
|
}
|
|
if (!block) {
|
|
if (fcntl(out_pfd[0], F_SETFL, O_NONBLOCK) < 0 || fcntl(out_pfd[1], F_SETFL, O_NONBLOCK) < 0) {
|
|
LogError("Failed to set O_NONBLOCK on pipe: %1", strerror(errno));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
#else
|
|
int flags = O_CLOEXEC | (block ? 0 : O_NONBLOCK);
|
|
|
|
if (pipe2(out_pfd, flags) < 0) {
|
|
LogError("Failed to create pipe: %1", strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
void CloseDescriptorSafe(int *fd_ptr)
|
|
{
|
|
if (*fd_ptr >= 0) {
|
|
close(*fd_ptr);
|
|
}
|
|
|
|
*fd_ptr = -1;
|
|
}
|
|
|
|
static void InitInterruptPipe()
|
|
{
|
|
static bool success = ([]() {
|
|
static int pfd[2];
|
|
|
|
if (!CreatePipe(false, pfd))
|
|
return false;
|
|
|
|
atexit([]() {
|
|
CloseDescriptor(pfd[0]);
|
|
CloseDescriptor(pfd[1]);
|
|
});
|
|
|
|
interrupt_pfd[0] = pfd[0];
|
|
interrupt_pfd[1] = pfd[1];
|
|
|
|
return true;
|
|
})();
|
|
|
|
K_CRITICAL(success, "Failed to create interrupt pipe");
|
|
}
|
|
|
|
bool ExecuteCommandLine(const char *cmd_line, const ExecuteInfo &info,
|
|
FunctionRef<Span<const uint8_t>()> in_func,
|
|
FunctionRef<void(Span<uint8_t> buf)> out_func, int *out_code)
|
|
{
|
|
BlockAllocator temp_alloc;
|
|
|
|
// Create read pipes
|
|
int in_pfd[2] = {-1, -1};
|
|
K_DEFER {
|
|
CloseDescriptorSafe(&in_pfd[0]);
|
|
CloseDescriptorSafe(&in_pfd[1]);
|
|
};
|
|
if (in_func.IsValid()) {
|
|
if (!CreatePipe(false, in_pfd))
|
|
return false;
|
|
}
|
|
|
|
// Create write pipes
|
|
int out_pfd[2] = {-1, -1};
|
|
K_DEFER {
|
|
CloseDescriptorSafe(&out_pfd[0]);
|
|
CloseDescriptorSafe(&out_pfd[1]);
|
|
};
|
|
if (out_func.IsValid()) {
|
|
if (!CreatePipe(false, out_pfd))
|
|
return false;
|
|
}
|
|
|
|
InitInterruptPipe();
|
|
|
|
// Prepare new environment (if needed)
|
|
HeapArray<char *> new_env;
|
|
if (info.reset_env || info.env_variables.len) {
|
|
if (!info.reset_env) {
|
|
char **ptr = environ;
|
|
|
|
while (*ptr) {
|
|
new_env.Append(*ptr);
|
|
ptr++;
|
|
}
|
|
}
|
|
|
|
for (const ExecuteInfo::KeyValue &kv: info.env_variables) {
|
|
const char *var = Fmt(&temp_alloc, "%1=%2", kv.key, kv.value).ptr;
|
|
new_env.Append((char *)var);
|
|
}
|
|
|
|
new_env.Append(nullptr);
|
|
}
|
|
|
|
// Start process
|
|
pid_t pid;
|
|
{
|
|
posix_spawn_file_actions_t file_actions;
|
|
if ((errno = posix_spawn_file_actions_init(&file_actions))) {
|
|
LogError("Failed to set up standard process descriptors: %1", strerror(errno));
|
|
return false;
|
|
}
|
|
K_DEFER { posix_spawn_file_actions_destroy(&file_actions); };
|
|
|
|
if (in_func.IsValid() && (errno = posix_spawn_file_actions_adddup2(&file_actions, in_pfd[0], STDIN_FILENO))) {
|
|
LogError("Failed to set up standard process descriptors: %1", strerror(errno));
|
|
return false;
|
|
}
|
|
if (out_func.IsValid() && ((errno = posix_spawn_file_actions_adddup2(&file_actions, out_pfd[1], STDOUT_FILENO)) ||
|
|
(errno = posix_spawn_file_actions_adddup2(&file_actions, out_pfd[1], STDERR_FILENO)))) {
|
|
LogError("Failed to set up standard process descriptors: %1", strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
if (info.work_dir) {
|
|
const char *argv[] = { "env", "-C", info.work_dir, "sh", "-c", cmd_line, nullptr };
|
|
errno = posix_spawn(&pid, "/bin/env", &file_actions, nullptr, const_cast<char **>(argv), new_env.ptr ? new_env.ptr : environ);
|
|
} else {
|
|
const char *argv[] = { "sh", "-c", cmd_line, nullptr };
|
|
errno = posix_spawn(&pid, "/bin/sh", &file_actions, nullptr, const_cast<char **>(argv), new_env.ptr ? new_env.ptr : environ);
|
|
}
|
|
if (errno) {
|
|
LogError("Failed to start process: %1", strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
CloseDescriptorSafe(&in_pfd[0]);
|
|
CloseDescriptorSafe(&out_pfd[1]);
|
|
}
|
|
|
|
Span<const uint8_t> write_buf = {};
|
|
bool terminate = false;
|
|
|
|
// Read and write standard process streams
|
|
while (in_pfd[1] >= 0 || out_pfd[0] >= 0) {
|
|
LocalArray<struct pollfd, 3> pfds;
|
|
int in_idx = -1, out_idx = -1, term_idx = -1;
|
|
if (in_pfd[1] >= 0) {
|
|
in_idx = pfds.len;
|
|
pfds.Append({ in_pfd[1], POLLOUT, 0 });
|
|
}
|
|
if (out_pfd[0] >= 0) {
|
|
out_idx = pfds.len;
|
|
pfds.Append({ out_pfd[0], POLLIN, 0 });
|
|
}
|
|
if (int fd = interrupt_pfd[0].load(); fd >= 0) {
|
|
term_idx = pfds.len;
|
|
pfds.Append({ fd, POLLIN, 0 });
|
|
}
|
|
|
|
if (K_RESTART_EINTR(poll(pfds.data, (nfds_t)pfds.len, -1), < 0) < 0) {
|
|
LogError("Failed to poll process I/O: %1", strerror(errno));
|
|
break;
|
|
}
|
|
|
|
unsigned int in_revents = (in_idx >= 0) ? pfds[in_idx].revents : 0;
|
|
unsigned int out_revents = (out_idx >= 0) ? pfds[out_idx].revents : 0;
|
|
unsigned int term_revents = (term_idx >= 0) ? pfds[term_idx].revents : 0;
|
|
|
|
// Try to write
|
|
if (in_revents & (POLLHUP | POLLERR)) {
|
|
CloseDescriptorSafe(&in_pfd[1]);
|
|
} else if (in_revents & POLLOUT) {
|
|
K_ASSERT(in_func.IsValid());
|
|
|
|
if (!write_buf.len) {
|
|
write_buf = in_func();
|
|
K_ASSERT(write_buf.len >= 0);
|
|
}
|
|
|
|
if (write_buf.len) {
|
|
ssize_t write_len = K_RESTART_EINTR(write(in_pfd[1], write_buf.ptr, (size_t)write_buf.len), < 0);
|
|
|
|
if (write_len > 0) {
|
|
write_buf.ptr += write_len;
|
|
write_buf.len -= (Size)write_len;
|
|
} else if (!write_len) {
|
|
CloseDescriptorSafe(&in_pfd[1]);
|
|
} else {
|
|
LogError("Failed to write process input: %1", strerror(errno));
|
|
CloseDescriptorSafe(&in_pfd[1]);
|
|
}
|
|
} else {
|
|
CloseDescriptorSafe(&in_pfd[1]);
|
|
}
|
|
}
|
|
|
|
// Try to read
|
|
if (out_revents & POLLERR) {
|
|
break;
|
|
} else if (out_revents & (POLLIN | POLLHUP)) {
|
|
K_ASSERT(out_func.IsValid());
|
|
|
|
uint8_t read_buf[4096];
|
|
ssize_t read_len = K_RESTART_EINTR(read(out_pfd[0], read_buf, K_SIZE(read_buf)), < 0);
|
|
|
|
if (read_len > 0) {
|
|
out_func(MakeSpan(read_buf, read_len));
|
|
} else if (!read_len) {
|
|
// EOF
|
|
break;
|
|
} else {
|
|
LogError("Failed to read process output: %1", strerror(errno));
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (term_revents) {
|
|
kill(pid, SIGTERM);
|
|
terminate = true;
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Done reading and writing
|
|
CloseDescriptorSafe(&in_pfd[1]);
|
|
CloseDescriptorSafe(&out_pfd[0]);
|
|
|
|
// Wait for process exit
|
|
int status;
|
|
{
|
|
int64_t start = GetMonotonicClock();
|
|
|
|
for (;;) {
|
|
int ret = K_RESTART_EINTR(waitpid(pid, &status, terminate ? WNOHANG : 0), < 0);
|
|
|
|
if (ret < 0) {
|
|
LogError("Failed to wait for process exit: %1", strerror(errno));
|
|
return false;
|
|
} else if (!ret) {
|
|
int64_t delay = GetMonotonicClock() - start;
|
|
|
|
if (delay < 2000) {
|
|
// A timeout on waitpid would be better, but... sigh
|
|
WaitDelay(10);
|
|
} else {
|
|
kill(pid, SIGKILL);
|
|
terminate = false;
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (WIFSIGNALED(status)) {
|
|
*out_code = 128 + WTERMSIG(status);
|
|
} else if (WIFEXITED(status)) {
|
|
*out_code = WEXITSTATUS(status);
|
|
} else {
|
|
*out_code = -1;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#endif
|
|
|
|
bool ExecuteCommandLine(const char *cmd_line, const ExecuteInfo &info,
|
|
Span<const uint8_t> in_buf, Size max_len,
|
|
HeapArray<uint8_t> *out_buf, int *out_code)
|
|
{
|
|
Size start_len = out_buf->len;
|
|
K_DEFER_N(out_guard) { out_buf->RemoveFrom(start_len); };
|
|
|
|
// Check virtual memory limits
|
|
{
|
|
Size memory_max = K_SIZE_MAX - out_buf->len - 1;
|
|
|
|
if (memory_max <= 0) [[unlikely]] {
|
|
LogError("Exhausted memory limit");
|
|
return false;
|
|
}
|
|
|
|
K_ASSERT(max_len);
|
|
max_len = (max_len >= 0) ? std::min(max_len, memory_max) : memory_max;
|
|
}
|
|
|
|
// Don't f*ck up the log
|
|
bool warned = false;
|
|
|
|
bool success = ExecuteCommandLine(cmd_line, info, [&]() { return in_buf; },
|
|
[&](Span<uint8_t> buf) {
|
|
if (out_buf->len - start_len <= max_len - buf.len) {
|
|
out_buf->Append(buf);
|
|
} else if (!warned) {
|
|
LogError("Truncated output");
|
|
warned = true;
|
|
}
|
|
}, out_code);
|
|
if (!success)
|
|
return false;
|
|
|
|
out_guard.Disable();
|
|
return true;
|
|
}
|
|
|
|
Size ReadCommandOutput(const char *cmd_line, Span<char> out_output)
|
|
{
|
|
static ExecuteInfo::KeyValue variables[] = {
|
|
{ "LANG", "C" },
|
|
{ "LC_ALL", "C" }
|
|
};
|
|
|
|
ExecuteInfo info = {};
|
|
info.env_variables = variables;
|
|
|
|
Size total_len = 0;
|
|
const auto write = [&](Span<uint8_t> buf) {
|
|
Size copy = std::min(out_output.len - total_len, buf.len);
|
|
|
|
MemCpy(out_output.ptr + total_len, buf.ptr, copy);
|
|
total_len += copy;
|
|
};
|
|
|
|
int exit_code;
|
|
if (!ExecuteCommandLine(cmd_line, info, MakeSpan((const uint8_t *)nullptr, 0), write, &exit_code))
|
|
return -1;
|
|
if (exit_code) {
|
|
LogDebug("Command '%1 failed (exit code: %2)", cmd_line, exit_code);
|
|
return -1;
|
|
}
|
|
|
|
return total_len;
|
|
}
|
|
|
|
bool ReadCommandOutput(const char *cmd_line, HeapArray<char> *out_output)
|
|
{
|
|
static ExecuteInfo::KeyValue variables[] = {
|
|
{ "LANG", "C" },
|
|
{ "LC_ALL", "C" }
|
|
};
|
|
|
|
ExecuteInfo info = {};
|
|
info.env_variables = variables;
|
|
|
|
int exit_code;
|
|
if (!ExecuteCommandLine(cmd_line, info, {}, Mebibytes(1), out_output, &exit_code))
|
|
return false;
|
|
if (exit_code) {
|
|
LogDebug("Command '%1 failed (exit code: %2)", cmd_line, exit_code);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#endif
|
|
|
|
#if defined(_WIN32)
|
|
|
|
static HANDLE wait_msg_event = CreateEvent(nullptr, TRUE, FALSE, nullptr);
|
|
|
|
void WaitDelay(int64_t delay)
|
|
{
|
|
K_ASSERT(delay >= 0);
|
|
K_ASSERT(delay < 1000ll * INT32_MAX);
|
|
|
|
while (delay) {
|
|
DWORD delay32 = (DWORD)std::min(delay, (int64_t)UINT32_MAX);
|
|
delay -= delay32;
|
|
|
|
Sleep(delay32);
|
|
}
|
|
}
|
|
|
|
WaitResult WaitEvents(Span<const WaitSource> sources, int64_t timeout, uint64_t *out_ready)
|
|
{
|
|
K_ASSERT(sources.len <= 62);
|
|
|
|
ignore_ctrl_event = InitConsoleCtrlHandler();
|
|
K_ASSERT(ignore_ctrl_event);
|
|
|
|
LocalArray<HANDLE, 64> events;
|
|
DWORD wake = 0;
|
|
DWORD wait_ret = 0;
|
|
|
|
events.Append(console_ctrl_event);
|
|
|
|
// There is no way to get a waitable HANDLE for the Win32 GUI message pump.
|
|
// Instead, waitable sources (such as the system tray code) return NULL to indicate that
|
|
// they need to wait for messages on the message pump.
|
|
for (const WaitSource &src: sources) {
|
|
if (src.handle) {
|
|
events.Append(src.handle);
|
|
} else {
|
|
wake = QS_ALLINPUT;
|
|
}
|
|
|
|
timeout = (int64_t)std::min((uint64_t)timeout, (uint64_t)src.timeout);
|
|
}
|
|
|
|
if (main_thread == GetCurrentThreadId()) {
|
|
wait_ret = WAIT_OBJECT_0 + (DWORD)events.len;
|
|
events.Append(wait_msg_event);
|
|
}
|
|
|
|
DWORD ret;
|
|
if (timeout >= 0) {
|
|
do {
|
|
DWORD timeout32 = (DWORD)std::min(timeout, (int64_t)UINT32_MAX);
|
|
timeout -= timeout32;
|
|
|
|
ret = MsgWaitForMultipleObjects((DWORD)events.len, events.data, FALSE, timeout32, wake);
|
|
} while (ret == WAIT_TIMEOUT && timeout);
|
|
} else {
|
|
ret = MsgWaitForMultipleObjects((DWORD)events.len, events.data, FALSE, INFINITE, wake);
|
|
}
|
|
|
|
if (ret == WAIT_TIMEOUT) {
|
|
return WaitResult::Timeout;
|
|
} else if (ret == WAIT_OBJECT_0) {
|
|
return WaitResult::Interrupt;
|
|
} else if (ret == wait_ret) {
|
|
ResetEvent(wait_msg_event);
|
|
return WaitResult::Message;
|
|
} else if (ret == WAIT_OBJECT_0 + events.len) {
|
|
// Mark all sources with an interest in the message pump as ready
|
|
if (out_ready) {
|
|
uint64_t flags = 0;
|
|
for (Size i = 0; i < sources.len; i++) {
|
|
flags |= !sources[i].handle ? (1ull << i) : 0;
|
|
}
|
|
*out_ready = flags;
|
|
}
|
|
return WaitResult::Ready;
|
|
} else {
|
|
Size idx = (Size)ret - WAIT_OBJECT_0 - 1;
|
|
K_ASSERT(idx >= 0 && idx < sources.len);
|
|
|
|
if (out_ready) {
|
|
*out_ready |= 1ull << idx;
|
|
}
|
|
return WaitResult::Ready;
|
|
}
|
|
}
|
|
|
|
WaitResult WaitEvents(int64_t timeout)
|
|
{
|
|
Span<const WaitSource> sources = {};
|
|
return WaitEvents(sources, timeout);
|
|
}
|
|
|
|
void PostWaitMessage()
|
|
{
|
|
SetEvent(wait_msg_event);
|
|
}
|
|
|
|
void PostTerminate()
|
|
{
|
|
SetEvent(console_ctrl_event);
|
|
}
|
|
|
|
#else
|
|
|
|
void WaitDelay(int64_t delay)
|
|
{
|
|
K_ASSERT(delay >= 0);
|
|
K_ASSERT(delay < 1000ll * INT32_MAX);
|
|
|
|
struct timespec ts;
|
|
ts.tv_sec = (int)(delay / 1000);
|
|
ts.tv_nsec = (int)((delay % 1000) * 1000000);
|
|
|
|
struct timespec rem;
|
|
while (nanosleep(&ts, &rem) < 0) {
|
|
K_ASSERT(errno == EINTR);
|
|
ts = rem;
|
|
}
|
|
}
|
|
|
|
#if !defined(__wasi__)
|
|
|
|
WaitResult WaitEvents(Span<const WaitSource> sources, int64_t timeout, uint64_t *out_ready)
|
|
{
|
|
LocalArray<struct pollfd, 64> pfds;
|
|
K_ASSERT(sources.len <= K_LEN(pfds.data) - 1);
|
|
|
|
// Don't exit after SIGINT/SIGTERM, just signal us
|
|
flag_signal = true;
|
|
|
|
static std::atomic_bool message { false };
|
|
SetSignalHandler(SIGUSR1, [](int) { message = true; });
|
|
|
|
for (const WaitSource &src: sources) {
|
|
short events = src.events ? (short)src.events : POLLIN;
|
|
pfds.Append({ src.fd, events, 0 });
|
|
|
|
timeout = (int64_t)std::min((uint64_t)timeout, (uint64_t)src.timeout);
|
|
}
|
|
|
|
InitInterruptPipe();
|
|
pfds.Append({ interrupt_pfd[0], POLLIN, 0 });
|
|
|
|
int64_t start = (timeout >= 0) ? GetMonotonicClock() : 0;
|
|
int64_t until = start + timeout;
|
|
int timeout32 = (int)std::min(until - start, (int64_t)INT_MAX);
|
|
|
|
for (;;) {
|
|
if (explicit_signal == SIGTERM) {
|
|
return WaitResult::Exit;
|
|
} else if (explicit_signal) {
|
|
return WaitResult::Interrupt;
|
|
} else if (message && pthread_self() == main_thread) {
|
|
message = false;
|
|
return WaitResult::Message;
|
|
}
|
|
|
|
int ready = poll(pfds.data, (nfds_t)pfds.len, timeout32);
|
|
|
|
if (ready < 0) {
|
|
if (errno == EINTR)
|
|
continue;
|
|
|
|
LogError("Failed to poll for events: %1", strerror(errno));
|
|
abort();
|
|
} else if (ready > 0) {
|
|
uint64_t flags = 0;
|
|
for (Size i = 0; i < pfds.len - 1; i++) {
|
|
flags |= pfds[i].revents ? (1ull << i) : 0;
|
|
}
|
|
|
|
if (flags) {
|
|
if (out_ready) {
|
|
*out_ready = flags;
|
|
}
|
|
return WaitResult::Ready;
|
|
}
|
|
}
|
|
|
|
if (timeout >= 0) {
|
|
int64_t clock = GetMonotonicClock();
|
|
if (clock >= until)
|
|
break;
|
|
timeout32 = (int)std::min(until - clock, (int64_t)INT_MAX);
|
|
}
|
|
}
|
|
|
|
return WaitResult::Timeout;
|
|
}
|
|
|
|
WaitResult WaitEvents(int64_t timeout)
|
|
{
|
|
Span<const WaitSource> sources = {};
|
|
return WaitEvents(sources, timeout);
|
|
}
|
|
|
|
void PostWaitMessage()
|
|
{
|
|
pid_t pid = getpid();
|
|
kill(pid, SIGUSR1);
|
|
}
|
|
|
|
void PostTerminate()
|
|
{
|
|
InitInterruptPipe();
|
|
|
|
char dummy = 0;
|
|
K_IGNORE write(interrupt_pfd[1], &dummy, 1);
|
|
}
|
|
|
|
#endif
|
|
|
|
#endif
|
|
|
|
int GetCoreCount()
|
|
{
|
|
#if defined(__wasi__)
|
|
return 1;
|
|
#else
|
|
static int cores;
|
|
|
|
if (!cores) {
|
|
const char *env = GetEnv("OVERRIDE_CORES");
|
|
|
|
if (env) {
|
|
char *end_ptr;
|
|
long value = strtol(env, &end_ptr, 10);
|
|
|
|
if (end_ptr > env && !end_ptr[0] && value > 0) {
|
|
cores = (int)value;
|
|
} else {
|
|
LogError("OVERRIDE_CORES must be positive number (ignored)");
|
|
cores = (int)std::thread::hardware_concurrency();
|
|
}
|
|
} else {
|
|
cores = (int)std::thread::hardware_concurrency();
|
|
}
|
|
|
|
K_ASSERT(cores > 0);
|
|
}
|
|
|
|
return cores;
|
|
#endif
|
|
}
|
|
|
|
#if !defined(_WIN32) && !defined(__wasi__)
|
|
|
|
bool RaiseMaximumOpenFiles(int limit)
|
|
{
|
|
struct rlimit lim;
|
|
|
|
if (getrlimit(RLIMIT_NOFILE, &lim) < 0) {
|
|
LogError("getrlimit(RLIMIT_NOFILE) failed: %1", strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
rlim_t target = (limit >= 0) ? (rlim_t)limit : lim.rlim_max;
|
|
|
|
if (lim.rlim_cur >= target)
|
|
return true;
|
|
|
|
lim.rlim_cur = std::min(target, lim.rlim_max);
|
|
|
|
if (setrlimit(RLIMIT_NOFILE, &lim) < 0) {
|
|
LogError("Could not raise RLIMIT_NOFILE: %1", strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
if (lim.rlim_cur < target) {
|
|
LogWarning("Maximum number of open descriptors is low: %1 (recommended: %2)", lim.rlim_cur, target);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool DropRootIdentity()
|
|
{
|
|
uid_t uid = getuid();
|
|
uid_t euid = geteuid();
|
|
gid_t gid = getgid();
|
|
|
|
if (!uid) {
|
|
LogError("This program must not be run as root");
|
|
return false;
|
|
}
|
|
if (uid != euid) {
|
|
LogDebug("Dropping SUID privileges...");
|
|
}
|
|
|
|
if (!euid && setgroups(1, &gid) < 0)
|
|
goto error;
|
|
if (setregid(gid, gid) < 0)
|
|
goto error;
|
|
if (setreuid(uid, uid) < 0)
|
|
goto error;
|
|
K_CRITICAL(setuid(0) < 0, "Managed to regain root privileges");
|
|
|
|
return true;
|
|
|
|
error:
|
|
LogError("Failed to drop root privilegies: %1", strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
#endif
|
|
|
|
#if defined(__linux__)
|
|
|
|
bool NotifySystemd()
|
|
{
|
|
const char *addr = GetEnv("NOTIFY_SOCKET");
|
|
if (!addr)
|
|
return true;
|
|
|
|
struct sockaddr_un sa;
|
|
if (addr[0] == '@') {
|
|
addr++;
|
|
|
|
if (strlen(addr) >= sizeof(sa.sun_path) - 1) {
|
|
LogError("Abstract socket address in NOTIFY_SOCKET is too long");
|
|
return false;
|
|
}
|
|
|
|
sa.sun_family = AF_UNIX;
|
|
sa.sun_path[0] = 0;
|
|
CopyString(addr, MakeSpan(sa.sun_path + 1, K_SIZE(sa.sun_path) - 1));
|
|
} else if (addr[0] == '/') {
|
|
if (strlen(addr) >= sizeof(sa.sun_path)) {
|
|
LogError("Socket pathname in NOTIFY_SOCKET is too long");
|
|
return false;
|
|
}
|
|
|
|
sa.sun_family = AF_UNIX;
|
|
CopyString(addr, sa.sun_path);
|
|
} else {
|
|
LogError("Invalid socket address in NOTIFY_SOCKET");
|
|
return false;
|
|
}
|
|
|
|
int fd = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0);
|
|
if (fd < 0) {
|
|
LogError("Failed to create UNIX socket: %1", strerror(errno));
|
|
return false;
|
|
}
|
|
K_DEFER { close(fd); };
|
|
|
|
struct iovec iov = {};
|
|
struct msghdr msg = {};
|
|
iov.iov_base = (void *)"READY=1";
|
|
iov.iov_len = strlen("READY=1");
|
|
msg.msg_name = &sa;
|
|
msg.msg_namelen = offsetof(struct sockaddr_un, sun_path) + strlen(addr);
|
|
msg.msg_iov = &iov;
|
|
msg.msg_iovlen = 1;
|
|
|
|
if (sendmsg(fd, &msg, MSG_NOSIGNAL) < 0) {
|
|
LogError("Failed to send message to systemd: %1", strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
unsetenv("NOTIFY_SOCKET");
|
|
return true;
|
|
}
|
|
|
|
#endif
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Main
|
|
// ------------------------------------------------------------------------
|
|
|
|
static InitHelper *init;
|
|
static FinalizeHelper *finalize;
|
|
|
|
InitHelper::InitHelper(const char *name)
|
|
: name(name)
|
|
{
|
|
next = init;
|
|
init = this;
|
|
}
|
|
|
|
FinalizeHelper::FinalizeHelper(const char *name)
|
|
: name(name)
|
|
{
|
|
next = finalize;
|
|
finalize = this;
|
|
}
|
|
|
|
void InitApp()
|
|
{
|
|
#if defined(_WIN32)
|
|
// Use binary standard I/O
|
|
_setmode(STDIN_FILENO, _O_BINARY);
|
|
_setmode(STDOUT_FILENO, _O_BINARY);
|
|
_setmode(STDERR_FILENO, _O_BINARY);
|
|
|
|
SetConsoleCP(CP_UTF8);
|
|
SetConsoleOutputCP(CP_UTF8);
|
|
#endif
|
|
|
|
#if !defined(_WIN32) && !defined(__wasi__)
|
|
// Setup default signal handlers
|
|
SetSignalHandler(SIGINT, DefaultSignalHandler);
|
|
SetSignalHandler(SIGTERM, DefaultSignalHandler);
|
|
SetSignalHandler(SIGHUP, DefaultSignalHandler);
|
|
SetSignalHandler(SIGPIPE, [](int) {});
|
|
|
|
InitInterruptPipe();
|
|
|
|
// Make sure timezone information is in place, which is useful if some kind of sandbox runs later and
|
|
// the timezone information is not available (seccomp, namespace, landlock, whatever).
|
|
tzset();
|
|
#endif
|
|
|
|
#if defined(__OpenBSD__)
|
|
// This can depend on PATH, which could change during execution
|
|
// so we want to cache the result as soon as possible.
|
|
GetApplicationExecutable();
|
|
#endif
|
|
|
|
// Init libraries
|
|
while (init) {
|
|
#if defined(K_DEBUG)
|
|
LogDebug("Init %1 library", init->name);
|
|
#endif
|
|
|
|
init->Run();
|
|
init = init->next;
|
|
}
|
|
}
|
|
|
|
void ExitApp()
|
|
{
|
|
while (finalize) {
|
|
#if defined(K_DEBUG)
|
|
LogDebug("Finalize %1 library", finalize->name);
|
|
#endif
|
|
|
|
finalize->Run();
|
|
finalize = finalize->next;
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Standard paths
|
|
// ------------------------------------------------------------------------
|
|
|
|
#if defined(_WIN32)
|
|
|
|
const char *GetUserConfigPath(const char *name, Allocator *alloc)
|
|
{
|
|
K_ASSERT(!strchr(K_PATH_SEPARATORS, name[0]));
|
|
|
|
static char cache_dir[4096];
|
|
static std::once_flag flag;
|
|
|
|
std::call_once(flag, []() {
|
|
wchar_t *dir = nullptr;
|
|
K_DEFER { CoTaskMemFree(dir); };
|
|
|
|
K_CRITICAL(SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr, &dir) == S_OK,
|
|
"Failed to retrieve path to roaming user AppData");
|
|
K_CRITICAL(ConvertWin32WideToUtf8(dir, cache_dir) >= 0,
|
|
"Path to roaming AppData is invalid or too big");
|
|
});
|
|
|
|
const char *path = Fmt(alloc, "%1%/%2", cache_dir, name).ptr;
|
|
return path;
|
|
}
|
|
|
|
const char *GetUserCachePath(const char *name, Allocator *alloc)
|
|
{
|
|
K_ASSERT(!strchr(K_PATH_SEPARATORS, name[0]));
|
|
|
|
static char cache_dir[4096];
|
|
static std::once_flag flag;
|
|
|
|
std::call_once(flag, []() {
|
|
wchar_t *dir = nullptr;
|
|
K_DEFER { CoTaskMemFree(dir); };
|
|
|
|
K_CRITICAL(SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &dir) == S_OK,
|
|
"Failed to retrieve path to local user AppData");
|
|
K_CRITICAL(ConvertWin32WideToUtf8(dir, cache_dir) >= 0,
|
|
"Path to local AppData is invalid or too big");
|
|
});
|
|
|
|
const char *path = Fmt(alloc, "%1%/%2", cache_dir, name).ptr;
|
|
return path;
|
|
}
|
|
|
|
const char *GetTemporaryDirectory()
|
|
{
|
|
static char temp_dir[4096];
|
|
static std::once_flag flag;
|
|
|
|
std::call_once(flag, []() {
|
|
Size len;
|
|
if (win32_utf8) {
|
|
len = (Size)GetTempPathA(K_SIZE(temp_dir), temp_dir);
|
|
K_CRITICAL(len < K_SIZE(temp_dir), "Temporary directory path is too big");
|
|
} else {
|
|
static wchar_t dir_w[4096];
|
|
Size len_w = (Size)GetTempPathW(K_LEN(dir_w), dir_w);
|
|
|
|
K_CRITICAL(len_w < K_LEN(dir_w), "Temporary directory path is too big");
|
|
|
|
len = ConvertWin32WideToUtf8(dir_w, temp_dir);
|
|
K_CRITICAL(len >= 0, "Temporary directory path is invalid or too big");
|
|
}
|
|
|
|
while (len > 0 && IsPathSeparator(temp_dir[len - 1])) {
|
|
len--;
|
|
}
|
|
temp_dir[len] = 0;
|
|
});
|
|
|
|
return temp_dir;
|
|
}
|
|
|
|
#else
|
|
|
|
const char *GetUserConfigPath(const char *name, Allocator *alloc)
|
|
{
|
|
K_ASSERT(!strchr(K_PATH_SEPARATORS, name[0]));
|
|
|
|
const char *xdg = GetEnv("XDG_CONFIG_HOME");
|
|
const char *home = GetEnv("HOME");
|
|
|
|
const char *path = nullptr;
|
|
|
|
if (xdg) {
|
|
path = Fmt(alloc, "%1%/%2", xdg, name).ptr;
|
|
} else if (home) {
|
|
path = Fmt(alloc, "%1%/.config/%2", home, name).ptr;
|
|
#if !defined(__wasi__)
|
|
} else if (!getuid()) {
|
|
path = Fmt(alloc, "/root/.config/%1", name).ptr;
|
|
#endif
|
|
}
|
|
|
|
if (path && !EnsureDirectoryExists(path))
|
|
return nullptr;
|
|
|
|
return path;
|
|
}
|
|
|
|
const char *GetUserCachePath(const char *name, Allocator *alloc)
|
|
{
|
|
K_ASSERT(!strchr(K_PATH_SEPARATORS, name[0]));
|
|
|
|
const char *xdg = GetEnv("XDG_CACHE_HOME");
|
|
const char *home = GetEnv("HOME");
|
|
|
|
const char *path = nullptr;
|
|
|
|
if (xdg) {
|
|
path = Fmt(alloc, "%1%/%2", xdg, name).ptr;
|
|
} else if (home) {
|
|
path = Fmt(alloc, "%1%/.cache/%2", home, name).ptr;
|
|
#if !defined(__wasi__)
|
|
} else if (!getuid()) {
|
|
path = Fmt(alloc, "/root/.cache/%1", name).ptr;
|
|
#endif
|
|
}
|
|
|
|
if (path && !EnsureDirectoryExists(path))
|
|
return nullptr;
|
|
|
|
return path;
|
|
}
|
|
|
|
const char *GetSystemConfigPath(const char *name, Allocator *alloc)
|
|
{
|
|
K_ASSERT(!strchr(K_PATH_SEPARATORS, name[0]));
|
|
|
|
const char *path = Fmt(alloc, "/etc/%1", name).ptr;
|
|
return path;
|
|
}
|
|
|
|
const char *GetTemporaryDirectory()
|
|
{
|
|
static char temp_dir[4096];
|
|
static std::once_flag flag;
|
|
|
|
std::call_once(flag, []() {
|
|
Span<const char> env = GetEnv("TMPDIR");
|
|
|
|
while (env.len > 0 && IsPathSeparator(env[env.len - 1])) {
|
|
env.len--;
|
|
}
|
|
|
|
if (env.len && env.len < K_SIZE(temp_dir)) {
|
|
CopyString(env, temp_dir);
|
|
} else {
|
|
CopyString("/tmp", temp_dir);
|
|
}
|
|
});
|
|
|
|
return temp_dir;
|
|
}
|
|
|
|
#endif
|
|
|
|
const char *FindConfigFile(const char *directory, Span<const char *const> names,
|
|
Allocator *alloc, HeapArray<const char *> *out_possibilities)
|
|
{
|
|
K_ASSERT(!directory || directory[0]);
|
|
|
|
decltype(GetUserConfigPath) *funcs[] = {
|
|
GetUserConfigPath,
|
|
#if !defined(_WIN32)
|
|
GetSystemConfigPath
|
|
#endif
|
|
};
|
|
|
|
const char *filename = nullptr;
|
|
|
|
// Try application directory
|
|
for (const char *name: names) {
|
|
Span<const char> dir = GetApplicationDirectory();
|
|
const char *path = Fmt(alloc, "%1%/%2", dir, name).ptr;
|
|
|
|
if (!filename && TestFile(path, FileType::File)) {
|
|
filename = path;
|
|
}
|
|
if (out_possibilities) {
|
|
out_possibilities->Append(path);
|
|
}
|
|
}
|
|
|
|
LocalArray<const char *, 8> tests;
|
|
{
|
|
K_ASSERT(names.len <= tests.Available());
|
|
|
|
for (const char *name: names) {
|
|
if (directory) {
|
|
const char *test = Fmt(alloc, "%1%/%2", directory, name).ptr;
|
|
tests.Append(test);
|
|
} else {
|
|
tests.Append(name);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try standard paths
|
|
for (const auto &func: funcs) {
|
|
for (const char *test: tests) {
|
|
const char *path = func(test, alloc);
|
|
|
|
if (!path)
|
|
continue;
|
|
|
|
if (!filename && TestFile(path, FileType::File)) {
|
|
filename = path;
|
|
}
|
|
if (out_possibilities) {
|
|
out_possibilities->Append(path);
|
|
}
|
|
}
|
|
}
|
|
|
|
return filename;
|
|
}
|
|
|
|
static const char *CreateUniquePath(Span<const char> directory, const char *prefix, const char *extension,
|
|
Allocator *alloc, FunctionRef<bool(const char *path)> create)
|
|
{
|
|
K_ASSERT(alloc);
|
|
|
|
HeapArray<char> filename(alloc);
|
|
filename.Append(directory);
|
|
filename.Append(*K_PATH_SEPARATORS);
|
|
if (prefix) {
|
|
filename.Append(prefix);
|
|
filename.Append('.');
|
|
}
|
|
|
|
Size change_offset = filename.len;
|
|
|
|
PushLogFilter([](LogLevel, const char *, const char *, FunctionRef<LogFunc>) {});
|
|
K_DEFER_N(log_guard) { PopLogFilter(); };
|
|
|
|
for (Size i = 0; i < 1000; i++) {
|
|
// We want to show an error on last try
|
|
if (i == 999) [[unlikely]] {
|
|
PopLogFilter();
|
|
log_guard.Disable();
|
|
}
|
|
|
|
filename.RemoveFrom(change_offset);
|
|
Fmt(&filename, "%1%2", FmtRandom(24), extension);
|
|
|
|
if (create(filename.ptr)) {
|
|
const char *ret = filename.TrimAndLeak(1).ptr;
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
const char *CreateUniqueFile(Span<const char> directory, const char *prefix, const char *extension,
|
|
Allocator *alloc, int *out_fd)
|
|
{
|
|
return CreateUniquePath(directory, prefix, extension, alloc, [&](const char *path) {
|
|
int flags = (int)OpenFlag::Read | (int)OpenFlag::Write |
|
|
(int)OpenFlag::Exclusive;
|
|
|
|
int fd = OpenFile(path, flags);
|
|
|
|
if (fd >= 0) {
|
|
if (out_fd) {
|
|
*out_fd = fd;
|
|
} else {
|
|
CloseDescriptor(fd);
|
|
}
|
|
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
|
|
const char *CreateUniqueDirectory(Span<const char> directory, const char *prefix, Allocator *alloc)
|
|
{
|
|
return CreateUniquePath(directory, prefix, "", alloc, [&](const char *path) {
|
|
return MakeDirectory(path);
|
|
});
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Parsing
|
|
// ------------------------------------------------------------------------
|
|
|
|
bool ParseBool(Span<const char> str, bool *out_value, unsigned int flags, Span<const char> *out_remaining)
|
|
{
|
|
union {
|
|
char raw[8];
|
|
uint64_t u;
|
|
} u = {};
|
|
Size end = 0;
|
|
bool value = false;
|
|
|
|
switch (str.len) {
|
|
default: { K_ASSERT(str.len >= 0); } [[fallthrough]];
|
|
|
|
#if defined(K_BIG_ENDIAN)
|
|
case 8: { u.raw[0] = LowerAscii(str[7]); } [[fallthrough]];
|
|
case 7: { u.raw[1] = LowerAscii(str[6]); } [[fallthrough]];
|
|
case 6: { u.raw[2] = LowerAscii(str[5]); } [[fallthrough]];
|
|
case 5: { u.raw[3] = LowerAscii(str[4]); } [[fallthrough]];
|
|
case 4: { u.raw[4] = LowerAscii(str[3]); } [[fallthrough]];
|
|
case 3: { u.raw[5] = LowerAscii(str[2]); } [[fallthrough]];
|
|
case 2: { u.raw[6] = LowerAscii(str[1]); } [[fallthrough]];
|
|
case 1: { u.raw[7] = LowerAscii(str[0]); } [[fallthrough]];
|
|
case 0: {} break;
|
|
#else
|
|
case 8: { u.raw[7] = LowerAscii(str[7]); } [[fallthrough]];
|
|
case 7: { u.raw[6] = LowerAscii(str[6]); } [[fallthrough]];
|
|
case 6: { u.raw[5] = LowerAscii(str[5]); } [[fallthrough]];
|
|
case 5: { u.raw[4] = LowerAscii(str[4]); } [[fallthrough]];
|
|
case 4: { u.raw[3] = LowerAscii(str[3]); } [[fallthrough]];
|
|
case 3: { u.raw[2] = LowerAscii(str[2]); } [[fallthrough]];
|
|
case 2: { u.raw[1] = LowerAscii(str[1]); } [[fallthrough]];
|
|
case 1: { u.raw[0] = LowerAscii(str[0]); } [[fallthrough]];
|
|
case 0: {} break;
|
|
#endif
|
|
}
|
|
|
|
#define MATCH(Wanted, Len, Value) \
|
|
if (uint64_t masked = u.u & ((1ull << ((Len) * 8)) - 1); masked == (Wanted)) { \
|
|
end = (Len); \
|
|
value = (Value); \
|
|
\
|
|
break; \
|
|
}
|
|
|
|
do {
|
|
MATCH(0x31ull, 1, true);
|
|
MATCH(0x6E6Full, 2, true);
|
|
MATCH(0x736579ull, 3, true);
|
|
MATCH(0x79ull, 1, true);
|
|
MATCH(0x65757274ull, 4, true);
|
|
MATCH(0x30ull, 1, false);
|
|
MATCH(0x66666Full, 3, false);
|
|
MATCH(0x6F6Eull, 2, false);
|
|
MATCH(0x6Eull, 1, false);
|
|
MATCH(0x65736C6166ull, 5, false);
|
|
|
|
if (flags & (int)ParseFlag::Log) {
|
|
LogError("Invalid boolean value '%1'", str);
|
|
}
|
|
return false;
|
|
} while (false);
|
|
|
|
#undef MATCH
|
|
|
|
if ((flags & (int)ParseFlag::End) && end < str.len) [[unlikely]] {
|
|
if (flags & (int)ParseFlag::Log) {
|
|
LogError("Malformed boolean '%1'", str);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
*out_value = value;
|
|
if (out_remaining) {
|
|
*out_remaining = str.Take(end, str.len - end);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool ParseSize(Span<const char> str, int64_t *out_size, unsigned int flags, Span<const char> *out_remaining)
|
|
{
|
|
uint64_t size = 0;
|
|
uint64_t multiplier = 1;
|
|
|
|
if (!ParseInt(str, &size, flags & ~(int)ParseFlag::End, &str))
|
|
return false;
|
|
if (size > INT64_MAX) [[unlikely]]
|
|
goto overflow;
|
|
|
|
if (str.len) {
|
|
int next = 1;
|
|
|
|
switch (str[0]) {
|
|
case 'B': { multiplier = 1; } break;
|
|
case 'k': { multiplier = 1000; } break;
|
|
case 'M': { multiplier = 1000000; } break;
|
|
case 'G': { multiplier = 1000000000; } break;
|
|
case 'T': { multiplier = 1000000000000; } break;
|
|
default: { next = 0; } break;
|
|
}
|
|
|
|
if ((flags & (int)ParseFlag::End) && str.len > next) [[unlikely]] {
|
|
if (flags & (int)ParseFlag::Log) {
|
|
LogError("Unknown size unit '%1'", str[0]);
|
|
}
|
|
return false;
|
|
}
|
|
str = str.Take(next, str.len - next);
|
|
}
|
|
|
|
#if defined(__GNUC__) || defined(__clang__)
|
|
if (__builtin_mul_overflow(size, multiplier, &size) || size > INT64_MAX) [[unlikely]]
|
|
goto overflow;
|
|
#else
|
|
{
|
|
uint64_t total = size * multiplier;
|
|
if ((size && total / size != multiplier) || total > INT64_MAX) [[unlikely]]
|
|
goto overflow;
|
|
size = (int64_t)total;
|
|
}
|
|
#endif
|
|
|
|
*out_size = (int64_t)size;
|
|
if (out_remaining) {
|
|
*out_remaining = str;
|
|
}
|
|
return true;
|
|
|
|
overflow:
|
|
if (flags & (int)ParseFlag::Log) {
|
|
LogError("Size value is too high");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool ParseDuration(Span<const char> str, int64_t *out_duration, unsigned int flags, Span<const char> *out_remaining)
|
|
{
|
|
int64_t duration = 0;
|
|
int64_t multiplier = 1000;
|
|
|
|
if (!ParseInt(str, &duration, flags & ~(int)ParseFlag::End, &str))
|
|
return false;
|
|
if (duration < 0) [[unlikely]] {
|
|
LogError("Duration values must be positive");
|
|
return false;
|
|
}
|
|
|
|
if (str.len) {
|
|
int next = 1;
|
|
|
|
switch (str[0]) {
|
|
case 's': { multiplier = 1000; } break;
|
|
case 'm': { multiplier = 60000; } break;
|
|
case 'h': { multiplier = 3600000; } break;
|
|
case 'd': { multiplier = 86400000; } break;
|
|
default: { next = 0; } break;
|
|
}
|
|
|
|
if ((flags & (int)ParseFlag::End) && str.len > next) [[unlikely]] {
|
|
if (flags & (int)ParseFlag::Log) {
|
|
LogError("Unknown duration unit '%1'", str[0]);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
str = str.Take(next, str.len - next);
|
|
}
|
|
|
|
#if defined(__GNUC__) || defined(__clang__)
|
|
if (__builtin_mul_overflow(duration, multiplier, &duration)) [[unlikely]]
|
|
goto overflow;
|
|
#else
|
|
{
|
|
uint64_t total = duration * multiplier;
|
|
if ((duration && total / duration != (uint64_t)multiplier) || total > INT64_MAX) [[unlikely]]
|
|
goto overflow;
|
|
duration = (int64_t)total;
|
|
}
|
|
#endif
|
|
|
|
*out_duration = duration;
|
|
if (out_remaining) {
|
|
*out_remaining = str;
|
|
}
|
|
return true;
|
|
|
|
overflow:
|
|
if (flags & (int)ParseFlag::Log) {
|
|
LogError("Duration value is too high");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// XXX: Rewrite the ugly parsing part
|
|
bool ParseDate(Span<const char> date_str, LocalDate *out_date, unsigned int flags, Span<const char> *out_remaining)
|
|
{
|
|
LocalDate date;
|
|
|
|
int parts[3] = {};
|
|
int lengths[3] = {};
|
|
Size offset = 0;
|
|
for (int i = 0; i < 3; i++) {
|
|
int mult = 1;
|
|
while (offset < date_str.len) {
|
|
char c = date_str[offset];
|
|
int digit = c - '0';
|
|
if ((unsigned int)digit < 10) {
|
|
parts[i] = (parts[i] * 10) + digit;
|
|
if (++lengths[i] > 5) [[unlikely]]
|
|
goto malformed;
|
|
} else if (!lengths[i] && c == '-' && mult == 1 && i != 1) {
|
|
mult = -1;
|
|
} else if (i == 2 && !(flags & (int)ParseFlag::End) && c != '/' && c != '-') [[unlikely]] {
|
|
break;
|
|
} else if (!lengths[i] || (c != '/' && c != '-')) [[unlikely]] {
|
|
goto malformed;
|
|
} else {
|
|
offset++;
|
|
break;
|
|
}
|
|
offset++;
|
|
}
|
|
parts[i] *= mult;
|
|
}
|
|
if ((flags & (int)ParseFlag::End) && offset < date_str.len)
|
|
goto malformed;
|
|
|
|
if ((unsigned int)lengths[1] > 2) [[unlikely]]
|
|
goto malformed;
|
|
if ((lengths[0] > 2) == (lengths[2] > 2)) [[unlikely]] {
|
|
if (flags & (int)ParseFlag::Log) {
|
|
LogError("Ambiguous date string '%1'", date_str);
|
|
}
|
|
return false;
|
|
} else if (lengths[2] > 2) {
|
|
std::swap(parts[0], parts[2]);
|
|
}
|
|
if (parts[0] < -INT16_MAX || parts[0] > INT16_MAX || (unsigned int)parts[2] > 99) [[unlikely]]
|
|
goto malformed;
|
|
|
|
date.st.year = (int16_t)parts[0];
|
|
date.st.month = (int8_t)parts[1];
|
|
date.st.day = (int8_t)parts[2];
|
|
|
|
if ((flags & (int)ParseFlag::Validate) && !date.IsValid()) {
|
|
if (flags & (int)ParseFlag::Log) {
|
|
LogError("Invalid date string '%1'", date_str);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
*out_date = date;
|
|
if (out_remaining) {
|
|
*out_remaining = date_str.Take(offset, date_str.len - offset);
|
|
}
|
|
return true;
|
|
|
|
malformed:
|
|
if (flags & (int)ParseFlag::Log) {
|
|
LogError("Malformed date string '%1'", date_str);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool ParseVersion(Span<const char> str, int parts, int multiplier,
|
|
int64_t *out_version, unsigned int flags, Span<const char> *out_remaining)
|
|
{
|
|
K_ASSERT(parts >= 0 && parts < 6);
|
|
|
|
int64_t version = 0;
|
|
Span<const char> remain = str;
|
|
|
|
while (remain.len && parts) {
|
|
int component = 0;
|
|
if (!ParseInt(remain, &component, 0, &remain)) {
|
|
if (flags & (int)ParseFlag::Log) {
|
|
LogError("Malformed version string '%1'", str);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
version = (version * multiplier) + component;
|
|
parts--;
|
|
|
|
if (!remain.len || remain[0] != '.')
|
|
break;
|
|
remain.ptr++;
|
|
remain.len--;
|
|
}
|
|
|
|
if (remain.len && (flags & (int)ParseFlag::End)) {
|
|
if (flags & (int)ParseFlag::Log) {
|
|
LogError("Malformed version string '%1'", str);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
while (parts) {
|
|
version *= multiplier;
|
|
parts--;
|
|
}
|
|
|
|
*out_version = version;
|
|
if (out_remaining) {
|
|
*out_remaining = remain;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Random
|
|
// ------------------------------------------------------------------------
|
|
|
|
static thread_local Size rnd_remain;
|
|
static thread_local int64_t rnd_clock;
|
|
#if !defined(_WIN32)
|
|
static thread_local pid_t rnd_pid;
|
|
#endif
|
|
static thread_local uint32_t rnd_state[16];
|
|
static thread_local uint8_t rnd_buf[64];
|
|
static thread_local Size rnd_offset;
|
|
|
|
static thread_local FastRandom rng_fast;
|
|
|
|
static inline uint32_t ROTL32(uint32_t v, int n)
|
|
{
|
|
return (v << n) | (v >> (32 - n));
|
|
}
|
|
|
|
static inline uint64_t ROTL64(uint64_t v, int n)
|
|
{
|
|
return (v << n) | (v >> (64 - n));
|
|
}
|
|
|
|
static inline uint32_t LE32(const uint8_t *ptr)
|
|
{
|
|
return ((uint32_t)ptr[0] << 0) |
|
|
((uint32_t)ptr[1] << 8) |
|
|
((uint32_t)ptr[2] << 16) |
|
|
((uint32_t)ptr[3] << 24);
|
|
}
|
|
|
|
void InitChaCha20(uint32_t state[16], const uint8_t key[32], const uint8_t iv[8], const uint8_t counter[8])
|
|
{
|
|
static uint8_t magic[] = "expand 32-byte k";
|
|
|
|
state[0] = LE32(magic + 0);
|
|
state[1] = LE32(magic + 4);
|
|
state[2] = LE32(magic + 8);
|
|
state[3] = LE32(magic + 12);
|
|
state[4] = LE32(key + 0);
|
|
state[5] = LE32(key + 4);
|
|
state[6] = LE32(key + 8);
|
|
state[7] = LE32(key + 12);
|
|
state[8] = LE32(key + 16);
|
|
state[9] = LE32(key + 20);
|
|
state[10] = LE32(key + 24);
|
|
state[11] = LE32(key + 28);
|
|
state[12] = counter ? LE32(counter + 0) : 0;
|
|
state[13] = counter ? LE32(counter + 4) : 0;
|
|
state[14] = LE32(iv + 0);
|
|
state[15] = LE32(iv + 4);
|
|
}
|
|
|
|
void RunChaCha20(uint32_t state[16], uint8_t out_buf[64])
|
|
{
|
|
uint32_t *out_buf32 = (uint32_t *)out_buf;
|
|
|
|
uint32_t x[16];
|
|
MemCpy(x, state, K_SIZE(x));
|
|
|
|
for (Size i = 0; i < 20; i += 2) {
|
|
x[0] += x[4]; x[12] = ROTL32(x[12] ^ x[0], 16);
|
|
x[1] += x[5]; x[13] = ROTL32(x[13] ^ x[1], 16);
|
|
x[2] += x[6]; x[14] = ROTL32(x[14] ^ x[2], 16);
|
|
x[3] += x[7]; x[15] = ROTL32(x[15] ^ x[3], 16);
|
|
|
|
x[8] += x[12]; x[4] = ROTL32(x[4] ^ x[8], 12);
|
|
x[9] += x[13]; x[5] = ROTL32(x[5] ^ x[9], 12);
|
|
x[10] += x[14]; x[6] = ROTL32(x[6] ^ x[10], 12);
|
|
x[11] += x[15]; x[7] = ROTL32(x[7] ^ x[11], 12);
|
|
|
|
x[0] += x[4]; x[12] = ROTL32(x[12] ^ x[0], 8);
|
|
x[1] += x[5]; x[13] = ROTL32(x[13] ^ x[1], 8);
|
|
x[2] += x[6]; x[14] = ROTL32(x[14] ^ x[2], 8);
|
|
x[3] += x[7]; x[15] = ROTL32(x[15] ^ x[3], 8);
|
|
|
|
x[8] += x[12]; x[4] = ROTL32(x[4] ^ x[8], 7);
|
|
x[9] += x[13]; x[5] = ROTL32(x[5] ^ x[9], 7);
|
|
x[10] += x[14]; x[6] = ROTL32(x[6] ^ x[10], 7);
|
|
x[11] += x[15]; x[7] = ROTL32(x[7] ^ x[11], 7);
|
|
|
|
x[0] += x[5]; x[15] = ROTL32(x[15] ^ x[0], 16);
|
|
x[1] += x[6]; x[12] = ROTL32(x[12] ^ x[1], 16);
|
|
x[2] += x[7]; x[13] = ROTL32(x[13] ^ x[2], 16);
|
|
x[3] += x[4]; x[14] = ROTL32(x[14] ^ x[3], 16);
|
|
|
|
x[10] += x[15]; x[5] = ROTL32(x[5] ^ x[10], 12);
|
|
x[11] += x[12]; x[6] = ROTL32(x[6] ^ x[11], 12);
|
|
x[8] += x[13]; x[7] = ROTL32(x[7] ^ x[8], 12);
|
|
x[9] += x[14]; x[4] = ROTL32(x[4] ^ x[9], 12);
|
|
|
|
x[0] += x[5]; x[15] = ROTL32(x[15] ^ x[0], 8);
|
|
x[1] += x[6]; x[12] = ROTL32(x[12] ^ x[1], 8);
|
|
x[2] += x[7]; x[13] = ROTL32(x[13] ^ x[2], 8);
|
|
x[3] += x[4]; x[14] = ROTL32(x[14] ^ x[3], 8);
|
|
|
|
x[10] += x[15]; x[5] = ROTL32(x[5] ^ x[10], 7);
|
|
x[11] += x[12]; x[6] = ROTL32(x[6] ^ x[11], 7);
|
|
x[8] += x[13]; x[7] = ROTL32(x[7] ^ x[8], 7);
|
|
x[9] += x[14]; x[4] = ROTL32(x[4] ^ x[9], 7);
|
|
}
|
|
|
|
for (Size i = 0; i < K_LEN(x); i++) {
|
|
out_buf32[i] = LittleEndian(x[i] + state[i]);
|
|
}
|
|
|
|
state[12]++;
|
|
state[13] += !state[12];
|
|
}
|
|
|
|
void FillRandomSafe(void *out_buf, Size len)
|
|
{
|
|
bool reseed = false;
|
|
|
|
// Reseed every 4 megabytes, or every hour, or after a fork
|
|
reseed |= (rnd_remain <= 0);
|
|
reseed |= (GetMonotonicClock() - rnd_clock > 3600 * 1000);
|
|
#if !defined(_WIN32)
|
|
reseed |= (getpid() != rnd_pid);
|
|
#endif
|
|
|
|
if (reseed) {
|
|
struct { uint8_t key[32]; uint8_t iv[8]; } buf;
|
|
|
|
MemSet(rnd_state, 0, K_SIZE(rnd_state));
|
|
#if defined(_WIN32)
|
|
K_CRITICAL(RtlGenRandom(&buf, K_SIZE(buf)), "RtlGenRandom() failed: %1", GetWin32ErrorString());
|
|
#elif defined(__linux__)
|
|
{
|
|
restart:
|
|
int ret = syscall(SYS_getrandom, &buf, K_SIZE(buf), 0);
|
|
K_CRITICAL(ret >= 0, "getrandom() failed: %1", strerror(errno));
|
|
|
|
if (ret < K_SIZE(buf)) [[unlikely]]
|
|
goto restart;
|
|
}
|
|
#else
|
|
K_CRITICAL(getentropy(&buf, K_SIZE(buf)) == 0, "getentropy() failed: %1", strerror(errno));
|
|
#endif
|
|
|
|
InitChaCha20(rnd_state, buf.key, buf.iv);
|
|
ZeroSafe(&buf, K_SIZE(buf));
|
|
|
|
rnd_remain = Mebibytes(4);
|
|
rnd_clock = GetMonotonicClock();
|
|
#if !defined(_WIN32)
|
|
rnd_pid = getpid();
|
|
#endif
|
|
|
|
rnd_offset = K_SIZE(rnd_buf);
|
|
}
|
|
|
|
Size copy_len = std::min(K_SIZE(rnd_buf) - rnd_offset, len);
|
|
MemCpy(out_buf, rnd_buf + rnd_offset, copy_len);
|
|
ZeroSafe(rnd_buf + rnd_offset, copy_len);
|
|
rnd_offset += copy_len;
|
|
|
|
for (Size i = copy_len; i < len; i += K_SIZE(rnd_buf)) {
|
|
RunChaCha20(rnd_state, rnd_buf);
|
|
|
|
copy_len = std::min(K_SIZE(rnd_buf), len - i);
|
|
MemCpy((uint8_t *)out_buf + i, rnd_buf, copy_len);
|
|
ZeroSafe(rnd_buf, copy_len);
|
|
rnd_offset = copy_len;
|
|
}
|
|
|
|
rnd_remain -= len;
|
|
}
|
|
|
|
FastRandom::FastRandom()
|
|
{
|
|
do {
|
|
FillRandomSafe(state, K_SIZE(state));
|
|
} while (std::all_of(std::begin(state), std::end(state), [](uint64_t v) { return !v; }));
|
|
}
|
|
|
|
FastRandom::FastRandom(uint64_t seed)
|
|
{
|
|
// splitmix64 generator to seed xoshiro256++, as recommended
|
|
|
|
seed += 0x9e3779b97f4a7c15;
|
|
|
|
for (int i = 0; i < 4; i++) {
|
|
seed = (seed ^ (seed >> 30)) * 0xbf58476d1ce4e5b9;
|
|
seed = (seed ^ (seed >> 27)) * 0x94d049bb133111eb;
|
|
state[i] = seed ^ (seed >> 31);
|
|
}
|
|
}
|
|
|
|
uint64_t FastRandom::Next()
|
|
{
|
|
// xoshiro256++ by David Blackman and Sebastiano Vigna (vigna@acm.org)
|
|
// Hopefully I did not screw it up :)
|
|
|
|
uint64_t result = ROTL64(state[0] + state[3], 23) + state[0];
|
|
uint64_t t = state[1] << 17;
|
|
|
|
state[2] ^= state[0];
|
|
state[3] ^= state[1];
|
|
state[1] ^= state[2];
|
|
state[0] ^= state[3];
|
|
state[2] ^= t;
|
|
state[3] = ROTL64(state[3], 45);
|
|
|
|
return result;
|
|
}
|
|
|
|
void FastRandom::Fill(void *out_buf, Size len)
|
|
{
|
|
for (Size i = 0; i < len; i += 8) {
|
|
uint64_t rnd = Next();
|
|
|
|
Size copy_len = std::min(K_SIZE(rnd), len - i);
|
|
MemCpy((uint8_t *)out_buf + i, &rnd, copy_len);
|
|
}
|
|
}
|
|
|
|
int FastRandom::GetInt(int min, int max)
|
|
{
|
|
int range = max - min;
|
|
|
|
if (range < 2) [[unlikely]] {
|
|
K_ASSERT(range >= 1);
|
|
return min;
|
|
}
|
|
|
|
unsigned int treshold = (UINT_MAX - UINT_MAX % range);
|
|
|
|
unsigned int x;
|
|
do {
|
|
x = (unsigned int)Next();
|
|
} while (x >= treshold);
|
|
x %= range;
|
|
|
|
return min + (int)x;
|
|
}
|
|
|
|
int64_t FastRandom::GetInt64(int64_t min, int64_t max)
|
|
{
|
|
int64_t range = max - min;
|
|
|
|
if (range < 2) [[unlikely]] {
|
|
K_ASSERT(range >= 1);
|
|
return min;
|
|
}
|
|
|
|
uint64_t treshold = (UINT64_MAX - UINT64_MAX % range);
|
|
|
|
uint64_t x;
|
|
do {
|
|
x = (uint64_t)Next();
|
|
} while (x >= treshold);
|
|
x %= range;
|
|
|
|
return min + (int64_t)x;
|
|
}
|
|
|
|
uint64_t GetRandom()
|
|
{
|
|
return rng_fast.Next();
|
|
}
|
|
|
|
int GetRandomInt(int min, int max)
|
|
{
|
|
return rng_fast.GetInt(min, max);
|
|
}
|
|
|
|
int64_t GetRandomInt64(int64_t min, int64_t max)
|
|
{
|
|
return rng_fast.GetInt64(min, max);
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Sockets
|
|
// ------------------------------------------------------------------------
|
|
|
|
#if !defined(__wasi__)
|
|
|
|
#if defined(_WIN32)
|
|
|
|
bool InitWinsock()
|
|
{
|
|
static bool ready = false;
|
|
static std::once_flag flag;
|
|
|
|
std::call_once(flag, []() {
|
|
WORD version = MAKEWORD(2, 2);
|
|
WSADATA wsa = {};
|
|
|
|
int ret = WSAStartup(version, &wsa);
|
|
|
|
if (ret) {
|
|
LogError("Failed to initialize Winsock: %1", GetWin32ErrorString(ret));
|
|
return;
|
|
}
|
|
|
|
K_ASSERT(LOBYTE(wsa.wVersion) == 2 && HIBYTE(wsa.wVersion) == 2);
|
|
atexit([]() { WSACleanup(); });
|
|
|
|
ready = true;
|
|
});
|
|
|
|
return ready;
|
|
}
|
|
|
|
int CreateSocket(SocketType type, int flags)
|
|
{
|
|
if (!InitWinsock())
|
|
return -1;
|
|
|
|
int family = 0;
|
|
|
|
switch (type) {
|
|
case SocketType::Dual:
|
|
case SocketType::IPv6: { family = AF_INET6; } break;
|
|
case SocketType::IPv4: { family = AF_INET; } break;
|
|
case SocketType::Unix: { family = AF_UNIX; } break;
|
|
}
|
|
|
|
bool overlapped = (flags & SOCK_OVERLAPPED);
|
|
flags &= ~SOCK_OVERLAPPED;
|
|
|
|
SOCKET sock = WSASocketW(family, flags, 0, nullptr, 0, overlapped ? WSA_FLAG_OVERLAPPED : 0);
|
|
if (sock == INVALID_SOCKET) {
|
|
LogError("Failed to create IP socket: %1", GetWin32ErrorString());
|
|
return -1;
|
|
}
|
|
K_DEFER_N(err_guard) { closesocket(sock); };
|
|
|
|
int reuse = 1;
|
|
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(reuse));
|
|
|
|
if (type == SocketType::Dual || type == SocketType::IPv6) {
|
|
int v6only = (type == SocketType::IPv6);
|
|
|
|
if (setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, (const char *)&v6only, sizeof(v6only)) < 0) {
|
|
LogError("Failed to change dual-stack socket option: %1", GetWin32ErrorString());
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
err_guard.Disable();
|
|
return (int)sock;
|
|
}
|
|
|
|
#else
|
|
|
|
int CreateSocket(SocketType type, int flags)
|
|
{
|
|
int family = 0;
|
|
|
|
switch (type) {
|
|
case SocketType::Dual:
|
|
case SocketType::IPv6: { family = AF_INET6; } break;
|
|
case SocketType::IPv4: { family = AF_INET; } break;
|
|
case SocketType::Unix: { family = AF_UNIX; } break;
|
|
}
|
|
|
|
#if defined(SOCK_CLOEXEC)
|
|
flags |= SOCK_CLOEXEC;
|
|
#endif
|
|
|
|
int sock = socket(family, flags, 0);
|
|
if (sock < 0) {
|
|
LogError("Failed to create IP socket: %1", strerror(errno));
|
|
return -1;
|
|
}
|
|
K_DEFER_N(err_guard) { close(sock); };
|
|
|
|
#if !defined(SOCK_CLOEXEC)
|
|
fcntl(sock, F_SETFD, FD_CLOEXEC);
|
|
#endif
|
|
|
|
int reuse = 1;
|
|
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
|
|
|
|
if (type == SocketType::Dual || type == SocketType::IPv6) {
|
|
int v6only = (type == SocketType::IPv6);
|
|
|
|
#if defined(__OpenBSD__)
|
|
if (!v6only) {
|
|
LogError("Dual-stack sockets are not supported on OpenBSD");
|
|
return -1;
|
|
}
|
|
#else
|
|
if (setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only)) < 0) {
|
|
LogError("Failed to change dual-stack socket option: %1", strerror(errno));
|
|
return -1;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
err_guard.Disable();
|
|
return (int)sock;
|
|
}
|
|
|
|
#endif
|
|
|
|
bool BindIPSocket(int sock, SocketType type, const char *addr, int port)
|
|
{
|
|
K_ASSERT(type == SocketType::Dual || type == SocketType::IPv4 || type == SocketType::IPv6);
|
|
|
|
if (type == SocketType::IPv4) {
|
|
struct sockaddr_in sa = {};
|
|
|
|
sa.sin_family = AF_INET;
|
|
sa.sin_port = htons((uint16_t)port);
|
|
|
|
if (addr) {
|
|
if (inet_pton(AF_INET, addr, &sa.sin_addr) <= 0) {
|
|
LogError("Invalid IPv4 address '%1'", addr);
|
|
return false;
|
|
}
|
|
} else {
|
|
sa.sin_addr.s_addr = htonl(INADDR_ANY);
|
|
}
|
|
|
|
if (bind(sock, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
|
|
#if defined(_WIN32)
|
|
LogError("Failed to bind to '%1:%2': %3", addr ? addr : "*", port, GetWin32ErrorString());
|
|
return false;
|
|
#else
|
|
LogError("Failed to bind to '%1:%2': %3", addr ? addr : "*", port, strerror(errno));
|
|
return false;
|
|
#endif
|
|
}
|
|
} else {
|
|
struct sockaddr_in6 sa = {};
|
|
|
|
sa.sin6_family = AF_INET6;
|
|
sa.sin6_port = htons((uint16_t)port);
|
|
|
|
if (addr) {
|
|
if (!strchr(addr, ':')) {
|
|
char buf[512];
|
|
Fmt(buf, "::FFFF:%1", addr);
|
|
|
|
if (inet_pton(AF_INET6, buf, &sa.sin6_addr) <= 0) {
|
|
LogError("Invalid IPv4 or IPv6 address '%1'", addr);
|
|
return false;
|
|
}
|
|
} else {
|
|
if (inet_pton(AF_INET6, addr, &sa.sin6_addr) <= 0) {
|
|
LogError("Invalid IPv6 address '%1'", addr);
|
|
return false;
|
|
}
|
|
}
|
|
} else {
|
|
sa.sin6_addr = IN6ADDR_ANY_INIT;
|
|
}
|
|
|
|
if (bind(sock, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
|
|
#if defined(_WIN32)
|
|
LogError("Failed to bind to '%1:%2': %3", addr ? addr : "*", port, GetWin32ErrorString());
|
|
return false;
|
|
#else
|
|
LogError("Failed to bind to '%1:%2': %3", addr ? addr : "*", port, strerror(errno));
|
|
return false;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool BindUnixSocket(int sock, const char *path)
|
|
{
|
|
struct sockaddr_un sa = {};
|
|
|
|
// Protect against abtract Unix sockets on Linux
|
|
if (!path[0]) {
|
|
LogError("Cannot open empty UNIX socket");
|
|
return false;
|
|
}
|
|
|
|
sa.sun_family = AF_UNIX;
|
|
if (!CopyString(path, sa.sun_path)) {
|
|
LogError("Excessive UNIX socket path length");
|
|
return false;
|
|
}
|
|
|
|
#if !defined(_WIN32)
|
|
// Remove existing socket (if any)
|
|
{
|
|
struct stat sb;
|
|
if (!stat(path, &sb) && S_ISSOCK(sb.st_mode)) {
|
|
LogDebug("Removing existing socket '%1'", path);
|
|
unlink(path);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (bind(sock, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
|
|
#if defined(_WIN32)
|
|
LogError("Failed to bind socket to '%1': %2", path, GetWin32ErrorString());
|
|
return false;
|
|
#else
|
|
LogError("Failed to bind socket to '%1': %2", path, strerror(errno));
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
#if !defined(_WIN32)
|
|
chmod(path, 0666);
|
|
#endif
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ConnectIPSocket(int sock, const char *addr, int port)
|
|
{
|
|
if (strchr(addr, ':')) {
|
|
struct sockaddr_in6 sa = {};
|
|
|
|
sa.sin6_family = AF_INET6;
|
|
sa.sin6_port = htons((unsigned short)port);
|
|
|
|
if (inet_pton(AF_INET6, addr, &sa.sin6_addr) <= 0) {
|
|
LogError("Invalid IPv6 address '%1'", addr);
|
|
return false;
|
|
}
|
|
|
|
if (connect(sock, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
|
|
#if defined(_WIN32)
|
|
LogError("Failed to connect to '%1' (%2): %3", addr, port, GetWin32ErrorString());
|
|
return false;
|
|
#else
|
|
LogError("Failed to connect to '%1' (%2): %3", addr, port, strerror(errno));
|
|
return false;
|
|
#endif
|
|
}
|
|
} else {
|
|
struct sockaddr_in sa = {};
|
|
|
|
sa.sin_family = AF_INET;
|
|
sa.sin_port = htons((unsigned short)port);
|
|
|
|
if (inet_pton(AF_INET, addr, &sa.sin_addr) <= 0) {
|
|
LogError("Invalid IPv4 address '%1'", addr);
|
|
return false;
|
|
}
|
|
|
|
if (connect(sock, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
|
|
#if defined(_WIN32)
|
|
LogError("Failed to connect to '%1' (%2): %3", addr, port, GetWin32ErrorString());
|
|
return false;
|
|
#else
|
|
LogError("Failed to connect to '%1' (%2): %3", addr, port, strerror(errno));
|
|
return false;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ConnectUnixSocket(int sock, const char *path)
|
|
{
|
|
struct sockaddr_un sa = {};
|
|
|
|
sa.sun_family = AF_UNIX;
|
|
if (!CopyString(path, sa.sun_path)) {
|
|
LogError("Excessive UNIX socket path length");
|
|
return false;
|
|
}
|
|
|
|
if (connect(sock, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
|
|
#if defined(_WIN32)
|
|
LogError("Failed to connect to UNIX socket '%1': %2", path, GetWin32ErrorString());
|
|
return false;
|
|
#else
|
|
LogError("Failed to connect to UNIX socket '%1': %2", path, strerror(errno));
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void SetDescriptorNonBlock(int fd, bool enable)
|
|
{
|
|
#if defined(_WIN32)
|
|
unsigned long mode = enable;
|
|
ioctlsocket((SOCKET)fd, FIONBIO, &mode);
|
|
#else
|
|
int flags = fcntl(fd, F_GETFL, 0);
|
|
flags = ApplyMask(flags, O_NONBLOCK, enable);
|
|
fcntl(fd, F_SETFL, flags);
|
|
#endif
|
|
}
|
|
|
|
void SetDescriptorRetain(int fd, bool retain)
|
|
{
|
|
#if defined(TCP_CORK)
|
|
int flag = retain;
|
|
setsockopt(fd, IPPROTO_TCP, TCP_CORK, &flag, sizeof(flag));
|
|
#elif defined(TCP_NOPUSH)
|
|
int flag = retain;
|
|
setsockopt(fd, IPPROTO_TCP, TCP_NOPUSH, &flag, sizeof(flag));
|
|
|
|
#if defined(__APPLE__)
|
|
if (!retain) {
|
|
send(fd, nullptr, 0, MSG_NOSIGNAL);
|
|
}
|
|
#endif
|
|
#else
|
|
// Nothing to see here
|
|
|
|
(void)fd;
|
|
(void)retain;
|
|
#endif
|
|
}
|
|
|
|
void CloseSocket(int fd)
|
|
{
|
|
if (fd < 0)
|
|
return;
|
|
|
|
#if defined(_WIN32)
|
|
shutdown((SOCKET)fd, SD_BOTH);
|
|
closesocket((SOCKET)fd);
|
|
#else
|
|
shutdown(fd, SHUT_RDWR);
|
|
close(fd);
|
|
#endif
|
|
}
|
|
|
|
#endif
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Tasks
|
|
// ------------------------------------------------------------------------
|
|
|
|
#if !defined(__wasi__)
|
|
|
|
struct Task {
|
|
Async *async;
|
|
std::function<bool()> func;
|
|
};
|
|
|
|
struct WorkerData {
|
|
AsyncPool *pool = nullptr;
|
|
int idx;
|
|
|
|
std::mutex queue_mutex;
|
|
BucketArray<Task> tasks;
|
|
};
|
|
|
|
class AsyncPool {
|
|
K_DELETE_COPY(AsyncPool)
|
|
|
|
std::mutex pool_mutex;
|
|
std::condition_variable pending_cv;
|
|
std::condition_variable sync_cv;
|
|
|
|
// Manipulate with pool_mutex locked
|
|
int refcount = 0;
|
|
|
|
int async_count = 0;
|
|
std::atomic_uint next_worker { 0 };
|
|
HeapArray<WorkerData> workers;
|
|
std::atomic_int pending_tasks { 0 };
|
|
|
|
public:
|
|
AsyncPool(int threads, bool leak);
|
|
|
|
int GetWorkerCount() const { return (int)workers.len; }
|
|
|
|
void RegisterAsync();
|
|
void UnregisterAsync();
|
|
|
|
void AddTask(Async *async, const std::function<bool()> &func);
|
|
void AddTask(Async *async, int worker_idx, const std::function<bool()> &func);
|
|
|
|
void RunWorker(int worker_idx);
|
|
void SyncOn(Async *async, bool soon);
|
|
bool WaitOn(Async *async, int timeout);
|
|
|
|
void RunTasks(int worker_idx, Async *only);
|
|
void RunTask(Task *task);
|
|
};
|
|
|
|
// thread_local breaks down on MinGW when destructors are involved, work
|
|
// around this with heap allocation.
|
|
static thread_local AsyncPool *async_default_pool = nullptr;
|
|
static thread_local AsyncPool *async_running_pool = nullptr;
|
|
static thread_local int async_running_worker_idx;
|
|
static thread_local bool async_running_task = false;
|
|
|
|
Async::Async(int threads)
|
|
{
|
|
K_ASSERT(threads);
|
|
|
|
if (threads > 0) {
|
|
pool = new AsyncPool(threads, false);
|
|
} else if (async_running_pool) {
|
|
pool = async_running_pool;
|
|
} else {
|
|
if (!async_default_pool) {
|
|
// NOTE: We're leaking one AsyncPool each time a non-worker thread uses Async()
|
|
// for the first time. That's only one leak in most cases, when the main thread
|
|
// is the only non-worker thread using Async, but still. Something to keep in mind.
|
|
|
|
threads = GetCoreCount();
|
|
async_default_pool = new AsyncPool(threads, true);
|
|
}
|
|
|
|
pool = async_default_pool;
|
|
}
|
|
|
|
pool->RegisterAsync();
|
|
}
|
|
|
|
Async::Async(Async *parent)
|
|
{
|
|
K_ASSERT(parent);
|
|
|
|
pool = parent->pool;
|
|
pool->RegisterAsync();
|
|
}
|
|
|
|
Async::~Async()
|
|
{
|
|
success = false;
|
|
Sync();
|
|
|
|
pool->UnregisterAsync();
|
|
}
|
|
|
|
void Async::Run(const std::function<bool()> &func)
|
|
{
|
|
pool->AddTask(this, func);
|
|
}
|
|
|
|
void Async::Run(int worker, const std::function<bool()> &func)
|
|
{
|
|
pool->AddTask(this, worker, func);
|
|
}
|
|
|
|
bool Async::Sync()
|
|
{
|
|
pool->SyncOn(this, false);
|
|
return success;
|
|
}
|
|
|
|
bool Async::SyncSoon()
|
|
{
|
|
pool->SyncOn(this, true);
|
|
return success;
|
|
}
|
|
|
|
bool Async::Wait(int timeout)
|
|
{
|
|
return pool->WaitOn(this, timeout);
|
|
}
|
|
|
|
int Async::GetWorkerCount()
|
|
{
|
|
return pool->GetWorkerCount();
|
|
}
|
|
|
|
bool Async::IsTaskRunning()
|
|
{
|
|
return async_running_task;
|
|
}
|
|
|
|
int Async::GetWorkerIdx()
|
|
{
|
|
return async_running_worker_idx;
|
|
}
|
|
|
|
AsyncPool::AsyncPool(int threads, bool leak)
|
|
{
|
|
if (threads > K_ASYNC_MAX_THREADS) {
|
|
LogError("Async cannot use more than %1 threads", K_ASYNC_MAX_THREADS);
|
|
threads = K_ASYNC_MAX_THREADS;
|
|
}
|
|
|
|
// The first queue is for the main thread
|
|
workers.AppendDefault(threads);
|
|
|
|
refcount = leak;
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
|
|
static DWORD WINAPI RunWorkerWin32(void *udata)
|
|
{
|
|
WorkerData *worker = (WorkerData *)udata;
|
|
worker->pool->RunWorker(worker->idx);
|
|
return 0;
|
|
}
|
|
|
|
#else
|
|
|
|
static void *RunWorkerPthread(void *udata)
|
|
{
|
|
WorkerData *worker = (WorkerData *)udata;
|
|
worker->pool->RunWorker(worker->idx);
|
|
return nullptr;
|
|
}
|
|
|
|
#endif
|
|
|
|
void AsyncPool::RegisterAsync()
|
|
{
|
|
std::lock_guard<std::mutex> lock_pool(pool_mutex);
|
|
|
|
if (!async_count++) {
|
|
for (int i = 1; i < workers.len; i++) {
|
|
WorkerData *worker = &workers[i];
|
|
|
|
if (!worker->pool) {
|
|
worker->pool = this;
|
|
worker->idx = i;
|
|
|
|
#if defined(_WIN32)
|
|
// Our worker threads may exit after main() has returned (or exit has been called),
|
|
// which can trigger crashes in _Cnd_do_broadcast_at_thread_exit() because it
|
|
// tries to dereference destroyed stuff. It turns out that std::thread calls this
|
|
// function, and we don't want that, so avoid std::thread on Windows.
|
|
HANDLE h = CreateThread(nullptr, 0, RunWorkerWin32, worker, 0, nullptr);
|
|
if (!h) [[unlikely]] {
|
|
LogError("Failed to create worker thread: %1", GetWin32ErrorString());
|
|
|
|
worker->pool = nullptr;
|
|
return;
|
|
}
|
|
|
|
CloseHandle(h);
|
|
#else
|
|
pthread_t thread;
|
|
int ret = pthread_create(&thread, nullptr, RunWorkerPthread, worker);
|
|
if (ret) [[unlikely]] {
|
|
LogError("Failed to create worker thread: %1", strerror(ret));
|
|
|
|
worker->pool = nullptr;
|
|
return;
|
|
}
|
|
|
|
pthread_detach(thread);
|
|
#endif
|
|
|
|
refcount++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void AsyncPool::UnregisterAsync()
|
|
{
|
|
std::lock_guard<std::mutex> lock_pool(pool_mutex);
|
|
async_count--;
|
|
}
|
|
|
|
void AsyncPool::AddTask(Async *async, const std::function<bool()> &func)
|
|
{
|
|
if (async_running_pool != this) {
|
|
int worker_idx = (next_worker++ % (int)workers.len);
|
|
AddTask(async, worker_idx, func);
|
|
} else {
|
|
AddTask(async, async_running_worker_idx, func);
|
|
}
|
|
}
|
|
|
|
void AsyncPool::AddTask(Async *async, int worker_idx, const std::function<bool()> &func)
|
|
{
|
|
WorkerData *worker = &workers[worker_idx];
|
|
|
|
// Add the task damn it
|
|
{
|
|
std::lock_guard<std::mutex> lock_queue(worker->queue_mutex);
|
|
worker->tasks.Append({ async, func });
|
|
}
|
|
|
|
async->remaining_tasks++;
|
|
|
|
int prev_pending = pending_tasks++;
|
|
|
|
if (prev_pending >= K_ASYNC_MAX_PENDING_TASKS) {
|
|
int worker_idx = async_running_worker_idx;
|
|
|
|
do {
|
|
RunTasks(worker_idx, nullptr);
|
|
} while (pending_tasks >= K_ASYNC_MAX_PENDING_TASKS);
|
|
} else if (!prev_pending) {
|
|
std::lock_guard<std::mutex> lock_pool(pool_mutex);
|
|
|
|
pending_cv.notify_all();
|
|
sync_cv.notify_all();
|
|
}
|
|
}
|
|
|
|
void AsyncPool::RunWorker(int worker_idx)
|
|
{
|
|
async_running_pool = this;
|
|
async_running_worker_idx = worker_idx;
|
|
|
|
std::unique_lock<std::mutex> lock_pool(pool_mutex);
|
|
|
|
while (async_count) {
|
|
lock_pool.unlock();
|
|
RunTasks(worker_idx, nullptr);
|
|
lock_pool.lock();
|
|
|
|
std::chrono::duration<int, std::milli> duration(K_ASYNC_MAX_IDLE_TIME); // Thanks C++
|
|
pending_cv.wait_for(lock_pool, duration, [&]() { return !!pending_tasks; });
|
|
}
|
|
|
|
workers[worker_idx].pool = nullptr;
|
|
|
|
if (!--refcount) {
|
|
lock_pool.unlock();
|
|
delete this;
|
|
}
|
|
}
|
|
|
|
void AsyncPool::SyncOn(Async *async, bool soon)
|
|
{
|
|
K_DEFER_C(pool = async_running_pool,
|
|
worker_idx = async_running_worker_idx) {
|
|
async_running_pool = pool;
|
|
async_running_worker_idx = worker_idx;
|
|
};
|
|
|
|
async_running_pool = this;
|
|
async_running_worker_idx = 0;
|
|
|
|
while (async->remaining_tasks) {
|
|
RunTasks(0, soon ? async : nullptr);
|
|
|
|
std::unique_lock<std::mutex> lock_sync(pool_mutex);
|
|
sync_cv.wait(lock_sync, [&]() { return pending_tasks || !async->remaining_tasks; });
|
|
}
|
|
}
|
|
|
|
bool AsyncPool::WaitOn(Async *async, int timeout)
|
|
{
|
|
std::unique_lock<std::mutex> lock_sync(pool_mutex);
|
|
|
|
if (timeout >= 0) {
|
|
std::chrono::milliseconds delay(timeout);
|
|
bool done = sync_cv.wait_for(lock_sync, delay, [&]() { return !async->remaining_tasks; });
|
|
return done;
|
|
} else {
|
|
sync_cv.wait(lock_sync, [&]() { return !async->remaining_tasks; });
|
|
return true;
|
|
}
|
|
}
|
|
|
|
void AsyncPool::RunTasks(int worker_idx, Async *only)
|
|
{
|
|
// The '12' factor is pretty arbitrary, don't try to find meaning there
|
|
for (int i = 0; i < workers.len * 12; i++) {
|
|
WorkerData *worker = &workers[worker_idx];
|
|
std::unique_lock<std::mutex> lock_queue(worker->queue_mutex, std::try_to_lock);
|
|
|
|
if (lock_queue.owns_lock()) {
|
|
Size idx = 0;
|
|
|
|
if (only) {
|
|
for (const Task &task: worker->tasks) {
|
|
if (task.async == only) {
|
|
std::swap(worker->tasks[0], worker->tasks[idx]);
|
|
break;
|
|
}
|
|
idx++;
|
|
}
|
|
}
|
|
|
|
if (idx < worker->tasks.count) {
|
|
Task task = std::move(worker->tasks[0]);
|
|
|
|
worker->tasks.RemoveFirst();
|
|
worker->tasks.Trim();
|
|
|
|
lock_queue.unlock();
|
|
|
|
RunTask(&task);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
worker_idx = GetRandomInt(0, (int)workers.len);
|
|
}
|
|
}
|
|
|
|
void AsyncPool::RunTask(Task *task)
|
|
{
|
|
Async *async = task->async;
|
|
|
|
K_DEFER_C(running = async_running_task) { async_running_task = running; };
|
|
async_running_task = true;
|
|
|
|
pending_tasks--;
|
|
|
|
if (!task->func()) {
|
|
async->success = false;
|
|
}
|
|
|
|
if (!--async->remaining_tasks) {
|
|
std::lock_guard<std::mutex> lock_sync(pool_mutex);
|
|
sync_cv.notify_all();
|
|
}
|
|
}
|
|
|
|
#else
|
|
|
|
|
|
Async::Async(int threads)
|
|
{
|
|
K_ASSERT(threads);
|
|
}
|
|
|
|
Async::Async(Async *parent)
|
|
{
|
|
K_ASSERT(parent);
|
|
}
|
|
|
|
Async::~Async()
|
|
{
|
|
// Nothing to do
|
|
}
|
|
|
|
void Async::Run(const std::function<bool()> &func)
|
|
{
|
|
success &= !!func();
|
|
}
|
|
|
|
void Async::Run(int, const std::function<bool()> &func)
|
|
{
|
|
success &= !!func();
|
|
}
|
|
|
|
bool Async::Sync()
|
|
{
|
|
return success;
|
|
}
|
|
|
|
bool Async::IsTaskRunning()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
int Async::GetWorkerIdx()
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
int Async::GetWorkerCount()
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
#endif
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Streams
|
|
// ------------------------------------------------------------------------
|
|
|
|
static NoDestroy<StreamReader> StdInStream(STDIN_FILENO, "<stdin>");
|
|
static NoDestroy<StreamWriter> StdOutStream(STDOUT_FILENO, "<stdout>", (int)StreamWriterFlag::LineBuffer);
|
|
static NoDestroy<StreamWriter> StdErrStream(STDERR_FILENO, "<stderr>", (int)StreamWriterFlag::LineBuffer);
|
|
|
|
extern StreamReader *const StdIn = StdInStream.Get();
|
|
extern StreamWriter *const StdOut = StdOutStream.Get();
|
|
extern StreamWriter *const StdErr = StdErrStream.Get();
|
|
|
|
static CreateDecompressorFunc *DecompressorFunctions[K_LEN(CompressionTypeNames)];
|
|
static CreateCompressorFunc *CompressorFunctions[K_LEN(CompressionTypeNames)];
|
|
|
|
K_EXIT(FlushStd)
|
|
{
|
|
StdOut->Flush();
|
|
StdErr->Flush();
|
|
}
|
|
|
|
void StreamReader::SetDecoder(StreamDecoder *decoder)
|
|
{
|
|
K_ASSERT(decoder);
|
|
K_ASSERT(!filename);
|
|
K_ASSERT(!this->decoder);
|
|
|
|
this->decoder = decoder;
|
|
}
|
|
|
|
bool StreamReader::Open(Span<const uint8_t> buf, const char *filename, CompressionType compression_type)
|
|
{
|
|
Close(true);
|
|
|
|
K_DEFER_N(err_guard) { error = true; };
|
|
|
|
error = false;
|
|
raw_read = 0;
|
|
read_total = 0;
|
|
read_max = -1;
|
|
|
|
K_ASSERT(filename);
|
|
this->filename = DuplicateString(filename, &str_alloc).ptr;
|
|
|
|
source.type = SourceType::Memory;
|
|
source.u.memory.buf = buf;
|
|
source.u.memory.pos = 0;
|
|
|
|
if (!InitDecompressor(compression_type))
|
|
return false;
|
|
|
|
err_guard.Disable();
|
|
return true;
|
|
}
|
|
|
|
bool StreamReader::Open(int fd, const char *filename, CompressionType compression_type)
|
|
{
|
|
Close(true);
|
|
|
|
K_DEFER_N(err_guard) { error = true; };
|
|
|
|
error = false;
|
|
raw_read = 0;
|
|
read_total = 0;
|
|
read_max = -1;
|
|
|
|
K_ASSERT(fd >= 0);
|
|
K_ASSERT(filename);
|
|
this->filename = DuplicateString(filename, &str_alloc).ptr;
|
|
|
|
source.type = SourceType::File;
|
|
source.u.file.fd = fd;
|
|
source.u.file.owned = false;
|
|
|
|
if (!InitDecompressor(compression_type))
|
|
return false;
|
|
|
|
err_guard.Disable();
|
|
return true;
|
|
}
|
|
|
|
OpenResult StreamReader::Open(const char *filename, CompressionType compression_type)
|
|
{
|
|
Close(true);
|
|
|
|
K_DEFER_N(err_guard) { error = true; };
|
|
|
|
error = false;
|
|
raw_read = 0;
|
|
read_total = 0;
|
|
read_max = -1;
|
|
|
|
K_ASSERT(filename);
|
|
this->filename = DuplicateString(filename, &str_alloc).ptr;
|
|
|
|
source.type = SourceType::File;
|
|
{
|
|
OpenResult ret = OpenFile(filename, (int)OpenFlag::Read, &source.u.file.fd);
|
|
if (ret != OpenResult::Success)
|
|
return ret;
|
|
}
|
|
source.u.file.owned = true;
|
|
|
|
if (!InitDecompressor(compression_type))
|
|
return OpenResult::OtherError;
|
|
|
|
err_guard.Disable();
|
|
return OpenResult::Success;
|
|
}
|
|
|
|
bool StreamReader::Open(const std::function<Size(Span<uint8_t>)> &func, const char *filename, CompressionType compression_type)
|
|
{
|
|
Close(true);
|
|
|
|
K_DEFER_N(err_guard) { error = true; };
|
|
|
|
error = false;
|
|
raw_read = 0;
|
|
read_total = 0;
|
|
read_max = -1;
|
|
|
|
K_ASSERT(filename);
|
|
this->filename = DuplicateString(filename, &str_alloc).ptr;
|
|
|
|
source.type = SourceType::Function;
|
|
new (&source.u.func) std::function<Size(Span<uint8_t>)>(func);
|
|
|
|
if (!InitDecompressor(compression_type))
|
|
return false;
|
|
|
|
err_guard.Disable();
|
|
return true;
|
|
}
|
|
|
|
bool StreamReader::Close(bool implicit)
|
|
{
|
|
K_ASSERT(implicit || this != StdIn);
|
|
|
|
if (decoder) {
|
|
delete decoder;
|
|
decoder = nullptr;
|
|
}
|
|
|
|
switch (source.type) {
|
|
case SourceType::Memory: { source.u.memory = {}; } break;
|
|
case SourceType::File: {
|
|
if (source.u.file.owned && source.u.file.fd >= 0) {
|
|
CloseDescriptor(source.u.file.fd);
|
|
}
|
|
|
|
source.u.file.fd = -1;
|
|
source.u.file.owned = false;
|
|
} break;
|
|
case SourceType::Function: { source.u.func.~function(); } break;
|
|
}
|
|
|
|
bool ret = !filename || !error;
|
|
|
|
filename = nullptr;
|
|
error = true;
|
|
source.type = SourceType::Memory;
|
|
source.eof = false;
|
|
eof = false;
|
|
raw_len = -1;
|
|
str_alloc.Reset();
|
|
|
|
return ret;
|
|
}
|
|
|
|
bool StreamReader::Rewind()
|
|
{
|
|
if (error) [[unlikely]]
|
|
return false;
|
|
|
|
if (decoder) [[unlikely]] {
|
|
LogError("Cannot rewind stream with decoder");
|
|
return false;
|
|
}
|
|
|
|
switch (source.type) {
|
|
case SourceType::Memory: { source.u.memory.pos = 0; } break;
|
|
case SourceType::File: {
|
|
if (lseek(source.u.file.fd, 0, SEEK_SET) < 0) {
|
|
LogError("Failed to rewind '%1': %2", filename, strerror(errno));
|
|
error = true;
|
|
return false;
|
|
}
|
|
} break;
|
|
case SourceType::Function: {
|
|
LogError("Cannot rewind stream '%1'", filename);
|
|
error = true;
|
|
return false;
|
|
} break;
|
|
}
|
|
|
|
source.eof = false;
|
|
raw_len = -1;
|
|
raw_read = 0;
|
|
eof = false;
|
|
|
|
return true;
|
|
}
|
|
|
|
int StreamReader::GetDescriptor() const
|
|
{
|
|
K_ASSERT(source.type == SourceType::File);
|
|
return source.u.file.fd;
|
|
}
|
|
|
|
void StreamReader::SetDescriptorOwned(bool owned)
|
|
{
|
|
K_ASSERT(source.type == SourceType::File);
|
|
source.u.file.owned = owned;
|
|
}
|
|
|
|
Size StreamReader::Read(Span<uint8_t> out_buf)
|
|
{
|
|
#if !defined(__wasm__)
|
|
std::lock_guard<std::mutex> lock(mutex);
|
|
#endif
|
|
|
|
if (error) [[unlikely]]
|
|
return -1;
|
|
|
|
Size len = 0;
|
|
|
|
if (decoder) {
|
|
len = decoder->Read(out_buf.len, out_buf.ptr);
|
|
if (len < 0) [[unlikely]] {
|
|
error = true;
|
|
return -1;
|
|
}
|
|
} else {
|
|
len = ReadRaw(out_buf.len, out_buf.ptr);
|
|
if (len < 0) [[unlikely]]
|
|
return -1;
|
|
eof = source.eof;
|
|
}
|
|
|
|
if (!error && read_max >= 0 && len > read_max - read_total) [[unlikely]] {
|
|
LogError("Exceeded max stream size of %1", FmtDiskSize(read_max));
|
|
error = true;
|
|
return -1;
|
|
}
|
|
|
|
read_total += len;
|
|
return len;
|
|
}
|
|
|
|
Size StreamReader::ReadFill(Span<uint8_t> out_buf)
|
|
{
|
|
#if !defined(__wasm__)
|
|
std::lock_guard<std::mutex> lock(mutex);
|
|
#endif
|
|
|
|
if (error) [[unlikely]]
|
|
return -1;
|
|
|
|
Size read_len = 0;
|
|
|
|
while (out_buf.len) {
|
|
Size len = 0;
|
|
|
|
if (decoder) {
|
|
len = decoder->Read(out_buf.len, out_buf.ptr);
|
|
if (len < 0) [[unlikely]] {
|
|
error = true;
|
|
return -1;
|
|
}
|
|
} else {
|
|
len = ReadRaw(out_buf.len, out_buf.ptr);
|
|
if (len < 0) [[unlikely]]
|
|
return -1;
|
|
eof = source.eof;
|
|
}
|
|
|
|
out_buf.ptr += len;
|
|
out_buf.len -= len;
|
|
read_len += len;
|
|
|
|
if (!error && read_max >= 0 && read_len > read_max - read_total) [[unlikely]] {
|
|
LogError("Exceeded max stream size of %1", FmtDiskSize(read_max));
|
|
error = true;
|
|
return -1;
|
|
}
|
|
|
|
if (eof)
|
|
break;
|
|
}
|
|
|
|
read_total += read_len;
|
|
return read_len;
|
|
}
|
|
|
|
Size StreamReader::ReadAll(Size max_len, HeapArray<uint8_t> *out_buf)
|
|
{
|
|
if (error) [[unlikely]]
|
|
return -1;
|
|
|
|
K_DEFER_NC(buf_guard, buf_len = out_buf->len) { out_buf->RemoveFrom(buf_len); };
|
|
|
|
// Check virtual memory limits
|
|
{
|
|
Size memory_max = K_SIZE_MAX - out_buf->len - 1;
|
|
|
|
if (memory_max <= 0) [[unlikely]] {
|
|
LogError("Exhausted memory limit reading file '%1'", filename);
|
|
return -1;
|
|
}
|
|
|
|
K_ASSERT(max_len);
|
|
max_len = (max_len >= 0) ? std::min(max_len, memory_max) : memory_max;
|
|
}
|
|
|
|
// For some files (such as in /proc), the file size is reported as 0 even though there
|
|
// is content inside, because these files are generated on demand. So we need to take
|
|
// the slow path for apparently empty files.
|
|
if (!decoder && ComputeRawLen() > 0) {
|
|
if (raw_len > max_len) {
|
|
LogError("File '%1' is too large (limit = %2)", filename, FmtDiskSize(max_len));
|
|
return -1;
|
|
}
|
|
|
|
// Count one trailing byte (if possible) to avoid reallocation for users
|
|
// who need/want to append a NUL character.
|
|
out_buf->Grow((Size)raw_len + 1);
|
|
|
|
Size read_len = ReadFill(out_buf->TakeAvailable());
|
|
if (read_len < 0)
|
|
return -1;
|
|
out_buf->len += (Size)std::min(raw_len, (int64_t)read_len);
|
|
|
|
buf_guard.Disable();
|
|
return read_len;
|
|
} else {
|
|
Size total_len = 0;
|
|
|
|
while (!eof) {
|
|
Size grow = std::min(total_len ? Megabytes(1) : Kibibytes(64), K_SIZE_MAX - out_buf->len);
|
|
out_buf->Grow(grow);
|
|
|
|
Size read_len = Read(out_buf->TakeAvailable());
|
|
if (read_len < 0)
|
|
return -1;
|
|
|
|
if (read_len > max_len - total_len) [[unlikely]] {
|
|
LogError("File '%1' is too large (limit = %2)", filename, FmtDiskSize(max_len));
|
|
return -1;
|
|
}
|
|
|
|
total_len += read_len;
|
|
out_buf->len += read_len;
|
|
}
|
|
|
|
buf_guard.Disable();
|
|
return total_len;
|
|
}
|
|
}
|
|
|
|
int64_t StreamReader::ComputeRawLen()
|
|
{
|
|
if (error) [[unlikely]]
|
|
return -1;
|
|
if (raw_read || raw_len >= 0)
|
|
return raw_len;
|
|
|
|
switch (source.type) {
|
|
case SourceType::Memory: {
|
|
raw_len = source.u.memory.buf.len;
|
|
} break;
|
|
|
|
case SourceType::File: {
|
|
#if defined(_WIN32)
|
|
struct __stat64 sb;
|
|
if (_fstat64(source.u.file.fd, &sb) < 0)
|
|
return -1;
|
|
raw_len = (int64_t)sb.st_size;
|
|
#else
|
|
struct stat sb;
|
|
if (fstat(source.u.file.fd, &sb) < 0 || S_ISFIFO(sb.st_mode) | S_ISSOCK(sb.st_mode))
|
|
return -1;
|
|
raw_len = (int64_t)sb.st_size;
|
|
#endif
|
|
} break;
|
|
|
|
case SourceType::Function: {
|
|
return -1;
|
|
} break;
|
|
}
|
|
|
|
return raw_len;
|
|
}
|
|
|
|
bool StreamReader::InitDecompressor(CompressionType type)
|
|
{
|
|
if (type != CompressionType::None) {
|
|
CreateDecompressorFunc *func = DecompressorFunctions[(int)type];
|
|
|
|
if (!func) {
|
|
LogError("%1 decompression is not available for '%2'", CompressionTypeNames[(int)type], filename);
|
|
error = true;
|
|
return false;
|
|
}
|
|
|
|
decoder = func(this, type);
|
|
K_ASSERT(decoder);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
Size StreamReader::ReadRaw(Size max_len, void *out_buf)
|
|
{
|
|
ComputeRawLen();
|
|
|
|
Size read_len = 0;
|
|
switch (source.type) {
|
|
case SourceType::Memory: {
|
|
read_len = source.u.memory.buf.len - source.u.memory.pos;
|
|
if (read_len > max_len) {
|
|
read_len = max_len;
|
|
}
|
|
MemCpy(out_buf, source.u.memory.buf.ptr + source.u.memory.pos, read_len);
|
|
source.u.memory.pos += read_len;
|
|
source.eof = (source.u.memory.pos >= source.u.memory.buf.len);
|
|
} break;
|
|
|
|
case SourceType::File: {
|
|
#if defined(_WIN32)
|
|
max_len = std::min(max_len, (Size)UINT_MAX);
|
|
read_len = _read(source.u.file.fd, out_buf, (unsigned int)max_len);
|
|
#else
|
|
read_len = K_RESTART_EINTR(read(source.u.file.fd, out_buf, (size_t)max_len), < 0);
|
|
#endif
|
|
if (read_len < 0) {
|
|
LogError("Error while reading file '%1': %2", filename, strerror(errno));
|
|
error = true;
|
|
return -1;
|
|
}
|
|
source.eof = (read_len == 0);
|
|
} break;
|
|
|
|
case SourceType::Function: {
|
|
read_len = source.u.func(MakeSpan((uint8_t *)out_buf, max_len));
|
|
if (read_len < 0) {
|
|
error = true;
|
|
return -1;
|
|
}
|
|
source.eof = (read_len == 0);
|
|
} break;
|
|
}
|
|
|
|
raw_read += read_len;
|
|
return read_len;
|
|
}
|
|
|
|
StreamDecompressorHelper::StreamDecompressorHelper(CompressionType compression_type, CreateDecompressorFunc *func)
|
|
{
|
|
K_ASSERT(!DecompressorFunctions[(int)compression_type]);
|
|
DecompressorFunctions[(int)compression_type] = func;
|
|
}
|
|
|
|
// XXX: Maximum line length
|
|
bool LineReader::Next(Span<char> *out_line)
|
|
{
|
|
if (eof) {
|
|
line_number = 0;
|
|
return false;
|
|
}
|
|
if (error) [[unlikely]]
|
|
return false;
|
|
|
|
for (;;) {
|
|
if (!view.len) {
|
|
buf.Grow(K_LINE_READER_STEP_SIZE + 1);
|
|
|
|
Span<char> available = MakeSpan(buf.end(), K_LINE_READER_STEP_SIZE);
|
|
|
|
Size read_len = st->Read(available);
|
|
if (read_len < 0) {
|
|
error = true;
|
|
return false;
|
|
}
|
|
buf.len += read_len;
|
|
eof = !read_len;
|
|
|
|
view = buf;
|
|
}
|
|
|
|
line = SplitStrLine(view, &view);
|
|
if (view.len || eof) {
|
|
line.ptr[line.len] = 0;
|
|
line_number++;
|
|
*out_line = line;
|
|
return true;
|
|
}
|
|
|
|
buf.len = view.ptr - line.ptr;
|
|
MemMove(buf.ptr, line.ptr, buf.len);
|
|
}
|
|
}
|
|
|
|
void LineReader::PushLogFilter()
|
|
{
|
|
K::PushLogFilter([this](LogLevel level, const char *, const char *msg, FunctionRef<LogFunc> func) {
|
|
char ctx[1024];
|
|
|
|
if (line_number > 0) {
|
|
Fmt(ctx, "%1(%2): ", st->GetFileName(), line_number);
|
|
} else {
|
|
Fmt(ctx, "%1: ", st->GetFileName());
|
|
}
|
|
|
|
func(level, ctx, msg);
|
|
});
|
|
}
|
|
|
|
void StreamWriter::SetEncoder(StreamEncoder *encoder)
|
|
{
|
|
K_ASSERT(encoder);
|
|
K_ASSERT(!filename);
|
|
K_ASSERT(!this->encoder);
|
|
|
|
this->encoder = encoder;
|
|
}
|
|
|
|
bool StreamWriter::Open(HeapArray<uint8_t> *mem, const char *filename, unsigned int,
|
|
CompressionType compression_type, CompressionSpeed compression_speed)
|
|
{
|
|
Close(true);
|
|
|
|
K_DEFER_N(err_guard) { error = true; };
|
|
error = false;
|
|
raw_written = 0;
|
|
|
|
K_ASSERT(filename);
|
|
this->filename = DuplicateString(filename, &str_alloc).ptr;
|
|
|
|
dest.type = DestinationType::Memory;
|
|
dest.u.mem.memory = mem;
|
|
dest.u.mem.start = mem->len;
|
|
dest.vt100 = false;
|
|
|
|
if (!InitCompressor(compression_type, compression_speed))
|
|
return false;
|
|
|
|
err_guard.Disable();
|
|
return true;
|
|
}
|
|
|
|
bool StreamWriter::Open(int fd, const char *filename, unsigned int flags,
|
|
CompressionType compression_type, CompressionSpeed compression_speed)
|
|
{
|
|
Close(true);
|
|
|
|
K_DEFER_N(err_guard) { error = true; };
|
|
error = false;
|
|
raw_written = 0;
|
|
|
|
K_ASSERT(fd >= 0);
|
|
K_ASSERT(filename);
|
|
this->filename = DuplicateString(filename, &str_alloc).ptr;
|
|
|
|
InitFile(flags);
|
|
|
|
dest.u.file.fd = fd;
|
|
dest.vt100 = FileIsVt100(fd);
|
|
|
|
if (!InitCompressor(compression_type, compression_speed))
|
|
return false;
|
|
|
|
err_guard.Disable();
|
|
return true;
|
|
}
|
|
|
|
bool StreamWriter::Open(const char *filename, unsigned int flags,
|
|
CompressionType compression_type, CompressionSpeed compression_speed)
|
|
{
|
|
Close(true);
|
|
|
|
K_DEFER_N(err_guard) { error = true; };
|
|
error = false;
|
|
raw_written = 0;
|
|
|
|
K_ASSERT(filename);
|
|
this->filename = DuplicateString(filename, &str_alloc).ptr;
|
|
|
|
InitFile(flags);
|
|
|
|
dest.u.file.atomic = (flags & (int)StreamWriterFlag::Atomic);
|
|
dest.u.file.exclusive = (flags & (int)StreamWriterFlag::Exclusive);
|
|
|
|
if (dest.u.file.atomic) {
|
|
Span<const char> directory = GetPathDirectory(filename);
|
|
|
|
if (dest.u.file.exclusive) {
|
|
int fd = OpenFile(filename, (int)OpenFlag::Write | (int)OpenFlag::Exclusive);
|
|
if (fd < 0)
|
|
return false;
|
|
CloseDescriptor(fd);
|
|
|
|
dest.u.file.unlink_on_error = true;
|
|
}
|
|
|
|
#if defined(O_TMPFILE)
|
|
{
|
|
static bool has_proc = !access("/proc/self/fd", X_OK);
|
|
|
|
if (has_proc) {
|
|
const char *dirname = DuplicateString(directory, &str_alloc).ptr;
|
|
dest.u.file.fd = K_RESTART_EINTR(open(dirname, O_WRONLY | O_TMPFILE | O_CLOEXEC, 0644), < 0);
|
|
|
|
if (dest.u.file.fd >= 0) {
|
|
dest.u.file.owned = true;
|
|
} else if (errno != EINVAL && errno != EOPNOTSUPP) {
|
|
LogError("Cannot open temporary file in '%1': %2", directory, strerror(errno));
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (!dest.u.file.owned) {
|
|
const char *basename = SplitStrReverseAny(filename, K_PATH_SEPARATORS).ptr;
|
|
|
|
dest.u.file.tmp_filename = CreateUniqueFile(directory, basename, ".tmp", &str_alloc, &dest.u.file.fd);
|
|
if (!dest.u.file.tmp_filename)
|
|
return false;
|
|
dest.u.file.owned = true;
|
|
}
|
|
} else {
|
|
unsigned int open_flags = (int)OpenFlag::Write;
|
|
open_flags |= dest.u.file.exclusive ? (int)OpenFlag::Exclusive : 0;
|
|
|
|
dest.u.file.fd = OpenFile(filename, open_flags);
|
|
if (dest.u.file.fd < 0)
|
|
return false;
|
|
dest.u.file.owned = true;
|
|
|
|
dest.u.file.unlink_on_error = dest.u.file.exclusive;
|
|
}
|
|
dest.vt100 = FileIsVt100(dest.u.file.fd);
|
|
|
|
if (!InitCompressor(compression_type, compression_speed))
|
|
return false;
|
|
|
|
err_guard.Disable();
|
|
return true;
|
|
}
|
|
|
|
bool StreamWriter::Open(const std::function<bool(Span<const uint8_t>)> &func, const char *filename, unsigned int,
|
|
CompressionType compression_type, CompressionSpeed compression_speed)
|
|
{
|
|
Close(true);
|
|
|
|
K_DEFER_N(err_guard) { error = true; };
|
|
error = false;
|
|
raw_written = 0;
|
|
|
|
K_ASSERT(filename);
|
|
this->filename = DuplicateString(filename, &str_alloc).ptr;
|
|
|
|
dest.type = DestinationType::Function;
|
|
new (&dest.u.func) std::function<bool(Span<const uint8_t>)>(func);
|
|
dest.vt100 = false;
|
|
|
|
if (!InitCompressor(compression_type, compression_speed))
|
|
return false;
|
|
|
|
err_guard.Disable();
|
|
return true;
|
|
}
|
|
|
|
bool StreamWriter::Rewind()
|
|
{
|
|
if (error) [[unlikely]]
|
|
return false;
|
|
|
|
if (encoder) [[unlikely]] {
|
|
LogError("Cannot rewind stream with encoder");
|
|
return false;
|
|
}
|
|
|
|
switch (dest.type) {
|
|
case DestinationType::Memory: { dest.u.mem.memory->RemoveFrom(dest.u.mem.start); } break;
|
|
|
|
case DestinationType::LineFile:
|
|
case DestinationType::BufferedFile:
|
|
case DestinationType::DirectFile: {
|
|
if (lseek(dest.u.file.fd, 0, SEEK_SET) < 0) {
|
|
LogError("Failed to rewind '%1': %2", filename, strerror(errno));
|
|
error = true;
|
|
return false;
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
HANDLE h = (HANDLE)_get_osfhandle(dest.u.file.fd);
|
|
|
|
if (!SetEndOfFile(h)) {
|
|
LogError("Failed to truncate '%1': %2", filename, GetWin32ErrorString());
|
|
error = true;
|
|
return false;
|
|
}
|
|
#else
|
|
if (ftruncate(dest.u.file.fd, 0) < 0) {
|
|
LogError("Failed to truncate '%1': %2", filename, strerror(errno));
|
|
error = true;
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
dest.u.file.buf_used = 0;
|
|
} break;
|
|
|
|
case DestinationType::Function: {
|
|
LogError("Cannot rewind stream '%1'", filename);
|
|
error = true;
|
|
return false;
|
|
} break;
|
|
}
|
|
|
|
raw_written = 0;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool StreamWriter::Flush()
|
|
{
|
|
#if !defined(__wasm__)
|
|
std::lock_guard<std::mutex> lock(mutex);
|
|
#endif
|
|
|
|
if (error) [[unlikely]]
|
|
return false;
|
|
|
|
switch (dest.type) {
|
|
case DestinationType::Memory: return true;
|
|
|
|
case DestinationType::LineFile:
|
|
case DestinationType::BufferedFile: {
|
|
if (!FlushBuffer())
|
|
return false;
|
|
} [[fallthrough]];
|
|
case DestinationType::DirectFile: {
|
|
if (!FlushFile(dest.u.file.fd, filename)) {
|
|
error = true;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
} break;
|
|
|
|
case DestinationType::Function: return true;
|
|
}
|
|
|
|
K_UNREACHABLE();
|
|
}
|
|
|
|
int StreamWriter::GetDescriptor() const
|
|
{
|
|
K_ASSERT(dest.type == DestinationType::BufferedFile ||
|
|
dest.type == DestinationType::LineFile ||
|
|
dest.type == DestinationType::DirectFile);
|
|
|
|
return dest.u.file.fd;
|
|
}
|
|
|
|
void StreamWriter::SetDescriptorOwned(bool owned)
|
|
{
|
|
K_ASSERT(dest.type == DestinationType::BufferedFile ||
|
|
dest.type == DestinationType::LineFile ||
|
|
dest.type == DestinationType::DirectFile);
|
|
|
|
dest.u.file.owned = owned;
|
|
}
|
|
|
|
bool StreamWriter::Write(Span<const uint8_t> buf)
|
|
{
|
|
#if !defined(__wasm__)
|
|
std::lock_guard<std::mutex> lock(mutex);
|
|
#endif
|
|
|
|
if (error) [[unlikely]]
|
|
return false;
|
|
|
|
if (encoder) {
|
|
error |= !encoder->Write(buf);
|
|
return !error;
|
|
} else {
|
|
return WriteRaw(buf);
|
|
}
|
|
}
|
|
|
|
bool StreamWriter::Close(bool implicit)
|
|
{
|
|
K_ASSERT(implicit || this != StdOut);
|
|
K_ASSERT(implicit || this != StdErr);
|
|
|
|
if (encoder) {
|
|
error = error || !encoder->Finalize();
|
|
|
|
delete encoder;
|
|
encoder = nullptr;
|
|
}
|
|
|
|
switch (dest.type) {
|
|
case DestinationType::Memory: { dest.u.mem = {}; } break;
|
|
|
|
case DestinationType::BufferedFile:
|
|
case DestinationType::LineFile: {
|
|
if (IsValid()) {
|
|
FlushBuffer();
|
|
}
|
|
} [[fallthrough]];
|
|
case DestinationType::DirectFile: {
|
|
if (dest.u.file.atomic) {
|
|
if (IsValid()) {
|
|
if (implicit) {
|
|
LogDebug("Deleting implicitly closed file '%1'", filename);
|
|
error = true;
|
|
} else if (!FlushFile(dest.u.file.fd, filename)) {
|
|
error = true;
|
|
}
|
|
}
|
|
|
|
if (IsValid()) {
|
|
#if defined(O_TMPFILE)
|
|
if (!dest.u.file.tmp_filename) {
|
|
bool linked = false;
|
|
|
|
// AT_EMPTY_PATH requires CAP_DAC_READ_SEARCH so use the /proc trick instead.
|
|
// Will revisit once this restriction is lifted (if ever).
|
|
char proc[256];
|
|
Fmt(proc, "/proc/self/fd/%1", dest.u.file.fd);
|
|
|
|
for (int i = 0; i < 10; i++) {
|
|
if (linkat(AT_FDCWD, proc, AT_FDCWD, filename, AT_SYMLINK_FOLLOW) < 0) {
|
|
if (errno == EEXIST) {
|
|
unlink(filename);
|
|
continue;
|
|
}
|
|
|
|
LogError("Failed to materialize file '%1': %2", filename, strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
linked = true;
|
|
break;
|
|
}
|
|
|
|
// The linkat() call cannot overwrite an existing file. We try to unlink() the file if
|
|
// needed several times (see loop above) to make it work but it it still doesn't, link to
|
|
// a temporary file and let RenameFile() handle the final step. Should be rare!
|
|
if (!linked) {
|
|
Span<const char> directory = GetPathDirectory(filename);
|
|
const char *basename = SplitStrReverseAny(filename, K_PATH_SEPARATORS).ptr;
|
|
|
|
dest.u.file.tmp_filename = CreateUniquePath(directory, basename, ".tmp", &str_alloc, [&](const char *path) {
|
|
return !linkat(AT_FDCWD, proc, AT_FDCWD, path, AT_SYMLINK_FOLLOW);
|
|
});
|
|
if (!dest.u.file.tmp_filename) {
|
|
LogError("Failed to materialize file '%1': %2", filename, strerror(errno));
|
|
error = true;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (dest.u.file.owned) {
|
|
CloseDescriptor(dest.u.file.fd);
|
|
dest.u.file.owned = false;
|
|
}
|
|
|
|
if (dest.u.file.tmp_filename) {
|
|
unsigned int flags = (int)RenameFlag::Overwrite | (int)RenameFlag::Sync;
|
|
|
|
if (RenameFile(dest.u.file.tmp_filename, filename, flags) == RenameResult::Success) {
|
|
dest.u.file.tmp_filename = nullptr;
|
|
} else {
|
|
error = true;
|
|
}
|
|
}
|
|
} else {
|
|
error = true;
|
|
}
|
|
}
|
|
|
|
if (dest.u.file.owned) {
|
|
CloseDescriptor(dest.u.file.fd);
|
|
dest.u.file.owned = false;
|
|
}
|
|
|
|
// Try to clean up, though we can't do much if that fails (except log error)
|
|
if (dest.u.file.tmp_filename) {
|
|
UnlinkFile(dest.u.file.tmp_filename);
|
|
}
|
|
if (error && dest.u.file.unlink_on_error) {
|
|
UnlinkFile(filename);
|
|
}
|
|
|
|
MemSet(&dest.u.file, 0, K_SIZE(dest.u.file));
|
|
} break;
|
|
|
|
case DestinationType::Function: {
|
|
error |= IsValid() && !dest.u.func({});
|
|
dest.u.func.~function();
|
|
} break;
|
|
}
|
|
|
|
bool ret = !filename || !error;
|
|
|
|
filename = nullptr;
|
|
error = true;
|
|
dest.type = DestinationType::Memory;
|
|
str_alloc.Reset();
|
|
|
|
return ret;
|
|
}
|
|
|
|
void StreamWriter::InitFile(unsigned int flags)
|
|
{
|
|
bool direct = (flags & (int)StreamWriterFlag::NoBuffer);
|
|
bool line = (flags & (int)StreamWriterFlag::LineBuffer);
|
|
|
|
K_ASSERT(!direct || !line);
|
|
|
|
MemSet(&dest.u.file, 0, K_SIZE(dest.u.file));
|
|
|
|
if (direct) {
|
|
dest.type = DestinationType::DirectFile;
|
|
} else if (line) {
|
|
dest.type = DestinationType::LineFile;
|
|
dest.u.file.buf = AllocateSpan<uint8_t>(&str_alloc, Kibibytes(4));
|
|
} else {
|
|
dest.type = DestinationType::BufferedFile;
|
|
dest.u.file.buf = AllocateSpan<uint8_t>(&str_alloc, Kibibytes(4));
|
|
}
|
|
}
|
|
|
|
bool StreamWriter::FlushBuffer()
|
|
{
|
|
K_ASSERT(!error);
|
|
K_ASSERT(dest.type == DestinationType::BufferedFile ||
|
|
dest.type == DestinationType::LineFile);
|
|
|
|
while (dest.u.file.buf_used) {
|
|
#if defined(_WIN32)
|
|
Size write_len = _write(dest.u.file.fd, dest.u.file.buf.ptr, (unsigned int)dest.u.file.buf_used);
|
|
#else
|
|
Size write_len = K_RESTART_EINTR(write(dest.u.file.fd, dest.u.file.buf.ptr, (size_t)dest.u.file.buf_used), < 0);
|
|
#endif
|
|
|
|
if (write_len < 0) {
|
|
LogError("Failed to write to '%1': %2", filename, strerror(errno));
|
|
error = true;
|
|
return false;
|
|
}
|
|
|
|
Size move_len = dest.u.file.buf_used - write_len;
|
|
MemMove(dest.u.file.buf.ptr, dest.u.file.buf.ptr + write_len, move_len);
|
|
dest.u.file.buf_used -= write_len;
|
|
|
|
raw_written += write_len;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool StreamWriter::InitCompressor(CompressionType type, CompressionSpeed speed)
|
|
{
|
|
if (type != CompressionType::None) {
|
|
CreateCompressorFunc *func = CompressorFunctions[(int)type];
|
|
|
|
if (!func) {
|
|
LogError("%1 compression is not available for '%2'", CompressionTypeNames[(int)type], filename);
|
|
error = true;
|
|
return false;
|
|
}
|
|
|
|
encoder = func(this, type, speed);
|
|
K_ASSERT(encoder);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#if defined(_WIN32) || defined(__APPLE__)
|
|
|
|
static void *memrchr(const void *m, int c, size_t n)
|
|
{
|
|
const uint8_t *ptr = (const uint8_t *)m + n;
|
|
|
|
while (ptr-- > m) {
|
|
if (*ptr == c)
|
|
return (void *)ptr;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
#endif
|
|
|
|
bool StreamWriter::WriteRaw(Span<const uint8_t> buf)
|
|
{
|
|
switch (dest.type) {
|
|
case DestinationType::Memory: {
|
|
// dest.u.memory->Append(buf) would work but it's probably slower
|
|
dest.u.mem.memory->Grow(buf.len);
|
|
MemCpy(dest.u.mem.memory->ptr + dest.u.mem.memory->len, buf.ptr, buf.len);
|
|
dest.u.mem.memory->len += buf.len;
|
|
|
|
raw_written += buf.len;
|
|
} break;
|
|
|
|
case DestinationType::BufferedFile: {
|
|
if (!buf.len)
|
|
return true;
|
|
|
|
for (;;) {
|
|
Size copy_len = std::min(buf.len, dest.u.file.buf.len - dest.u.file.buf_used);
|
|
MemCpy(dest.u.file.buf.ptr + dest.u.file.buf_used, buf.ptr, copy_len);
|
|
|
|
buf.ptr += copy_len;
|
|
buf.len -= copy_len;
|
|
dest.u.file.buf_used += copy_len;
|
|
|
|
if (!buf.len)
|
|
break;
|
|
if (!FlushBuffer())
|
|
return false;
|
|
}
|
|
} break;
|
|
|
|
case DestinationType::LineFile: {
|
|
while (buf.len) {
|
|
const uint8_t *end = (const uint8_t *)memrchr(buf.ptr, '\n', (size_t)buf.len);
|
|
|
|
if (end++) {
|
|
Size copy_len = std::min((Size)(end - buf.ptr), dest.u.file.buf.len - dest.u.file.buf_used);
|
|
MemCpy(dest.u.file.buf.ptr + dest.u.file.buf_used, buf.ptr, copy_len);
|
|
|
|
buf.ptr += copy_len;
|
|
buf.len -= copy_len;
|
|
dest.u.file.buf_used += copy_len;
|
|
} else {
|
|
Size copy_len = std::min(buf.len, dest.u.file.buf.len - dest.u.file.buf_used);
|
|
MemCpy(dest.u.file.buf.ptr + dest.u.file.buf_used, buf.ptr, copy_len);
|
|
|
|
buf.ptr += copy_len;
|
|
buf.len -= copy_len;
|
|
dest.u.file.buf_used += copy_len;
|
|
|
|
if (!buf.len)
|
|
break;
|
|
}
|
|
|
|
if (!FlushBuffer())
|
|
return false;
|
|
}
|
|
} break;
|
|
|
|
case DestinationType::DirectFile: {
|
|
while (buf.len) {
|
|
#if defined(_WIN32)
|
|
unsigned int int_len = (unsigned int)std::min(buf.len, (Size)UINT_MAX);
|
|
Size write_len = _write(dest.u.file.fd, buf.ptr, int_len);
|
|
#else
|
|
Size write_len = K_RESTART_EINTR(write(dest.u.file.fd, buf.ptr, (size_t)buf.len), < 0);
|
|
#endif
|
|
|
|
if (write_len < 0) {
|
|
LogError("Failed to write to '%1': %2", filename, strerror(errno));
|
|
error = true;
|
|
return false;
|
|
}
|
|
|
|
buf.ptr += write_len;
|
|
buf.len -= write_len;
|
|
|
|
raw_written += write_len;
|
|
}
|
|
} break;
|
|
|
|
case DestinationType::Function: {
|
|
// Empty writes are used to "close" the file.. don't!
|
|
if (!buf.len)
|
|
return true;
|
|
|
|
if (!dest.u.func(buf)) {
|
|
error = true;
|
|
return false;
|
|
}
|
|
|
|
raw_written += buf.len;
|
|
} break;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
StreamCompressorHelper::StreamCompressorHelper(CompressionType compression_type, CreateCompressorFunc *func)
|
|
{
|
|
K_ASSERT(!CompressorFunctions[(int)compression_type]);
|
|
CompressorFunctions[(int)compression_type] = func;
|
|
}
|
|
|
|
bool SpliceStream(StreamReader *reader, int64_t max_len, StreamWriter *writer, Span<uint8_t> buf,
|
|
FunctionRef<void(int64_t, int64_t)> progress)
|
|
{
|
|
K_ASSERT(buf.len >= Kibibytes(2));
|
|
|
|
if (!reader->IsValid())
|
|
return false;
|
|
|
|
int64_t raw_len = reader->ComputeRawLen();
|
|
int64_t total_len = 0;
|
|
|
|
do {
|
|
Size read_len = reader->Read(buf);
|
|
if (read_len < 0)
|
|
return false;
|
|
|
|
if (max_len >= 0 && read_len > max_len - total_len) [[unlikely]] {
|
|
LogError("File '%1' is too large (limit = %2)", reader->GetFileName(), FmtDiskSize(max_len));
|
|
return false;
|
|
}
|
|
total_len += read_len;
|
|
|
|
if (!writer->Write(buf.ptr, read_len))
|
|
return false;
|
|
|
|
progress(reader->GetRawRead(), raw_len);
|
|
} while (!reader->IsEOF());
|
|
|
|
return true;
|
|
}
|
|
|
|
bool IsCompressorAvailable(CompressionType compression_type)
|
|
{
|
|
return CompressorFunctions[(int)compression_type];
|
|
}
|
|
|
|
bool IsDecompressorAvailable(CompressionType compression_type)
|
|
{
|
|
return DecompressorFunctions[(int)compression_type];
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// INI
|
|
// ------------------------------------------------------------------------
|
|
|
|
IniParser::LineType IniParser::FindNextLine(IniProperty *out_prop)
|
|
{
|
|
if (error) [[unlikely]]
|
|
return LineType::Exit;
|
|
|
|
K_DEFER_N(err_guard) { error = true; };
|
|
|
|
Span<char> line;
|
|
while (reader.Next(&line)) {
|
|
line = TrimStr(line);
|
|
|
|
if (!line.len || line[0] == ';' || line[0] == '#') {
|
|
// Ignore this line (empty or comment)
|
|
} else if (line[0] == '[') {
|
|
if (line.len < 2 || line[line.len - 1] != ']') {
|
|
LogError("Malformed [section] line");
|
|
return LineType::Exit;
|
|
}
|
|
|
|
Span<const char> section = TrimStr(line.Take(1, line.len - 2));
|
|
if (!section.len) {
|
|
LogError("Empty section name");
|
|
return LineType::Exit;
|
|
}
|
|
|
|
current_section.RemoveFrom(0);
|
|
current_section.Grow(section.len + 1);
|
|
current_section.Append(section);
|
|
current_section.ptr[current_section.len] = 0;
|
|
|
|
err_guard.Disable();
|
|
return LineType::Section;
|
|
} else {
|
|
Span<char> value;
|
|
|
|
Span<char> key = TrimStr(SplitStr(line, '=', &value));
|
|
if (!key.len || key.end() == line.end()) {
|
|
LogError("Expected [section] or <key> = <value> pair");
|
|
return LineType::Exit;
|
|
}
|
|
key.ptr[key.len] = 0;
|
|
|
|
value = TrimStr(value);
|
|
*value.end() = 0;
|
|
|
|
out_prop->section = current_section;
|
|
out_prop->key = key;
|
|
out_prop->value = value;
|
|
|
|
err_guard.Disable();
|
|
return LineType::KeyValue;
|
|
}
|
|
}
|
|
if (!reader.IsValid())
|
|
return LineType::Exit;
|
|
|
|
eof = true;
|
|
|
|
err_guard.Disable();
|
|
return LineType::Exit;
|
|
}
|
|
|
|
bool IniParser::Next(IniProperty *out_prop)
|
|
{
|
|
LineType type;
|
|
while ((type = FindNextLine(out_prop)) == LineType::Section);
|
|
return type == LineType::KeyValue;
|
|
}
|
|
|
|
bool IniParser::NextInSection(IniProperty *out_prop)
|
|
{
|
|
LineType type = FindNextLine(out_prop);
|
|
return type == LineType::KeyValue;
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Assets
|
|
// ------------------------------------------------------------------------
|
|
|
|
#if defined(FELIX_HOT_ASSETS)
|
|
|
|
static char assets_filename[4096];
|
|
static int64_t assets_last_check = -1;
|
|
static HeapArray<AssetInfo> assets;
|
|
static HashTable<const char *, const AssetInfo *> assets_map;
|
|
static BlockAllocator assets_alloc;
|
|
static bool assets_ready;
|
|
|
|
bool ReloadAssets()
|
|
{
|
|
const Span<const AssetInfo> *lib_assets = nullptr;
|
|
|
|
// Make asset library filename
|
|
if (!assets_filename[0]) {
|
|
Span<const char> prefix = GetApplicationExecutable();
|
|
#if defined(_WIN32)
|
|
SplitStrReverse(prefix, '.', &prefix);
|
|
#endif
|
|
|
|
Fmt(assets_filename, "%1_assets%2", prefix, K_SHARED_LIBRARY_EXTENSION);
|
|
}
|
|
|
|
// Check library time
|
|
{
|
|
FileInfo file_info;
|
|
if (StatFile(assets_filename, &file_info) != StatResult::Success)
|
|
return false;
|
|
|
|
if (assets_last_check == file_info.mtime)
|
|
return false;
|
|
assets_last_check = file_info.mtime;
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
HMODULE h;
|
|
if (win32_utf8) {
|
|
h = LoadLibraryA(assets_filename);
|
|
} else {
|
|
wchar_t filename_w[4096];
|
|
if (ConvertUtf8ToWin32Wide(assets_filename, filename_w) < 0)
|
|
return false;
|
|
|
|
h = LoadLibraryW(filename_w);
|
|
}
|
|
if (!h) {
|
|
LogError("Cannot load library '%1'", assets_filename);
|
|
return false;
|
|
}
|
|
K_DEFER { FreeLibrary(h); };
|
|
|
|
lib_assets = (const Span<const AssetInfo> *)(void *)GetProcAddress(h, "EmbedAssets");
|
|
#else
|
|
void *h = dlopen(assets_filename, RTLD_LAZY | RTLD_LOCAL);
|
|
if (!h) {
|
|
LogError("Cannot load library '%1': %2", assets_filename, dlerror());
|
|
return false;
|
|
}
|
|
K_DEFER { dlclose(h); };
|
|
|
|
lib_assets = (const Span<const AssetInfo> *)dlsym(h, "EmbedAssets");
|
|
#endif
|
|
if (!lib_assets) {
|
|
LogError("Cannot find symbol 'EmbedAssets' in library '%1'", assets_filename);
|
|
return false;
|
|
}
|
|
|
|
// We are not allowed to fail from now on
|
|
assets.Clear();
|
|
assets_map.Clear();
|
|
assets_alloc.Reset();
|
|
|
|
for (const AssetInfo &asset: *lib_assets) {
|
|
AssetInfo asset_copy;
|
|
|
|
asset_copy.name = DuplicateString(asset.name, &assets_alloc).ptr;
|
|
asset_copy.data = AllocateSpan<uint8_t>(&assets_alloc, asset.data.len);
|
|
MemCpy((void *)asset_copy.data.ptr, asset.data.ptr, asset.data.len);
|
|
asset_copy.compression_type = asset.compression_type;
|
|
|
|
assets.Append(asset_copy);
|
|
}
|
|
for (const AssetInfo &asset: assets) {
|
|
assets_map.Set(&asset);
|
|
}
|
|
|
|
assets_ready = true;
|
|
return true;
|
|
}
|
|
|
|
Span<const AssetInfo> GetEmbedAssets()
|
|
{
|
|
if (!assets_ready) {
|
|
ReloadAssets();
|
|
K_ASSERT(assets_ready);
|
|
}
|
|
|
|
return assets;
|
|
}
|
|
|
|
const AssetInfo *FindEmbedAsset(const char *name)
|
|
{
|
|
if (!assets_ready) {
|
|
ReloadAssets();
|
|
K_ASSERT(assets_ready);
|
|
}
|
|
|
|
return assets_map.FindValue(name, nullptr);
|
|
}
|
|
|
|
#else
|
|
|
|
HashTable<const char *, const AssetInfo *> EmbedAssetsMap;
|
|
static bool assets_ready;
|
|
|
|
void InitEmbedMap(Span<const AssetInfo> assets)
|
|
{
|
|
if (!assets_ready) [[likely]] {
|
|
for (const AssetInfo &asset: assets) {
|
|
EmbedAssetsMap.Set(&asset);
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
bool PatchFile(StreamReader *reader, StreamWriter *writer,
|
|
FunctionRef<void(Span<const char>, StreamWriter *)> func)
|
|
{
|
|
LineReader splitter(reader);
|
|
|
|
Span<const char> line;
|
|
while (splitter.Next(&line) && writer->IsValid()) {
|
|
while (line.len) {
|
|
Span<const char> before = SplitStr(line, "{{", &line);
|
|
|
|
writer->Write(before);
|
|
|
|
if (before.end() < line.ptr) {
|
|
Span<const char> expr = SplitStr(line, "}}", &line);
|
|
|
|
if (expr.end() < line.ptr) {
|
|
func(expr, writer);
|
|
} else {
|
|
Print(writer, "{{%1", expr);
|
|
}
|
|
}
|
|
}
|
|
|
|
writer->Write('\n');
|
|
}
|
|
|
|
if (!reader->IsValid())
|
|
return false;
|
|
if (!writer->IsValid())
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool PatchFile(Span<const uint8_t> data, StreamWriter *writer,
|
|
FunctionRef<void(Span<const char>, StreamWriter *)> func)
|
|
{
|
|
StreamReader reader(data, "<asset>");
|
|
|
|
if (!PatchFile(&reader, writer, func)) {
|
|
K_ASSERT(reader.IsValid());
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool PatchFile(const AssetInfo &asset, StreamWriter *writer,
|
|
FunctionRef<void(Span<const char>, StreamWriter *)> func)
|
|
{
|
|
StreamReader reader(asset.data, "<asset>", asset.compression_type);
|
|
|
|
if (!PatchFile(&reader, writer, func)) {
|
|
K_ASSERT(reader.IsValid());
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
Span<const uint8_t> PatchFile(Span<const uint8_t> data, Allocator *alloc,
|
|
FunctionRef<void(Span<const char>, StreamWriter *)> func)
|
|
{
|
|
K_ASSERT(alloc);
|
|
|
|
HeapArray<uint8_t> buf(alloc);
|
|
StreamWriter writer(&buf, "<asset>");
|
|
|
|
PatchFile(data, &writer, func);
|
|
|
|
bool success = writer.Close();
|
|
K_ASSERT(success);
|
|
|
|
buf.Grow(1);
|
|
buf.ptr[buf.len] = 0;
|
|
|
|
return buf.Leak();
|
|
}
|
|
|
|
Span<const uint8_t> PatchFile(const AssetInfo &asset, Allocator *alloc,
|
|
FunctionRef<void(Span<const char>, StreamWriter *)> func)
|
|
{
|
|
K_ASSERT(alloc);
|
|
|
|
HeapArray<uint8_t> buf(alloc);
|
|
StreamWriter writer(&buf, "<asset>", 0, asset.compression_type);
|
|
|
|
PatchFile(asset, &writer, func);
|
|
|
|
bool success = writer.Close();
|
|
K_ASSERT(success);
|
|
|
|
buf.Grow(1);
|
|
buf.ptr[buf.len] = 0;
|
|
|
|
return buf.Leak();
|
|
}
|
|
|
|
Span<const char> PatchFile(Span<const char> data, Allocator *alloc,
|
|
FunctionRef<void(Span<const char> key, StreamWriter *)> func)
|
|
{
|
|
Span<const uint8_t> ret = PatchFile(data.As<const uint8_t>(), alloc, func);
|
|
return ret.As<const char>();
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Translations
|
|
// ------------------------------------------------------------------------
|
|
|
|
typedef HashMap<const char *, const char *> TranslationMap;
|
|
|
|
static HeapArray<TranslationTable> i18n_tables;
|
|
static NoDestroy<HeapArray<TranslationMap>> i18n_maps;
|
|
static HashMap<Span<const char> , const TranslationTable *> i18n_locales;
|
|
|
|
static const TranslationTable *i18n_default_table;
|
|
static const TranslationMap *i18n_default_map;
|
|
static thread_local const TranslationTable *i18n_thread_table = i18n_default_table;
|
|
static thread_local const TranslationMap *i18n_thread_map = i18n_default_map;
|
|
|
|
static void SetDefaultLocale(const char *default_lang)
|
|
{
|
|
if (i18n_default_table)
|
|
return;
|
|
|
|
// Obey environment settings, even on Windows, for easy override
|
|
{
|
|
// Yeah this order makes perfect sense. Don't ask.
|
|
static const char *const EnvVariables[] = { "LANGUAGE", "LC_MESSAGES", "LC_ALL", "LANG" };
|
|
|
|
for (const char *variable: EnvVariables) {
|
|
const char *env = GetEnv(variable);
|
|
|
|
if (env) {
|
|
ChangeThreadLocale(env);
|
|
|
|
i18n_default_table = i18n_thread_table;
|
|
i18n_default_map = i18n_thread_map;
|
|
|
|
if (i18n_default_table)
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
{
|
|
wchar_t buffer[16384];
|
|
unsigned long languages = 0;
|
|
unsigned long size = K_LEN(buffer);
|
|
|
|
if (GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &languages, buffer, &size)) {
|
|
if (languages) {
|
|
char lang[256] = {};
|
|
ConvertWin32WideToUtf8(buffer, lang);
|
|
|
|
ChangeThreadLocale(lang);
|
|
|
|
i18n_default_table = i18n_thread_table;
|
|
i18n_default_map = i18n_thread_map;
|
|
|
|
if (i18n_default_table)
|
|
return;
|
|
}
|
|
} else {
|
|
LogError("Failed to retrieve preferred Windows UI language: %1", GetWin32ErrorString());
|
|
}
|
|
}
|
|
#endif
|
|
|
|
ChangeThreadLocale(default_lang);
|
|
K_CRITICAL(i18n_thread_table, "Missing default locale");
|
|
|
|
i18n_default_table = i18n_thread_table;
|
|
i18n_default_map = i18n_thread_map;
|
|
}
|
|
|
|
void InitLocales(Span<const TranslationTable> tables, const char *default_lang)
|
|
{
|
|
K_ASSERT(!i18n_tables.len);
|
|
|
|
for (const TranslationTable &table: tables) {
|
|
i18n_tables.Append(table);
|
|
|
|
TranslationMap *map = i18n_maps->AppendDefault();
|
|
|
|
for (const TranslationTable::Pair &pair: table.messages) {
|
|
map->Set(pair.key, pair.value);
|
|
}
|
|
}
|
|
for (const TranslationTable &table: i18n_tables) {
|
|
i18n_locales.Set(table.language, &table);
|
|
}
|
|
|
|
SetDefaultLocale(default_lang);
|
|
}
|
|
|
|
void ChangeThreadLocale(const char *name)
|
|
{
|
|
Span<const char> lang = name ? SplitStrAny(name, "_-") : "";
|
|
const TranslationTable *table = i18n_locales.FindValue(lang, nullptr);
|
|
|
|
if (table) {
|
|
Size idx = table - i18n_tables.ptr;
|
|
|
|
i18n_thread_table = table;
|
|
i18n_thread_map = &(*i18n_maps)[idx];
|
|
} else {
|
|
i18n_thread_table = i18n_default_table;
|
|
i18n_thread_map = i18n_default_map;
|
|
}
|
|
}
|
|
|
|
const char *GetThreadLocale()
|
|
{
|
|
K_ASSERT(i18n_thread_table);
|
|
return i18n_thread_table->language;
|
|
}
|
|
|
|
const char *T(const char *key)
|
|
{
|
|
if (!i18n_thread_map)
|
|
return key;
|
|
|
|
return i18n_thread_map->FindValue(key, key);
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Options
|
|
// ------------------------------------------------------------------------
|
|
|
|
static inline bool IsOption(const char *arg)
|
|
{
|
|
return arg[0] == '-' && arg[1];
|
|
}
|
|
|
|
static inline bool IsLongOption(const char *arg)
|
|
{
|
|
return arg[0] == '-' && arg[1] == '-' && arg[2];
|
|
}
|
|
|
|
static inline bool IsDashDash(const char *arg)
|
|
{
|
|
return arg[0] == '-' && arg[1] == '-' && !arg[2];
|
|
}
|
|
|
|
const char *OptionParser::Next()
|
|
{
|
|
current_option = nullptr;
|
|
current_value = nullptr;
|
|
test_failed = false;
|
|
|
|
// Support aggregate short options, such as '-fbar'. Note that this can also be
|
|
// parsed as the short option '-f' with value 'bar', if the user calls
|
|
// ConsumeOptionValue() after getting '-f'.
|
|
if (smallopt_offset) {
|
|
const char *opt = args[pos];
|
|
|
|
buf[1] = opt[smallopt_offset];
|
|
current_option = buf;
|
|
|
|
if (!opt[++smallopt_offset]) {
|
|
smallopt_offset = 0;
|
|
pos++;
|
|
}
|
|
|
|
return current_option;
|
|
}
|
|
|
|
if (mode == OptionMode::Stop && (pos >= limit || !IsOption(args[pos]))) {
|
|
limit = pos;
|
|
return nullptr;
|
|
}
|
|
|
|
// Skip non-options, do the permutation once we reach an option or the last argument
|
|
Size next_index = pos;
|
|
while (next_index < limit && !IsOption(args[next_index])) {
|
|
next_index++;
|
|
}
|
|
if (mode == OptionMode::Rotate) {
|
|
std::rotate(args.ptr + pos, args.ptr + next_index, args.end());
|
|
limit -= (next_index - pos);
|
|
} else if (mode == OptionMode::Skip) {
|
|
pos = next_index;
|
|
}
|
|
if (pos >= limit)
|
|
return nullptr;
|
|
|
|
const char *opt = args[pos];
|
|
|
|
if (IsLongOption(opt)) {
|
|
const char *needle = strchr(opt, '=');
|
|
if (needle) {
|
|
// We can reorder args, but we don't want to change strings. So copy the
|
|
// option up to '=' in our buffer. And store the part after '=' as the
|
|
// current value.
|
|
Size len = needle - opt;
|
|
if (len > K_SIZE(buf) - 1) {
|
|
len = K_SIZE(buf) - 1;
|
|
}
|
|
MemCpy(buf, opt, len);
|
|
buf[len] = 0;
|
|
current_option = buf;
|
|
current_value = needle + 1;
|
|
} else {
|
|
current_option = opt;
|
|
}
|
|
pos++;
|
|
} else if (IsDashDash(opt)) {
|
|
// We may have previously moved non-options to the end of args. For example,
|
|
// at this point 'a b c -- d e' is reordered to '-- d e a b c'. Fix it.
|
|
std::rotate(args.ptr + pos + 1, args.ptr + limit, args.end());
|
|
limit = pos;
|
|
pos++;
|
|
} else if (opt[2]) {
|
|
// We either have aggregated short options or one short option with a value,
|
|
// depending on whether or not the user calls ConsumeOptionValue().
|
|
buf[0] = '-';
|
|
buf[1] = opt[1];
|
|
buf[2] = 0;
|
|
current_option = buf;
|
|
smallopt_offset = opt[2] ? 2 : 0;
|
|
|
|
// The main point of Skip mode is to be able to parse arguments in
|
|
// multiple passes. This does not work well with ambiguous short options
|
|
// (such as -oOption, which can be interpeted as multiple one-char options
|
|
// or one -o option with a value), so force the value interpretation.
|
|
if (mode == OptionMode::Skip) {
|
|
ConsumeValue();
|
|
}
|
|
} else {
|
|
current_option = opt;
|
|
pos++;
|
|
}
|
|
|
|
return current_option;
|
|
}
|
|
|
|
const char *OptionParser::ConsumeValue()
|
|
{
|
|
if (current_value)
|
|
return current_value;
|
|
|
|
// Support '-fbar' where bar is the value, but only for the first short option
|
|
// if it's an aggregate.
|
|
if (smallopt_offset == 2 && args[pos][2]) {
|
|
smallopt_offset = 0;
|
|
current_value = args[pos] + 2;
|
|
pos++;
|
|
// Support '-f bar' and '--foo bar', see ConsumeOption() for '--foo=bar'
|
|
} else if (current_option != buf && pos < limit && !IsOption(args[pos])) {
|
|
current_value = args[pos];
|
|
pos++;
|
|
}
|
|
|
|
return current_value;
|
|
}
|
|
|
|
const char *OptionParser::ConsumeNonOption()
|
|
{
|
|
if (pos == args.len)
|
|
return nullptr;
|
|
// Beyond limit there are only non-options, the limit is moved when we move non-options
|
|
// to the end or upon encouteering a double dash '--'.
|
|
if (pos < limit && IsOption(args[pos]))
|
|
return nullptr;
|
|
|
|
return args[pos++];
|
|
}
|
|
|
|
void OptionParser::ConsumeNonOptions(HeapArray<const char *> *non_options)
|
|
{
|
|
const char *non_option;
|
|
while ((non_option = ConsumeNonOption())) {
|
|
non_options->Append(non_option);
|
|
}
|
|
}
|
|
|
|
bool OptionParser::Test(const char *test1, const char *test2, OptionType type)
|
|
{
|
|
K_ASSERT(test1 && IsOption(test1));
|
|
K_ASSERT(!test2 || IsOption(test2));
|
|
|
|
if (TestStr(test1, current_option) || (test2 && TestStr(test2, current_option))) {
|
|
switch (type) {
|
|
case OptionType::NoValue: {
|
|
if (current_value) {
|
|
LogError("Option '%1' does not support values", current_option);
|
|
test_failed = true;
|
|
return false;
|
|
}
|
|
} break;
|
|
case OptionType::Value: {
|
|
if (!ConsumeValue()) {
|
|
LogError("Option '%1' requires a value", current_option);
|
|
test_failed = true;
|
|
return false;
|
|
}
|
|
} break;
|
|
case OptionType::OptionalValue: {
|
|
ConsumeValue();
|
|
} break;
|
|
}
|
|
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void OptionParser::LogUnknownError() const
|
|
{
|
|
if (!TestHasFailed()) {
|
|
LogError("Unknown option '%1'", current_option);
|
|
}
|
|
}
|
|
|
|
void OptionParser::LogUnusedArguments() const
|
|
{
|
|
if (pos < args.len) {
|
|
LogWarning("Unused command-line arguments");
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Console prompter (simplified readline)
|
|
// ------------------------------------------------------------------------
|
|
|
|
static bool input_is_raw;
|
|
#if defined(_WIN32)
|
|
static HANDLE stdin_handle;
|
|
static DWORD input_orig_mode;
|
|
#elif !defined(__wasm__)
|
|
static struct termios input_orig_tio;
|
|
#endif
|
|
|
|
ConsolePrompter::ConsolePrompter()
|
|
{
|
|
entries.AppendDefault();
|
|
}
|
|
|
|
static bool EnableRawMode()
|
|
{
|
|
#if defined(_WIN32)
|
|
static bool init_atexit = false;
|
|
|
|
if (!input_is_raw) {
|
|
stdin_handle = (HANDLE)_get_osfhandle(STDIN_FILENO);
|
|
|
|
if (GetConsoleMode(stdin_handle, &input_orig_mode)) {
|
|
input_is_raw = SetConsoleMode(stdin_handle, ENABLE_WINDOW_INPUT);
|
|
|
|
if (input_is_raw && !init_atexit) {
|
|
atexit([]() { SetConsoleMode(stdin_handle, input_orig_mode); });
|
|
init_atexit = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return input_is_raw;
|
|
#elif !defined(__wasm__)
|
|
static bool init_atexit = false;
|
|
|
|
if (!input_is_raw) {
|
|
if (isatty(STDIN_FILENO) && tcgetattr(STDIN_FILENO, &input_orig_tio) >= 0) {
|
|
struct termios raw = input_orig_tio;
|
|
cfmakeraw(&raw);
|
|
|
|
input_is_raw = (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) >= 0);
|
|
|
|
if (input_is_raw && !init_atexit) {
|
|
atexit([]() { tcsetattr(STDIN_FILENO, TCSAFLUSH, &input_orig_tio); });
|
|
init_atexit = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return input_is_raw;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
static void DisableRawMode()
|
|
{
|
|
if (input_is_raw) {
|
|
#if defined(_WIN32)
|
|
input_is_raw = !SetConsoleMode(stdin_handle, input_orig_mode);
|
|
#elif !defined(__wasm__)
|
|
input_is_raw = !(tcsetattr(STDIN_FILENO, TCSAFLUSH, &input_orig_tio) >= 0);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
#if !defined(_WIN32) && !defined(__wasm__)
|
|
static void IgnoreSigWinch(struct sigaction *old_sa)
|
|
{
|
|
struct sigaction sa;
|
|
|
|
sa.sa_handler = [](int) {};
|
|
sigemptyset(&sa.sa_mask);
|
|
sa.sa_flags = 0;
|
|
|
|
sigaction(SIGWINCH, &sa, old_sa);
|
|
}
|
|
#endif
|
|
|
|
bool ConsolePrompter::Read(Span<const char> *out_str)
|
|
{
|
|
#if !defined(_WIN32) && !defined(__wasm__)
|
|
struct sigaction old_sa;
|
|
IgnoreSigWinch(&old_sa);
|
|
K_DEFER { sigaction(SIGWINCH, &old_sa, nullptr); };
|
|
#endif
|
|
|
|
if (FileIsVt100(STDERR_FILENO) && EnableRawMode()) {
|
|
K_DEFER {
|
|
Print(StdErr, "%!0");
|
|
DisableRawMode();
|
|
};
|
|
|
|
return ReadRaw(out_str);
|
|
} else {
|
|
return ReadBuffered(out_str);
|
|
}
|
|
}
|
|
|
|
Size ConsolePrompter::ReadEnum(Span<const PromptChoice> choices, Size value)
|
|
{
|
|
K_ASSERT(value < choices.len);
|
|
|
|
#if !defined(_WIN32) && !defined(__wasm__)
|
|
struct sigaction old_sa;
|
|
IgnoreSigWinch(&old_sa);
|
|
K_DEFER { sigaction(SIGWINCH, &old_sa, nullptr); };
|
|
#endif
|
|
|
|
if (FileIsVt100(STDERR_FILENO) && EnableRawMode()) {
|
|
K_DEFER {
|
|
Print(StdErr, "%!0");
|
|
DisableRawMode();
|
|
};
|
|
|
|
return ReadRawEnum(choices, value);
|
|
} else {
|
|
return ReadBufferedEnum(choices);
|
|
}
|
|
}
|
|
|
|
void ConsolePrompter::Commit()
|
|
{
|
|
str.len = TrimStrRight(str.Take(), "\r\n").len;
|
|
|
|
if (str.len) {
|
|
std::swap(str, entries[entries.len - 1]);
|
|
entries.AppendDefault();
|
|
}
|
|
entry_idx = entries.len - 1;
|
|
str.RemoveFrom(0);
|
|
str_offset = 0;
|
|
|
|
rows = 0;
|
|
rows_with_extra = 0;
|
|
x = 0;
|
|
y = 0;
|
|
}
|
|
|
|
bool ConsolePrompter::ReadRaw(Span<const char> *out_str)
|
|
{
|
|
StdErr->Flush();
|
|
|
|
prompt_columns = ComputeUnicodeWidth(prompt) + 1;
|
|
str_offset = str.len;
|
|
|
|
RenderRaw();
|
|
|
|
int32_t uc;
|
|
while ((uc = ReadChar()) >= 0) {
|
|
// Fix display if terminal is resized
|
|
if (GetConsoleSize().x != columns) {
|
|
RenderRaw();
|
|
}
|
|
|
|
switch (uc) {
|
|
case 0x1B: {
|
|
LocalArray<char, 16> buf;
|
|
|
|
const auto match_escape = [&](const char *seq) {
|
|
K_ASSERT(strlen(seq) < K_SIZE(buf.data));
|
|
|
|
for (Size i = 0; seq[i]; i++) {
|
|
if (i >= buf.len) {
|
|
uc = ReadChar();
|
|
|
|
if (uc >= 128) {
|
|
// Got some kind of non-ASCII character, make sure nothing else matches
|
|
buf.Append(0);
|
|
return false;
|
|
}
|
|
|
|
buf.Append((char)uc);
|
|
}
|
|
if (buf[i] != seq[i])
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
if (match_escape("[1;5D")) { // Ctrl-Left
|
|
str_offset = FindBackward(str_offset, " \t\r\n");
|
|
RenderRaw();
|
|
} else if (match_escape("[1;5C")) { // Ctrl-Right
|
|
str_offset = FindForward(str_offset, " \t\r\n");
|
|
RenderRaw();
|
|
} else if (match_escape("[3~")) { // Delete
|
|
if (str_offset < str.len) {
|
|
Delete(str_offset, SkipForward(str_offset, 1));
|
|
RenderRaw();
|
|
}
|
|
} else if (match_escape("\x1B")) { // Double escape
|
|
StdErr->Write("\r\n");
|
|
StdErr->Flush();
|
|
return false;
|
|
} else if (match_escape("\x7F")) { // Alt-Backspace
|
|
Delete(FindBackward(str_offset, " \t\r\n"), str_offset);
|
|
RenderRaw();
|
|
} else if (match_escape("d")) { // Alt-D
|
|
Delete(str_offset, FindForward(str_offset, " \t\r\n"));
|
|
RenderRaw();
|
|
} else if (match_escape("[A")) { // Up
|
|
fake_input = "\x10";
|
|
} else if (match_escape("[B")) { // Down
|
|
fake_input = "\x0E";
|
|
} else if (match_escape("[D")) { // Left
|
|
fake_input = "\x02";
|
|
} else if (match_escape("[C")) { // Right
|
|
fake_input = "\x06";
|
|
} else if (match_escape("[H")) { // Home
|
|
fake_input = "\x01";
|
|
} else if (match_escape("[F")) { // End
|
|
fake_input = "\x05";
|
|
}
|
|
} break;
|
|
|
|
case 0x2: { // Left
|
|
if (str_offset > 0) {
|
|
str_offset = SkipBackward(str_offset, 1);
|
|
RenderRaw();
|
|
}
|
|
} break;
|
|
case 0x6: { // Right
|
|
if (str_offset < str.len) {
|
|
str_offset = SkipForward(str_offset, 1);
|
|
RenderRaw();
|
|
}
|
|
} break;
|
|
case 0xE: { // Down
|
|
Span<const char> remain = str.Take(str_offset, str.len - str_offset);
|
|
SplitStr(remain, '\n', &remain);
|
|
|
|
if (remain.len) {
|
|
Span<const char> line = SplitStr(remain, '\n', &remain);
|
|
|
|
Size line_offset = std::min(line.len, (Size)x - prompt_columns);
|
|
str_offset = std::min((Size)(line.ptr - str.ptr + line_offset), str.len);
|
|
|
|
RenderRaw();
|
|
} else if (entry_idx < entries.len - 1) {
|
|
ChangeEntry(entry_idx + 1);
|
|
RenderRaw();
|
|
}
|
|
} break;
|
|
case 0x10: { // Up
|
|
Span<const char> remain = str.Take(0, str_offset);
|
|
SplitStrReverse(remain, '\n', &remain);
|
|
|
|
if (remain.len) {
|
|
Span<const char> line = SplitStrReverse(remain, '\n', &remain);
|
|
|
|
Size line_offset = std::min(line.len, (Size)x - prompt_columns);
|
|
str_offset = std::min((Size)(line.ptr - str.ptr + line_offset), str.len);
|
|
|
|
RenderRaw();
|
|
} else if (entry_idx > 0) {
|
|
ChangeEntry(entry_idx - 1);
|
|
RenderRaw();
|
|
}
|
|
} break;
|
|
|
|
case 0x1: { // Home
|
|
str_offset = FindBackward(str_offset, "\n");
|
|
RenderRaw();
|
|
} break;
|
|
case 0x5: { // End
|
|
str_offset = FindForward(str_offset, "\n");
|
|
RenderRaw();
|
|
} break;
|
|
|
|
case 0x8:
|
|
case 0x7F: { // Backspace
|
|
if (str.len) {
|
|
Delete(SkipBackward(str_offset, 1), str_offset);
|
|
RenderRaw();
|
|
}
|
|
} break;
|
|
case 0x3: { // Ctrl-C
|
|
if (str.len) {
|
|
str.RemoveFrom(0);
|
|
str_offset = 0;
|
|
entry_idx = entries.len - 1;
|
|
entries[entry_idx].RemoveFrom(0);
|
|
|
|
RenderRaw();
|
|
} else {
|
|
StdErr->Write("\r\n");
|
|
StdErr->Flush();
|
|
return false;
|
|
}
|
|
} break;
|
|
case 0x4: { // Ctrl-D
|
|
if (str.len) {
|
|
Delete(str_offset, SkipForward(str_offset, 1));
|
|
RenderRaw();
|
|
} else {
|
|
return false;
|
|
}
|
|
} break;
|
|
case 0x14: { // Ctrl-T
|
|
Size middle = SkipBackward(str_offset, 1);
|
|
Size start = SkipBackward(middle, 1);
|
|
|
|
if (start < middle) {
|
|
std::rotate(str.ptr + start, str.ptr + middle, str.ptr + str_offset);
|
|
RenderRaw();
|
|
}
|
|
} break;
|
|
case 0xB: { // Ctrl-K
|
|
Delete(str_offset, FindForward(str_offset, "\n"));
|
|
RenderRaw();
|
|
} break;
|
|
case 0x15: { // Ctrl-U
|
|
Delete(FindBackward(str_offset, "\n"), str_offset);
|
|
RenderRaw();
|
|
} break;
|
|
case 0xC: { // Ctrl-L
|
|
StdErr->Write("\x1B[2J\x1B[999A");
|
|
RenderRaw();
|
|
} break;
|
|
|
|
case '\r':
|
|
case '\n': {
|
|
if (rows > y) {
|
|
Print(StdErr, "\x1B[%1B", rows - y);
|
|
}
|
|
StdErr->Write("\r\n");
|
|
StdErr->Flush();
|
|
y = rows + 1;
|
|
|
|
EnsureNulTermination();
|
|
if (out_str) {
|
|
*out_str = str;
|
|
}
|
|
return true;
|
|
} break;
|
|
|
|
case '\t': {
|
|
if (complete) {
|
|
BlockAllocator temp_alloc;
|
|
HeapArray<CompleteChoice> choices;
|
|
|
|
PushLogFilter([](LogLevel, const char *, const char *, FunctionRef<LogFunc>) {});
|
|
K_DEFER_N(log_guard) { PopLogFilter(); };
|
|
|
|
CompleteResult ret = complete(str, &temp_alloc, &choices);
|
|
|
|
switch (ret) {
|
|
case CompleteResult::Success: {
|
|
if (choices.len == 1) {
|
|
const CompleteChoice &choice = choices[0];
|
|
|
|
str.RemoveFrom(0);
|
|
str.Append(choice.value);
|
|
str_offset = str.len;
|
|
RenderRaw();
|
|
} else if (choices.len) {
|
|
for (const CompleteChoice &choice: choices) {
|
|
Print(StdErr, "\r\n %!0%!Y..%1%!0", choice.name);
|
|
}
|
|
StdErr->Write("\r\n");
|
|
|
|
RenderRaw();
|
|
}
|
|
} break;
|
|
|
|
case CompleteResult::TooMany: {
|
|
Print(StdErr, "\r\n %!0%!Y..%1%!0\r\n", T("Too many possibilities to show"));
|
|
RenderRaw();
|
|
} break;
|
|
case CompleteResult::Error: {
|
|
Print(StdErr, "\r\n %!0%!Y..%1%!0\r\n", T("Autocompletion error"));
|
|
RenderRaw();
|
|
} break;
|
|
}
|
|
|
|
break;
|
|
}
|
|
} [[fallthrough]];
|
|
|
|
default: {
|
|
LocalArray<char, 16> frag;
|
|
if (uc == '\t') {
|
|
frag.Append(" ");
|
|
} else if (!IsAsciiControl(uc)) {
|
|
frag.len = EncodeUtf8(uc, frag.data);
|
|
} else {
|
|
continue;
|
|
}
|
|
|
|
str.Grow(frag.len);
|
|
MemMove(str.ptr + str_offset + frag.len, str.ptr + str_offset, str.len - str_offset);
|
|
MemCpy(str.ptr + str_offset, frag.data, frag.len);
|
|
str.len += frag.len;
|
|
str_offset += frag.len;
|
|
|
|
if (!mask && str_offset == str.len && uc < 128 && x + frag.len < columns) {
|
|
StdErr->Write(frag.data, frag.len);
|
|
StdErr->Flush();
|
|
x += (int)frag.len;
|
|
} else {
|
|
RenderRaw();
|
|
}
|
|
} break;
|
|
}
|
|
}
|
|
|
|
EnsureNulTermination();
|
|
if (out_str) {
|
|
*out_str = str;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
Size ConsolePrompter::ReadRawEnum(Span<const PromptChoice> choices, Size value)
|
|
{
|
|
StdErr->Flush();
|
|
|
|
prompt_columns = 0;
|
|
FormatChoices(choices, value);
|
|
RenderRaw();
|
|
|
|
int32_t uc;
|
|
while ((uc = ReadChar()) >= 0) {
|
|
// Fix display if terminal is resized
|
|
if (GetConsoleSize().x != columns) {
|
|
RenderRaw();
|
|
Print(StdErr, "%!D..[Y/N]%!0 ");
|
|
}
|
|
|
|
switch (uc) {
|
|
case 0x1B: {
|
|
LocalArray<char, 16> buf;
|
|
|
|
const auto match_escape = [&](const char *seq) {
|
|
K_ASSERT(strlen(seq) < K_SIZE(buf.data));
|
|
|
|
for (Size i = 0; seq[i]; i++) {
|
|
if (i >= buf.len) {
|
|
uc = ReadChar();
|
|
|
|
if (uc >= 128) {
|
|
// Got some kind of non-ASCII character, make sure nothing else matches
|
|
buf.Append(0);
|
|
return false;
|
|
}
|
|
|
|
buf.Append((char)uc);
|
|
}
|
|
if (buf[i] != seq[i])
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
if (match_escape("[A")) { // Up
|
|
fake_input = "\x10";
|
|
} else if (match_escape("[B")) { // Down
|
|
fake_input = "\x0E";
|
|
} else if (match_escape("\x1B")) { // Double escape
|
|
if (rows > y) {
|
|
Print(StdErr, "\x1B[%1B", rows - y);
|
|
}
|
|
StdErr->Write("\r");
|
|
StdErr->Flush();
|
|
|
|
return -1;
|
|
}
|
|
} break;
|
|
|
|
case 0x3: // Ctrl-C
|
|
case 0x4: { // Ctrl-D
|
|
if (rows > y) {
|
|
Print(StdErr, "\x1B[%1B", rows - y);
|
|
}
|
|
StdErr->Write("\r");
|
|
StdErr->Flush();
|
|
|
|
return -1;
|
|
} break;
|
|
|
|
case 0xE: { // Down
|
|
if (value + 1 < choices.len) {
|
|
FormatChoices(choices, ++value);
|
|
RenderRaw();
|
|
}
|
|
} break;
|
|
case 0x10: { // Up
|
|
if (value > 0) {
|
|
FormatChoices(choices, --value);
|
|
RenderRaw();
|
|
}
|
|
} break;
|
|
|
|
default: {
|
|
const auto it = std::find_if(choices.begin(), choices.end(),
|
|
[&](const PromptChoice &choice) { return choice.c == uc; });
|
|
if (it == choices.end())
|
|
break;
|
|
value = it - choices.begin();
|
|
} [[fallthrough]];
|
|
|
|
case '\r':
|
|
case '\n': {
|
|
str.RemoveFrom(0);
|
|
str.Append(choices[value].str);
|
|
str_offset = str.len;
|
|
RenderRaw();
|
|
|
|
StdErr->Write("\r\n");
|
|
StdErr->Flush();
|
|
|
|
return value;
|
|
} break;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
bool ConsolePrompter::ReadBuffered(Span<const char> *out_str)
|
|
{
|
|
prompt_columns = ComputeUnicodeWidth(prompt) + 1;
|
|
|
|
RenderBuffered();
|
|
|
|
do {
|
|
uint8_t c = 0;
|
|
if (StdIn->Read(MakeSpan(&c, 1)) < 0)
|
|
return false;
|
|
|
|
if (c == '\n') {
|
|
EnsureNulTermination();
|
|
if (out_str) {
|
|
*out_str = str;
|
|
}
|
|
return true;
|
|
} else if (!IsAsciiControl(c)) {
|
|
str.Append((char)c);
|
|
}
|
|
} while (!StdIn->IsEOF());
|
|
|
|
// EOF
|
|
return false;
|
|
}
|
|
|
|
Size ConsolePrompter::ReadBufferedEnum(Span<const PromptChoice> choices)
|
|
{
|
|
static const Span<const char> prefix = "Input your choice: ";
|
|
|
|
prompt_columns = 0;
|
|
FormatChoices(choices, 0);
|
|
RenderBuffered();
|
|
|
|
Print(StdErr, "\n%1", prefix);
|
|
StdErr->Flush();
|
|
|
|
do {
|
|
uint8_t c = 0;
|
|
if (StdIn->Read(MakeSpan(&c, 1)) < 0)
|
|
return -1;
|
|
|
|
if (c == '\n') {
|
|
Span<const char> end = TrimStr(SplitStrReverse(str, '\n'));
|
|
|
|
if (end.len == 1) {
|
|
const auto it = std::find_if(choices.begin(), choices.end(),
|
|
[&](const PromptChoice &choice) { return choice.c == end[0]; });
|
|
if (it != choices.end())
|
|
return it - choices.ptr;
|
|
}
|
|
|
|
str.RemoveFrom(end.ptr - str.ptr);
|
|
|
|
StdErr->Write(prefix);
|
|
StdErr->Flush();
|
|
} else if (!IsAsciiControl(c)) {
|
|
str.Append((char)c);
|
|
}
|
|
} while (!StdIn->IsEOF());
|
|
|
|
// EOF
|
|
return -1;
|
|
}
|
|
|
|
void ConsolePrompter::ChangeEntry(Size new_idx)
|
|
{
|
|
if (str.len) {
|
|
std::swap(str, entries[entry_idx]);
|
|
}
|
|
|
|
str.RemoveFrom(0);
|
|
str.Append(entries[new_idx]);
|
|
str_offset = str.len;
|
|
entry_idx = new_idx;
|
|
}
|
|
|
|
Size ConsolePrompter::SkipForward(Size offset, Size count)
|
|
{
|
|
if (offset < str.len) {
|
|
offset++;
|
|
|
|
while (offset < str.len && (((str[offset] & 0xC0) == 0x80) || --count)) {
|
|
offset++;
|
|
}
|
|
}
|
|
|
|
return offset;
|
|
}
|
|
|
|
Size ConsolePrompter::SkipBackward(Size offset, Size count)
|
|
{
|
|
if (offset > 0) {
|
|
offset--;
|
|
|
|
while (offset > 0 && (((str[offset] & 0xC0) == 0x80) || --count)) {
|
|
offset--;
|
|
}
|
|
}
|
|
|
|
return offset;
|
|
}
|
|
|
|
Size ConsolePrompter::FindForward(Size offset, const char *chars)
|
|
{
|
|
while (offset < str.len && strchr(chars, str[offset])) {
|
|
offset++;
|
|
}
|
|
while (offset < str.len && !strchr(chars, str[offset])) {
|
|
offset++;
|
|
}
|
|
|
|
return offset;
|
|
}
|
|
|
|
Size ConsolePrompter::FindBackward(Size offset, const char *chars)
|
|
{
|
|
if (offset > 0) {
|
|
offset--;
|
|
|
|
while (offset > 0 && strchr(chars, str[offset])) {
|
|
offset--;
|
|
}
|
|
while (offset > 0 && !strchr(chars, str[offset - 1])) {
|
|
offset--;
|
|
}
|
|
}
|
|
|
|
return offset;
|
|
}
|
|
|
|
void ConsolePrompter::Delete(Size start, Size end)
|
|
{
|
|
K_ASSERT(start >= 0);
|
|
K_ASSERT(end >= start && end <= str.len);
|
|
|
|
MemMove(str.ptr + start, str.ptr + end, str.len - end);
|
|
str.len -= end - start;
|
|
|
|
if (str_offset > end) {
|
|
str_offset -= end - start;
|
|
} else if (str_offset > start) {
|
|
str_offset = start;
|
|
}
|
|
}
|
|
|
|
void ConsolePrompter::FormatChoices(Span<const PromptChoice> choices, Size value)
|
|
{
|
|
int align = 0;
|
|
|
|
for (const PromptChoice &choice: choices) {
|
|
align = std::max(align, (int)ComputeUnicodeWidth(choice.str));
|
|
}
|
|
|
|
str.RemoveFrom(0);
|
|
str.Append('\n');
|
|
for (Size i = 0; i < choices.len; i++) {
|
|
const PromptChoice &choice = choices[i];
|
|
int pad = align - ComputeUnicodeWidth(choice.str);
|
|
|
|
if (choice.c) {
|
|
Fmt(&str, " [%1] %2%3 ", choice.c, choice.str, FmtRepeat(" ", pad));
|
|
} else {
|
|
Fmt(&str, " %1%2 ", choice.str, FmtRepeat(" ", pad));
|
|
}
|
|
if (i == value) {
|
|
str_offset = str.len;
|
|
}
|
|
str.Append('\n');
|
|
}
|
|
}
|
|
|
|
void ConsolePrompter::RenderRaw()
|
|
{
|
|
columns = GetConsoleSize().x;
|
|
rows = 0;
|
|
|
|
int mask_columns = mask ? ComputeUnicodeWidth(mask) : 0;
|
|
|
|
// Hide cursor during refresh
|
|
StdErr->Write("\x1B[?25l");
|
|
if (y) {
|
|
Print(StdErr, "\x1B[%1A", y);
|
|
}
|
|
|
|
// Output prompt(s) and string lines
|
|
{
|
|
Size i = 0;
|
|
int x2 = prompt_columns;
|
|
|
|
Print(StdErr, "\r%!0%1 %!..+", prompt);
|
|
|
|
for (;;) {
|
|
if (i == str_offset) {
|
|
x = x2;
|
|
y = rows;
|
|
}
|
|
if (i >= str.len)
|
|
break;
|
|
|
|
Size bytes = std::min((Size)CountUtf8Bytes(str[i]), str.len - i);
|
|
int width = mask ? mask_columns : ComputeUnicodeWidth(str.Take(i, bytes));
|
|
|
|
if (x2 + width >= columns || str[i] == '\n') {
|
|
FmtArg prefix = FmtRepeat(" ", prompt_columns - 1);
|
|
Print(StdErr, "\x1B[0K\r\n%!D.+%1%!0 %!..+", prefix);
|
|
|
|
x2 = prompt_columns;
|
|
rows++;
|
|
}
|
|
if (width > 0) {
|
|
if (mask) {
|
|
StdErr->Write(mask);
|
|
} else {
|
|
StdErr->Write(str.ptr + i, bytes);
|
|
}
|
|
}
|
|
|
|
x2 += width;
|
|
i += bytes;
|
|
}
|
|
StdErr->Write("\x1B[0K");
|
|
}
|
|
|
|
// Clear remaining rows
|
|
for (int i = rows; i < rows_with_extra; i++) {
|
|
StdErr->Write("\r\n\x1B[0K");
|
|
}
|
|
rows_with_extra = std::max(rows_with_extra, rows);
|
|
|
|
// Fix up cursor and show it
|
|
if (rows_with_extra > y) {
|
|
Print(StdErr, "\x1B[%1A", rows_with_extra - y);
|
|
}
|
|
Print(StdErr, "\r\x1B[%1C", x);
|
|
Print(StdErr, "\x1B[?25h");
|
|
|
|
StdErr->Flush();
|
|
}
|
|
|
|
void ConsolePrompter::RenderBuffered()
|
|
{
|
|
Span<const char> remain = str;
|
|
Span<const char> line = SplitStr(remain, '\n', &remain);
|
|
|
|
Print(StdErr, "%1 %2", prompt, line);
|
|
while (remain.len) {
|
|
line = SplitStr(remain, '\n', &remain);
|
|
Print(StdErr, "\n%1%2", FmtRepeat(" ", prompt_columns), line);
|
|
}
|
|
|
|
StdErr->Flush();
|
|
}
|
|
|
|
Vec2<int> ConsolePrompter::GetConsoleSize()
|
|
{
|
|
#if defined(_WIN32)
|
|
HANDLE h = (HANDLE)_get_osfhandle(STDERR_FILENO);
|
|
|
|
CONSOLE_SCREEN_BUFFER_INFO screen;
|
|
if (GetConsoleScreenBufferInfo(h, &screen))
|
|
return { screen.dwSize.X, screen.dwSize.Y };
|
|
#elif !defined(__wasm__)
|
|
struct winsize ws;
|
|
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) >= 0 && ws.ws_col)
|
|
return { ws.ws_col, ws.ws_row };
|
|
#endif
|
|
|
|
// Give up!
|
|
return { 80, 24 };
|
|
}
|
|
|
|
int32_t ConsolePrompter::ReadChar()
|
|
{
|
|
if (fake_input[0]) {
|
|
int c = fake_input[0];
|
|
fake_input++;
|
|
return c;
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
HANDLE h = (HANDLE)_get_osfhandle(STDIN_FILENO);
|
|
|
|
for (;;) {
|
|
INPUT_RECORD ev;
|
|
DWORD ev_len;
|
|
if (!ReadConsoleInputW(h, &ev, 1, &ev_len))
|
|
return -1;
|
|
if (!ev_len)
|
|
return -1;
|
|
|
|
if (ev.EventType == KEY_EVENT && ev.Event.KeyEvent.bKeyDown) {
|
|
bool ctrl = ev.Event.KeyEvent.dwControlKeyState & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED);
|
|
bool alt = ev.Event.KeyEvent.dwControlKeyState & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED);
|
|
|
|
if (ctrl && !alt) {
|
|
switch (ev.Event.KeyEvent.wVirtualKeyCode) {
|
|
case 'A': return 0x1;
|
|
case 'B': return 0x2;
|
|
case 'C': return 0x3;
|
|
case 'D': return 0x4;
|
|
case 'E': return 0x5;
|
|
case 'F': return 0x6;
|
|
case 'H': return 0x8;
|
|
case 'K': return 0xB;
|
|
case 'L': return 0xC;
|
|
case 'N': return 0xE;
|
|
case 'P': return 0x10;
|
|
case 'T': return 0x14;
|
|
case 'U': return 0x15;
|
|
case VK_LEFT: {
|
|
fake_input = "[1;5D";
|
|
return 0x1B;
|
|
} break;
|
|
case VK_RIGHT: {
|
|
fake_input = "[1;5C";
|
|
return 0x1B;
|
|
} break;
|
|
}
|
|
} else {
|
|
if (alt) {
|
|
switch (ev.Event.KeyEvent.wVirtualKeyCode) {
|
|
case VK_BACK: {
|
|
fake_input = "\x7F";
|
|
return 0x1B;
|
|
} break;
|
|
case 'D': {
|
|
fake_input = "d";
|
|
return 0x1B;
|
|
} break;
|
|
}
|
|
}
|
|
|
|
switch (ev.Event.KeyEvent.wVirtualKeyCode) {
|
|
case VK_UP: return 0x10;
|
|
case VK_DOWN: return 0xE;
|
|
case VK_LEFT: return 0x2;
|
|
case VK_RIGHT: return 0x6;
|
|
case VK_HOME: return 0x1;
|
|
case VK_END: return 0x5;
|
|
case VK_RETURN: return '\r';
|
|
case VK_BACK: return 0x8;
|
|
case VK_DELETE: {
|
|
fake_input = "[3~";
|
|
return 0x1B;
|
|
} break;
|
|
|
|
default: {
|
|
uint32_t uc = ev.Event.KeyEvent.uChar.UnicodeChar;
|
|
|
|
if ((uc - 0xD800u) < 0x800u) {
|
|
if ((uc & 0xFC00u) == 0xD800u) {
|
|
surrogate_buf = uc;
|
|
return 0;
|
|
} else if (surrogate_buf && (uc & 0xFC00) == 0xDC00) {
|
|
uc = (surrogate_buf << 10) + uc - 0x35FDC00;
|
|
} else {
|
|
// Yeah something is up. Give up on this character.
|
|
surrogate_buf = 0;
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
return (int32_t)uc;
|
|
} break;
|
|
}
|
|
}
|
|
} else if (ev.EventType == WINDOW_BUFFER_SIZE_EVENT) {
|
|
return 0;
|
|
}
|
|
}
|
|
#else
|
|
int32_t uc = 0;
|
|
|
|
{
|
|
uint8_t c = 0;
|
|
ssize_t read_len = read(STDIN_FILENO, &c, 1);
|
|
if (read_len < 0)
|
|
goto error;
|
|
if (!read_len)
|
|
return -1;
|
|
uc = c;
|
|
}
|
|
|
|
if (uc >= 128) {
|
|
Size bytes = CountUtf8Bytes((char)uc);
|
|
|
|
LocalArray<char, 4> buf;
|
|
buf.Append((char)uc);
|
|
buf.len += read(STDIN_FILENO, buf.end(), bytes - 1);
|
|
if (buf.len < 1)
|
|
goto error;
|
|
|
|
if (buf.len != bytes)
|
|
return 0;
|
|
if (DecodeUtf8(buf, 0, &uc) != bytes)
|
|
return 0;
|
|
}
|
|
|
|
return uc;
|
|
|
|
error:
|
|
if (errno == EINTR) {
|
|
// Could be SIGWINCH, give the user a chance to deal with it
|
|
return 0;
|
|
} else {
|
|
LogError("Failed to read from standard input: %1", strerror(errno));
|
|
return -1;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void ConsolePrompter::EnsureNulTermination()
|
|
{
|
|
str.Grow(1);
|
|
str.ptr[str.len] = 0;
|
|
}
|
|
|
|
const char *Prompt(const char *prompt, const char *default_value, const char *mask, Allocator *alloc)
|
|
{
|
|
K_ASSERT(alloc);
|
|
|
|
ConsolePrompter prompter;
|
|
|
|
prompter.prompt = prompt;
|
|
prompter.mask = mask;
|
|
|
|
prompter.str.allocator = alloc;
|
|
if (default_value) {
|
|
prompter.str.Append(default_value);
|
|
}
|
|
|
|
if (!prompter.Read())
|
|
return nullptr;
|
|
|
|
const char *str = prompter.str.Leak().ptr;
|
|
return str;
|
|
}
|
|
|
|
Size PromptEnum(const char *prompt, Span<const PromptChoice> choices, Size value)
|
|
{
|
|
#if defined(K_DEBUG)
|
|
{
|
|
HashSet<char> keys;
|
|
|
|
for (const PromptChoice &choice: choices) {
|
|
if (!choice.c)
|
|
continue;
|
|
|
|
bool duplicates = !keys.InsertOrFail(choice.c);
|
|
K_ASSERT(!duplicates);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
ConsolePrompter prompter;
|
|
prompter.prompt = prompt;
|
|
|
|
return prompter.ReadEnum(choices, value);
|
|
}
|
|
|
|
Size PromptEnum(const char *prompt, Span<const char *const> strings, Size value)
|
|
{
|
|
static const char literals[] = "123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
|
|
HeapArray<PromptChoice> choices;
|
|
|
|
for (Size i = 0; i < strings.len; i++) {
|
|
const char *str = strings[i];
|
|
PromptChoice choice = { str, i < K_LEN(literals) ? literals[i] : (char)0 };
|
|
|
|
choices.Append(choice);
|
|
}
|
|
|
|
return PromptEnum(prompt, choices, value);
|
|
}
|
|
|
|
int PromptYN(const char *prompt)
|
|
{
|
|
const char *yes = T("Yes");
|
|
const char *no = T("No");
|
|
|
|
const char *shortcuts = T("yn");
|
|
K_ASSERT(strlen(shortcuts) == 2);
|
|
|
|
Size ret = PromptEnum(prompt, {{ yes, shortcuts[0] }, { no, shortcuts[1] }});
|
|
if (ret < 0)
|
|
return -1;
|
|
|
|
return !ret;
|
|
}
|
|
|
|
const char *PromptPath(const char *prompt, const char *default_path, Span<const char> root_directory, Allocator *alloc)
|
|
{
|
|
K_ASSERT(alloc);
|
|
|
|
ConsolePrompter prompter;
|
|
|
|
prompter.prompt = prompt;
|
|
prompter.complete = [&](Span<const char> str, Allocator *alloc, HeapArray<CompleteChoice> *out_choices) {
|
|
Size start_len = out_choices->len;
|
|
K_DEFER_N(err_guard) { out_choices->RemoveFrom(start_len); };
|
|
|
|
Span<const char> path = TrimStrRight(str, K_PATH_SEPARATORS);
|
|
bool separator = (path.len < str.len);
|
|
|
|
// If the value points to a directory, append separator and return
|
|
if (str.len && !separator) {
|
|
const char *filename = NormalizePath(path, root_directory, alloc).ptr;
|
|
|
|
FileInfo file_info;
|
|
StatResult ret = StatFile(filename, (int)StatFlag::SilentMissing | (int)StatFlag::FollowSymlink, &file_info);
|
|
|
|
if (ret == StatResult::Success && file_info.type == FileType::Directory) {
|
|
const char *value = Fmt(alloc, "%1%/", path).ptr;
|
|
out_choices->Append({ value, value });
|
|
|
|
err_guard.Disable();
|
|
return CompleteResult::Success;
|
|
}
|
|
}
|
|
|
|
Span<const char> directory = path;
|
|
Span<const char> prefix = separator ? "" : SplitStrReverseAny(path, K_PATH_SEPARATORS, &directory);
|
|
|
|
// EnumerateDirectory takes a C string, so we need the NUL terminator,
|
|
// and we also need to take root_dir into account.
|
|
const char *dirname = nullptr;
|
|
|
|
if (PathIsAbsolute(directory)) {
|
|
dirname = DuplicateString(directory, alloc).ptr;
|
|
} else {
|
|
if (!root_directory.len)
|
|
return CompleteResult::Success;
|
|
|
|
dirname = NormalizePath(directory, root_directory, alloc).ptr;
|
|
dirname = dirname[0] ? dirname : ".";
|
|
}
|
|
|
|
EnumResult ret = EnumerateDirectory(dirname, nullptr, -1, [&](const char *basename, FileType file_type) {
|
|
#if defined(_WIN32)
|
|
if (!StartsWithI(basename, prefix))
|
|
return true;
|
|
#else
|
|
if (!StartsWith(basename, prefix))
|
|
return true;
|
|
#endif
|
|
|
|
if (out_choices->len - start_len >= K_COMPLETE_PATH_LIMIT)
|
|
return false;
|
|
|
|
CompleteChoice choice;
|
|
{
|
|
HeapArray<char> buf(alloc);
|
|
|
|
// Make directory part
|
|
buf.Append(directory);
|
|
if (directory.len && !IsPathSeparator(directory[directory.len - 1])) {
|
|
buf.Append(*K_PATH_SEPARATORS);
|
|
}
|
|
|
|
Size name_offset = buf.len;
|
|
|
|
// Append name
|
|
buf.Append(basename);
|
|
if (file_type == FileType::Directory) {
|
|
buf.Append(*K_PATH_SEPARATORS);
|
|
}
|
|
buf.Append(0);
|
|
buf.Trim();
|
|
|
|
choice.value = buf.Leak().ptr;
|
|
choice.name = choice.value + name_offset;
|
|
}
|
|
|
|
out_choices->Append(choice);
|
|
return true;
|
|
});
|
|
|
|
if (ret == EnumResult::CallbackFail) {
|
|
return CompleteResult::TooMany;
|
|
} else if (ret != EnumResult::Success) {
|
|
// Just ignore it and don't print anything
|
|
return CompleteResult::Success;
|
|
}
|
|
|
|
std::sort(out_choices->ptr + start_len, out_choices->end(),
|
|
[](const CompleteChoice &choice1, const CompleteChoice &choice2) { return CmpNaturalI(choice1.name, choice2.name) < 0; });
|
|
|
|
err_guard.Disable();
|
|
return CompleteResult::Success;
|
|
};
|
|
|
|
prompter.str.allocator = alloc;
|
|
if (default_path) {
|
|
prompter.str.Append(default_path);
|
|
}
|
|
|
|
if (!prompter.Read())
|
|
return nullptr;
|
|
|
|
const char *str = NormalizePath(prompter.str, alloc).ptr;
|
|
return str;
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Mime types
|
|
// ------------------------------------------------------------------------
|
|
|
|
const char *GetMimeType(Span<const char> extension, const char *default_type)
|
|
{
|
|
static const HashMap<Span<const char>, const char *> mimetypes = {
|
|
#define MIMETYPE(Extension, MimeType) { (Extension), (MimeType) },
|
|
#include "mimetypes.inc"
|
|
|
|
{ "", "application/octet-stream" }
|
|
};
|
|
|
|
char lower[32];
|
|
{
|
|
Size take = std::min(extension.len, (Size)16);
|
|
Span<const char> truncated = extension.Take(0, take);
|
|
|
|
for (Size i = 0; i < truncated.len; i++) {
|
|
lower[i] = LowerAscii(truncated[i]);
|
|
}
|
|
lower[truncated.len] = 0;
|
|
}
|
|
|
|
const char *mimetype = mimetypes.FindValue(lower, nullptr);
|
|
|
|
if (!mimetype) {
|
|
LogError("Unknown MIME type for extension '%1'", extension);
|
|
mimetype = default_type;
|
|
}
|
|
|
|
return mimetype;
|
|
}
|
|
|
|
bool CanCompressFile(const char *filename)
|
|
{
|
|
char extension[8];
|
|
{
|
|
const char *ptr = GetPathExtension(filename).ptr;
|
|
|
|
Size i = 0;
|
|
while (i < K_SIZE(extension) - 1 && ptr[i]) {
|
|
extension[i] = LowerAscii(ptr[i]);
|
|
i++;
|
|
}
|
|
extension[i] = 0;
|
|
}
|
|
|
|
if (TestStrI(extension, ".zip"))
|
|
return false;
|
|
if (TestStrI(extension, ".rar"))
|
|
return false;
|
|
if (TestStrI(extension, ".7z"))
|
|
return false;
|
|
if (TestStrI(extension, ".gz") || TestStrI(extension, ".tgz"))
|
|
return false;
|
|
if (TestStrI(extension, ".bz2") || TestStrI(extension, ".tbz2"))
|
|
return false;
|
|
if (TestStrI(extension, ".xz") || TestStrI(extension, ".txz"))
|
|
return false;
|
|
if (TestStrI(extension, ".zst") || TestStrI(extension, ".tzst"))
|
|
return false;
|
|
if (TestStrI(extension, ".woff") || TestStrI(extension, ".woff2"))
|
|
return false;
|
|
if (TestStrI(extension, ".db") || TestStrI(extension, ".sqlite3"))
|
|
return false;
|
|
|
|
const char *mimetype = GetMimeType(extension);
|
|
|
|
if (StartsWith(mimetype, "video/"))
|
|
return false;
|
|
if (StartsWith(mimetype, "audio/"))
|
|
return false;
|
|
if (StartsWith(mimetype, "image/") && !TestStr(mimetype, "image/svg+xml"))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Unicode
|
|
// ------------------------------------------------------------------------
|
|
|
|
bool IsValidUtf8(Span<const char> str)
|
|
{
|
|
Size i = 0;
|
|
|
|
while (i < str.len) {
|
|
int32_t uc;
|
|
Size bytes = DecodeUtf8(str, i, &uc);
|
|
|
|
if (!bytes) [[unlikely]]
|
|
return false;
|
|
|
|
i += bytes;
|
|
}
|
|
|
|
return i == str.len;
|
|
}
|
|
|
|
static bool TestUnicodeTable(Span<const int32_t> table, int32_t uc)
|
|
{
|
|
K_ASSERT(table.len > 0);
|
|
K_ASSERT(table.len % 2 == 0);
|
|
|
|
auto it = std::upper_bound(table.begin(), table.end(), uc,
|
|
[](int32_t uc, int32_t x) { return uc < x; });
|
|
Size idx = it - table.ptr;
|
|
|
|
// Each pair of value in table represents a valid interval
|
|
return idx & 0x1;
|
|
}
|
|
|
|
static inline int ComputeCharacterWidth(int32_t uc)
|
|
{
|
|
// Fast path
|
|
if (uc < 128)
|
|
return IsAsciiControl(uc) ? 0 : 1;
|
|
|
|
if (TestUnicodeTable(WcWidthNull, uc))
|
|
return 0;
|
|
if (TestUnicodeTable(WcWidthWide, uc))
|
|
return 2;
|
|
|
|
return 1;
|
|
}
|
|
|
|
int ComputeUnicodeWidth(Span<const char> str)
|
|
{
|
|
Size i = 0;
|
|
int width = 0;
|
|
|
|
while (i < str.len) {
|
|
int32_t uc;
|
|
Size bytes = DecodeUtf8(str, i, &uc);
|
|
|
|
if (!bytes) [[unlikely]]
|
|
return false;
|
|
|
|
i += bytes;
|
|
width += ComputeCharacterWidth(uc);
|
|
}
|
|
|
|
return width;
|
|
}
|
|
|
|
bool IsXidStart(int32_t uc)
|
|
{
|
|
if (IsAsciiAlpha(uc))
|
|
return true;
|
|
if (uc == '_')
|
|
return true;
|
|
if (TestUnicodeTable(XidStartTable, uc))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
bool IsXidContinue(int32_t uc)
|
|
{
|
|
if (IsAsciiAlphaOrDigit(uc))
|
|
return true;
|
|
if (uc == '_')
|
|
return true;
|
|
if (TestUnicodeTable(XidContinueTable, uc))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// CRC
|
|
// ------------------------------------------------------------------------
|
|
|
|
uint32_t CRC32(uint32_t state, Span<const uint8_t> buf)
|
|
{
|
|
state = ~state;
|
|
|
|
Size right = buf.len & (K_SIZE_MAX - 3);
|
|
|
|
for (Size i = 0; i < right; i += 4) {
|
|
state = (state >> 8) ^ Crc32Table[(state ^ buf[i + 0]) & 0xFF];
|
|
state = (state >> 8) ^ Crc32Table[(state ^ buf[i + 1]) & 0xFF];
|
|
state = (state >> 8) ^ Crc32Table[(state ^ buf[i + 2]) & 0xFF];
|
|
state = (state >> 8) ^ Crc32Table[(state ^ buf[i + 3]) & 0xFF];
|
|
}
|
|
for (Size i = right; i < buf.len; i++) {
|
|
state = (state >> 8) ^ Crc32Table[(state ^ buf[i]) & 0xFF];
|
|
}
|
|
|
|
return ~state;
|
|
}
|
|
|
|
uint32_t CRC32C(uint32_t state, Span<const uint8_t> buf)
|
|
{
|
|
state = ~state;
|
|
|
|
Size right = buf.len & (K_SIZE_MAX - 3);
|
|
|
|
for (Size i = 0; i < right; i += 4) {
|
|
state = (state >> 8) ^ Crc32CTable[(state ^ buf[i + 0]) & 0xFF];
|
|
state = (state >> 8) ^ Crc32CTable[(state ^ buf[i + 1]) & 0xFF];
|
|
state = (state >> 8) ^ Crc32CTable[(state ^ buf[i + 2]) & 0xFF];
|
|
state = (state >> 8) ^ Crc32CTable[(state ^ buf[i + 3]) & 0xFF];
|
|
}
|
|
for (Size i = right; i < buf.len; i++) {
|
|
state = (state >> 8) ^ Crc32CTable[(state ^ buf[i]) & 0xFF];
|
|
}
|
|
|
|
return ~state;
|
|
}
|
|
|
|
static uint64_t XzUpdate1(uint64_t state, uint8_t byte)
|
|
{
|
|
uint64_t ret = (state >> 8) ^ Crc64XzTable0[byte ^ (uint8_t)state];
|
|
return ret;
|
|
}
|
|
|
|
static uint64_t XzUpdate16(uint64_t state, const uint8_t *bytes)
|
|
{
|
|
uint64_t ret = Crc64XzTable0[bytes[15]] ^
|
|
Crc64XzTable1[bytes[14]] ^
|
|
Crc64XzTable2[bytes[13]] ^
|
|
Crc64XzTable3[bytes[12]] ^
|
|
Crc64XzTable4[bytes[11]] ^
|
|
Crc64XzTable5[bytes[10]] ^
|
|
Crc64XzTable6[bytes[9]] ^
|
|
Crc64XzTable7[bytes[8]] ^
|
|
Crc64XzTable8[bytes[7] ^ (uint8_t)(state >> 56)] ^
|
|
Crc64XzTable9[bytes[6] ^ (uint8_t)(state >> 48)] ^
|
|
Crc64XzTable10[bytes[5] ^ (uint8_t)(state >> 40)] ^
|
|
Crc64XzTable11[bytes[4] ^ (uint8_t)(state >> 32)] ^
|
|
Crc64XzTable12[bytes[3] ^ (uint8_t)(state >> 24)] ^
|
|
Crc64XzTable13[bytes[2] ^ (uint8_t)(state >> 16)] ^
|
|
Crc64XzTable14[bytes[1] ^ (uint8_t)(state >> 8)] ^
|
|
Crc64XzTable15[bytes[0] ^ (uint8_t)(state >> 0)];
|
|
return ret;
|
|
}
|
|
|
|
uint64_t CRC64xz(uint64_t state, Span<const uint8_t> buf)
|
|
{
|
|
state = ~state;
|
|
|
|
Size len16 = buf.len / 16 * 16;
|
|
|
|
for (Size i = 0; i < len16; i += 16) {
|
|
state = XzUpdate16(state, buf.ptr + i);
|
|
}
|
|
for (Size i = len16; i < buf.len; i++) {
|
|
state = XzUpdate1(state, buf[i]);
|
|
}
|
|
|
|
return ~state;
|
|
}
|
|
|
|
static uint64_t NvmeUpdate1(uint64_t state, uint8_t byte)
|
|
{
|
|
uint64_t ret = (state >> 8) ^ Crc64NvmeTable0[byte ^ (uint8_t)state];
|
|
return ret;
|
|
}
|
|
|
|
static uint64_t NvmeUpdate16(uint64_t state, const uint8_t *bytes)
|
|
{
|
|
uint64_t ret = Crc64NvmeTable0[bytes[15]] ^
|
|
Crc64NvmeTable1[bytes[14]] ^
|
|
Crc64NvmeTable2[bytes[13]] ^
|
|
Crc64NvmeTable3[bytes[12]] ^
|
|
Crc64NvmeTable4[bytes[11]] ^
|
|
Crc64NvmeTable5[bytes[10]] ^
|
|
Crc64NvmeTable6[bytes[9]] ^
|
|
Crc64NvmeTable7[bytes[8]] ^
|
|
Crc64NvmeTable8[bytes[7] ^ (uint8_t)(state >> 56)] ^
|
|
Crc64NvmeTable9[bytes[6] ^ (uint8_t)(state >> 48)] ^
|
|
Crc64NvmeTable10[bytes[5] ^ (uint8_t)(state >> 40)] ^
|
|
Crc64NvmeTable11[bytes[4] ^ (uint8_t)(state >> 32)] ^
|
|
Crc64NvmeTable12[bytes[3] ^ (uint8_t)(state >> 24)] ^
|
|
Crc64NvmeTable13[bytes[2] ^ (uint8_t)(state >> 16)] ^
|
|
Crc64NvmeTable14[bytes[1] ^ (uint8_t)(state >> 8)] ^
|
|
Crc64NvmeTable15[bytes[0] ^ (uint8_t)(state >> 0)];
|
|
return ret;
|
|
}
|
|
|
|
uint64_t CRC64nvme(uint64_t state, Span<const uint8_t> buf)
|
|
{
|
|
state = ~state;
|
|
|
|
Size len16 = buf.len / 16 * 16;
|
|
|
|
for (Size i = 0; i < len16; i += 16) {
|
|
state = NvmeUpdate16(state, buf.ptr + i);
|
|
}
|
|
for (Size i = len16; i < buf.len; i++) {
|
|
state = NvmeUpdate1(state, buf[i]);
|
|
}
|
|
|
|
return ~state;
|
|
}
|
|
|
|
}
|