sysclk: remove sysclk old and libultrahand old

This commit is contained in:
souldbminersmwc
2026-04-01 16:00:54 -04:00
parent 554b66e25f
commit 5f2d7a68a9
203 changed files with 37127 additions and 27224 deletions

View File

@@ -0,0 +1,410 @@
/********************************************************************************
* File: audio.cpp
* Author: ppkantorski
* Description:
* Render-thread-safe audio with a single shared DMA playback buffer.
* Key design:
* - rawBuf : only per-sound allocation — compact native-channel 16-bit PCM,
* native sample rate, no volume. Kept for re-render on vol/dock change.
* - m_playBuf : single shared DMA-ready buffer, sized to the largest sound's
* 48 kHz stereo output. All sounds share this one allocation.
* - renderToPlayBuf() runs inside playSound(): resamples to 48 kHz (linear interp),
* expands mono → L+R, and applies volume in one pass.
* - No per-sound stereo copy is kept — memory cost per sound is rawBuf only.
* - m_playBuf is safe to reuse because audout is always drained before writing.
* - Volume and dock state are read live at render time — no stale tracking needed.
* - Supports any WAV sample rate ≤ 48 kHz (8/11025/16000/22050/32000/44100/48000 Hz).
* Source rates > 48 kHz are rejected at load time.
*
* For the latest updates and contributions, visit the project's GitHub repository.
* (GitHub Repository: https://github.com/ppkantorski/Ultrahand-Overlay)
*
* Note: Please be aware that this notice cannot be altered or removed. It is a part
* of the project's documentation and must remain intact.
*
* Licensed under both GPLv2 and CC-BY-4.0
* Copyright (c) 2025-2026 ppkantorski
********************************************************************************/
#include "audio.hpp"
namespace ult {
// ── Static member definitions ─────────────────────────────────────────────
bool Audio::m_initialized = false;
std::atomic<bool> Audio::m_enabled{true};
std::atomic<int32_t> Audio::m_masterVolumeFixed{154}; // 0.6 * 256 ≈ 154
bool Audio::m_lastDockedState = false;
std::vector<Audio::CachedSound> Audio::m_cachedSounds;
std::mutex Audio::m_audioMutex;
void* Audio::m_playBuf = nullptr;
uint32_t Audio::m_playBufCap = 0;
AudioOutBuffer Audio::m_audoutBuf = {};
// 4 KB — required by Switch audout DMA
static constexpr uint32_t AUDIO_ALIGN = 0x1000;
static constexpr uint32_t TARGET_RATE = 48000;
// ── initialize ────────────────────────────────────────────────────────────
bool Audio::initialize() {
std::lock_guard<std::mutex> lock(m_audioMutex);
if (m_initialized) return true;
if (R_FAILED(audoutInitialize()) || R_FAILED(audoutStartAudioOut())) {
audoutExit();
return false;
}
m_initialized = true;
m_cachedSounds.resize(static_cast<uint32_t>(SoundType::Count));
m_lastDockedState = ult::consoleIsDocked();
reloadAllSounds();
return true;
}
// ── exit ──────────────────────────────────────────────────────────────────
void Audio::exit() {
std::lock_guard<std::mutex> lock(m_audioMutex);
for (auto& s : m_cachedSounds) {
free(s.rawBuf);
s = CachedSound{};
}
free(m_playBuf);
m_playBuf = nullptr;
m_playBufCap = 0;
m_audoutBuf = {};
if (m_initialized) {
audoutStopAudioOut();
audoutExit();
m_initialized = false;
}
}
// ── reloadAllSounds ───────────────────────────────────────────────────────
void Audio::reloadAllSounds() {
for (uint32_t i = 0; i < static_cast<uint32_t>(SoundType::Count); ++i)
loadSoundFromWav(static_cast<SoundType>(i), m_soundPaths[i]);
// growPlayBuf() is called inside loadSoundFromWav after each load,
// so m_playBuf is always sized to the current high-water mark.
}
// ── unloadAllSounds ───────────────────────────────────────────────────────
void Audio::unloadAllSounds(const std::initializer_list<SoundType>& excludeSounds) {
std::lock_guard<std::mutex> lock(m_audioMutex);
if (!m_initialized) return;
for (uint32_t i = 0; i < m_cachedSounds.size(); ++i) {
const SoundType cur = static_cast<SoundType>(i);
if (std::find(excludeSounds.begin(), excludeSounds.end(), cur) != excludeSounds.end())
continue;
CachedSound& s = m_cachedSounds[i];
free(s.rawBuf);
s = CachedSound{};
}
// m_playBuf stays allocated — it will be reused when any remaining sound plays.
}
// ── reloadIfDockedChanged ─────────────────────────────────────────────────
// Volume and dock state are read live in renderToPlayBuf(), so no stale
// marking is needed here — just update m_lastDockedState.
bool Audio::reloadIfDockedChanged() {
if (!m_initialized) return false;
const bool currentDocked = ult::consoleIsDocked();
if (currentDocked == m_lastDockedState) return false;
std::lock_guard<std::mutex> lock(m_audioMutex);
m_lastDockedState = currentDocked;
return true;
}
// ── growPlayBuf ───────────────────────────────────────────────────────────
// Computes the 48 kHz stereo output size needed for every currently loaded
// sound and grows m_playBuf to cover the largest one.
// Called after each loadSoundFromWav. Must hold m_audioMutex.
void Audio::growPlayBuf() {
uint32_t maxNeeded = 0;
for (const auto& s : m_cachedSounds) {
if (!s.rawBuf || s.rawSize == 0) continue;
const uint32_t srcPerChan = s.isMono
? (s.rawSize / sizeof(s16))
: (s.rawSize / sizeof(s16)) / 2;
const uint32_t outPerChan = (s.sampleRate == TARGET_RATE || s.sampleRate == 0)
? srcPerChan
: static_cast<uint32_t>(
((uint64_t)srcPerChan * TARGET_RATE + s.sampleRate - 1) / s.sampleRate);
const uint32_t stereoBytes = outPerChan * 2 * sizeof(s16);
const uint32_t needed = (stereoBytes + AUDIO_ALIGN - 1) & ~(AUDIO_ALIGN - 1);
if (needed > maxNeeded) maxNeeded = needed;
}
if (maxNeeded <= m_playBufCap) return; // already large enough
free(m_playBuf);
m_playBuf = aligned_alloc(AUDIO_ALIGN, maxNeeded);
if (m_playBuf) {
m_playBufCap = maxNeeded;
} else {
m_playBufCap = 0;
}
}
// ── renderToPlayBuf ───────────────────────────────────────────────────────
// Writes rawBuf → m_playBuf: resample to 48 kHz (linear interp if needed),
// expand mono → L+R, apply current volume and dock attenuation.
// Returns actual output byte count written, or 0 on error.
// Must be called under m_audioMutex.
uint32_t Audio::renderToPlayBuf(const CachedSound& s) {
if (!s.rawBuf || s.rawSize == 0 || !m_playBuf) return 0;
const uint32_t srcSamples = s.rawSize / sizeof(s16);
const uint32_t srcPerChan = s.isMono ? srcSamples : srcSamples / 2;
const uint32_t outPerChan = (s.sampleRate == TARGET_RATE || s.sampleRate == 0)
? srcPerChan
: static_cast<uint32_t>(
((uint64_t)srcPerChan * TARGET_RATE + s.sampleRate - 1) / s.sampleRate);
const uint32_t stereoBytes = outPerChan * 2 * sizeof(s16);
const uint32_t needed = (stereoBytes + AUDIO_ALIGN - 1) & ~(AUDIO_ALIGN - 1);
if (needed > m_playBufCap) return 0; // shouldn't happen after growPlayBuf()
// Effective volume: master * 0.5 when docked (TV speaker protection).
// Fixed-point: 0256 where 256 == 1.0.
int32_t vol = m_masterVolumeFixed.load(std::memory_order_relaxed);
if (m_lastDockedState) vol >>= 1;
const s16* src = static_cast<const s16*>(s.rawBuf);
s16* dst = static_cast<s16*>(m_playBuf);
const bool needsResample = (s.sampleRate != TARGET_RATE && s.sampleRate != 0);
if (!needsResample) {
// ── Fast path: native 48 kHz — no resampling ─────────────────────
if (s.isMono) {
for (uint32_t i = 0; i < srcSamples; ++i) {
const s16 v = static_cast<s16>((static_cast<int32_t>(src[i]) * vol) >> 8);
*dst++ = v; // L
*dst++ = v; // R
}
} else {
for (uint32_t i = 0; i < srcSamples; ++i)
*dst++ = static_cast<s16>((static_cast<int32_t>(src[i]) * vol) >> 8);
}
} else {
// ── Resample path: linear interpolation to 48 kHz ────────────────
// step is source frames per output frame in 16.16 fixed-point.
// For all rates ≤ 48 kHz this is < 1.0 (upsampling).
const uint64_t step = ((uint64_t)s.sampleRate << 16) / TARGET_RATE;
uint64_t srcFixed = 0;
if (s.isMono) {
for (uint32_t i = 0; i < outPerChan; ++i) {
const uint32_t i0 = static_cast<uint32_t>(srcFixed >> 16);
const uint32_t i1 = (i0 + 1 < srcPerChan) ? i0 + 1 : i0;
const int32_t frac = static_cast<int32_t>(srcFixed & 0xFFFF);
const int32_t s0 = src[i0], s1 = src[i1];
const s16 v = static_cast<s16>(
((s0 + (((s1 - s0) * frac) >> 16)) * vol) >> 8);
*dst++ = v; // L
*dst++ = v; // R
srcFixed += step;
}
} else {
// Stereo interleaved: index [frame*2+0] = L, [frame*2+1] = R
for (uint32_t i = 0; i < outPerChan; ++i) {
const uint32_t i0 = static_cast<uint32_t>(srcFixed >> 16);
const uint32_t i1 = (i0 + 1 < srcPerChan) ? i0 + 1 : i0;
const int32_t frac = static_cast<int32_t>(srcFixed & 0xFFFF);
const int32_t l0 = src[i0*2], l1 = src[i1*2];
const int32_t r0 = src[i0*2 + 1], r1 = src[i1*2 + 1];
*dst++ = static_cast<s16>(((l0 + (((l1-l0)*frac)>>16)) * vol) >> 8);
*dst++ = static_cast<s16>(((r0 + (((r1-r0)*frac)>>16)) * vol) >> 8);
srcFixed += step;
}
}
}
// Zero-fill alignment padding
if (stereoBytes < needed)
memset(static_cast<u8*>(m_playBuf) + stereoBytes, 0, needed - stereoBytes);
return stereoBytes;
}
// ── loadSoundFromWav ──────────────────────────────────────────────────────
// Reads WAV → rawBuf (16-bit, native channels, no volume applied).
// Rejects source rates > 48 kHz. Calls growPlayBuf() after a successful load.
// Must be called under m_audioMutex.
bool Audio::loadSoundFromWav(SoundType type, const char* path) {
const uint32_t idx = static_cast<uint32_t>(type);
if (!m_initialized || idx >= static_cast<uint32_t>(SoundType::Count)) return false;
CachedSound& s = m_cachedSounds[idx];
free(s.rawBuf);
s = CachedSound{}; // reset all fields
FILE* f = fopen(path, "rb");
if (!f) return false;
// ── RIFF/WAVE header ──────────────────────────────────────────────────
char hdr[12];
if (fread(hdr, 1, 12, f) != 12 ||
memcmp(hdr, "RIFF", 4) ||
memcmp(hdr + 8, "WAVE", 4)) {
fclose(f); return false;
}
u16 fmt = 0, ch = 0, bits = 0;
u32 rate = 0, dSize = 0;
long dPos = 0;
// ── Chunk scan ────────────────────────────────────────────────────────
while (fread(hdr, 1, 8, f) == 8) {
const u32 sz = *reinterpret_cast<const u32*>(hdr + 4);
if (!memcmp(hdr, "fmt ", 4)) {
fread(&fmt, 2, 1, f);
fread(&ch, 2, 1, f);
fread(&rate, 4, 1, f);
fseek(f, 6, SEEK_CUR); // skip byte rate + block align
fread(&bits, 2, 1, f);
fseek(f, (long)sz - 16, SEEK_CUR);
} else if (!memcmp(hdr, "data", 4)) {
dSize = sz;
dPos = ftell(f);
break;
} else {
fseek(f, sz, SEEK_CUR);
}
}
// ── Validate ──────────────────────────────────────────────────────────
// Reject rates above 48 kHz — downsampling would expand rawBuf beyond its
// target-rate output and defeat the purpose of small source files.
if (!dSize || fmt != 1 || ch == 0 || ch > 2 ||
(bits != 8 && bits != 16) || rate == 0 || rate > TARGET_RATE) {
fclose(f); return false;
}
const uint32_t inSamples = dSize / (bits / 8);
const uint32_t rawBytes = inSamples * sizeof(s16); // normalised to 16-bit
const uint32_t rawCap = (rawBytes + AUDIO_ALIGN - 1) & ~(AUDIO_ALIGN - 1);
void* buf = aligned_alloc(AUDIO_ALIGN, rawCap);
if (!buf) { fclose(f); return false; }
fseek(f, dPos, SEEK_SET);
s16* out = static_cast<s16*>(buf);
uint32_t remaining = inSamples;
uint32_t outIdx = 0;
// ── Chunked read + bit-depth normalisation ────────────────────────────
constexpr uint32_t CHUNK = 512;
if (bits == 8) {
u8 chunk[CHUNK];
while (remaining > 0) {
const uint32_t toRead = std::min(remaining, CHUNK);
if (fread(chunk, 1, toRead, f) != toRead) {
free(buf); fclose(f); return false;
}
for (uint32_t i = 0; i < toRead; ++i)
out[outIdx++] = static_cast<s16>((static_cast<int32_t>(chunk[i]) - 128) << 8);
remaining -= toRead;
}
} else {
s16 chunk[CHUNK];
while (remaining > 0) {
const uint32_t toRead = std::min(remaining, CHUNK);
if (fread(chunk, sizeof(s16), toRead, f) != toRead) {
free(buf); fclose(f); return false;
}
memcpy(out + outIdx, chunk, toRead * sizeof(s16));
outIdx += toRead;
remaining -= toRead;
}
}
fclose(f);
if (rawBytes < rawCap)
memset(static_cast<u8*>(buf) + rawBytes, 0, rawCap - rawBytes);
s.rawBuf = buf;
s.rawSize = rawBytes;
s.rawCap = rawCap;
s.sampleRate = rate;
s.isMono = (ch == 1);
// Grow shared play buffer if this sound's 48 kHz output would exceed it.
growPlayBuf();
return (m_playBuf != nullptr);
}
// ── playSound ─────────────────────────────────────────────────────────────
// Drains the audout queue, renders the sound into the shared play buffer,
// then submits. Volume and dock attenuation are applied live inside render.
void Audio::playSound(SoundType type) {
if (!m_enabled.load(std::memory_order_relaxed)) return;
const uint32_t idx = static_cast<uint32_t>(type);
if (idx >= static_cast<uint32_t>(SoundType::Count)) return;
std::lock_guard<std::mutex> lock(m_audioMutex);
if (!m_initialized || !m_playBuf) return;
const CachedSound& s = m_cachedSounds[idx];
if (!s.rawBuf) return; // sound file not loaded
// Drain finished buffers so audout's queue stays healthy and so we know
// the shared buffer is no longer in use by a previous submission.
AudioOutBuffer* released = nullptr;
u32 releasedCount = 0;
audoutGetReleasedAudioOutBuffer(&released, &releasedCount);
const uint32_t outBytes = renderToPlayBuf(s);
if (outBytes == 0) return;
const uint32_t bufCap = (outBytes + AUDIO_ALIGN - 1) & ~(AUDIO_ALIGN - 1);
m_audoutBuf = {};
m_audoutBuf.buffer = m_playBuf;
m_audoutBuf.buffer_size = bufCap;
m_audoutBuf.data_size = outBytes;
m_audoutBuf.data_offset = 0;
m_audoutBuf.next = nullptr;
AudioOutBuffer* rel = nullptr;
audoutPlayBuffer(&m_audoutBuf, &rel);
}
// ── Volume / enable accessors ─────────────────────────────────────────────
// Volume is read live in renderToPlayBuf() — no stale marking needed.
void Audio::setMasterVolume(float v) {
const int32_t fixed = static_cast<int32_t>(std::clamp(v, 0.0f, 1.0f) * 256.0f);
m_masterVolumeFixed.store(fixed, std::memory_order_relaxed);
}
void Audio::setEnabled(bool e) {
m_enabled.store(e, std::memory_order_relaxed);
}
bool Audio::isEnabled() {
return m_enabled.load(std::memory_order_relaxed);
}
} // namespace ult

View File

@@ -0,0 +1,52 @@
/********************************************************************************
* File: debug_funcs.cpp
* Author: ppkantorski
* Description:
* This source file contains the implementation of debugging functions for the
* Ultrahand Overlay project.
*
* For the latest updates and contributions, visit the project's GitHub repository.
* (GitHub Repository: https://github.com/ppkantorski/Ultrahand-Overlay)
*
* Note: Please be aware that this notice cannot be altered or removed. It is a part
* of the project's documentation and must remain intact.
*
* Licensed under both GPLv2 and CC-BY-4.0
* Copyright (c) 2023-2026 ppkantorski
********************************************************************************/
#include "debug_funcs.hpp"
namespace ult {
#if USING_LOGGING_DIRECTIVE
// Define static variables
const std::string defaultLogFilePath = "sdmc:/switch/.packages/log.txt";
std::string logFilePath = defaultLogFilePath;
bool disableLogging = true;
std::mutex logMutex;
void logMessage(const char* message) {
std::time_t currentTime = std::time(nullptr);
std::tm* timeInfo = std::localtime(&currentTime);
char timestamp[30];
strftime(timestamp, sizeof(timestamp), "[%Y-%m-%d %H:%M:%S] ", timeInfo);
{
std::lock_guard<std::mutex> lock(logMutex);
FILE* file = fopen(logFilePath.c_str(), "a");
if (file != nullptr) {
fputs(timestamp, file);
fputs(message, file);
fputc('\n', file);
fclose(file);
}
}
}
// Overload for std::string
void logMessage(const std::string& message) {
logMessage(message.c_str());
}
#endif
}

View File

@@ -0,0 +1,754 @@
/********************************************************************************
* File: download_funcs.cpp
* Author: ppkantorski
* Description:
* This source file provides implementations for the functions declared in
* download_funcs.hpp. These functions utilize libcurl for downloading files
* from the internet and minizip-ng for extracting ZIP archives with proper
* 64-bit file support.
*
* For the latest updates and contributions, visit the project's GitHub repository.
* (GitHub Repository: https://github.com/ppkantorski/Ultrahand-Overlay)
*
* Note: Please be aware that this notice cannot be altered or removed. It is a part
* of the project's documentation and must remain intact.
*
* Licensed under both GPLv2 and CC-BY-4.0
* Copyright (c) 2023-2026 ppkantorski
********************************************************************************/
#include "download_funcs.hpp"
namespace ult {
// Base loader definitions
size_t DOWNLOAD_READ_BUFFER = 32*1024;//64 * 1024;//4096*10;
size_t DOWNLOAD_WRITE_BUFFER = 16*1024;//64 * 1024;
size_t UNZIP_READ_BUFFER = 32*1024;//131072*2;//4096*4;
size_t UNZIP_WRITE_BUFFER = 16*1024;//131072*2;//4096*4;
// User agent string for curl requests
static constexpr const char* userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36";
// Shared atomic flag to indicate whether to abort the download operation
std::atomic<bool> abortDownload(false);
// Define an atomic bool for interpreter completion
std::atomic<bool> abortUnzip(false);
std::atomic<int> downloadPercentage(-1);
std::atomic<int> unzipPercentage(-1);
// Thread-safe curl initialization
static std::mutex curlInitMutex;
static std::atomic<bool> curlInitialized(false);
// Definition of CurlDeleter
struct CurlDeleter {
void operator()(CURL* curl) const noexcept {
if (curl) {
curl_easy_cleanup(curl);
}
}
};
struct FileDeleter {
void operator()(FILE* f) const {
if (f) {
fclose(f);
}
}
};
// Using stdio.h functions (FILE*, fwrite)
size_t writeCallback(void* ptr, size_t size, size_t nmemb, FILE* stream) {
if (!ptr || !stream) return 0;
return fwrite(ptr, 1, size * nmemb, stream);
}
// Your C function
int progressCallback(void *ptr, curl_off_t totalToDownload, curl_off_t nowDownloaded, curl_off_t totalToUpload, curl_off_t nowUploaded) {
if (!ptr) return 1;
auto percentage = static_cast<std::atomic<int>*>(ptr);
if (totalToDownload > 0) {
percentage->store(static_cast<int>((static_cast<double>(nowDownloaded) / static_cast<double>(totalToDownload)) * 100.0), std::memory_order_release);
}
if (abortDownload.load(std::memory_order_acquire)) {
percentage->store(-1, std::memory_order_release);
return 1; // Abort the download
}
return 0; // Continue the download
}
// Quick connectivity pre-check before spinning up curl
static bool hasInternetAccess() {
if (R_FAILED(nifmInitialize(NifmServiceType_User)))
return false;
NifmInternetConnectionType type;
u32 strength;
NifmInternetConnectionStatus status;
u32 current_addr, subnet_mask, gateway, primary_dns, secondary_dns;
bool connected = R_SUCCEEDED(nifmGetInternetConnectionStatus(&type, &strength, &status))
&& status == NifmInternetConnectionStatus_Connected
&& R_SUCCEEDED(nifmGetCurrentIpConfigInfo(&current_addr, &subnet_mask, &gateway, &primary_dns, &secondary_dns))
&& current_addr != 0
&& primary_dns != 0;
nifmExit();
return connected;
}
/**
* @brief Downloads a file from a URL to a specified destination.
*
* @param url The URL of the file to download.
* @param toDestination The destination path where the file should be saved.
* @return True if the download was successful, false otherwise.
*/
bool downloadFile(const std::string& url, const std::string& toDestination, bool noSocketInit, bool noPercentagePolling) {
abortDownload.store(false, std::memory_order_release);
if (url.find_first_of("{}") != std::string::npos) {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Invalid URL: " + url);
#endif
return false;
}
// Fast pre-flight: ~1.5s max vs curl's unpredictable DNS hang
if (!hasInternetAccess()) {
return false;
}
std::string destination = toDestination;
if (destination.back() == '/') {
createDirectory(destination);
const size_t lastSlash = url.find_last_of('/');
if (lastSlash != std::string::npos) {
destination += url.substr(lastSlash + 1);
} else {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Invalid URL: " + url);
#endif
return false;
}
} else {
createDirectory(destination.substr(0, destination.find_last_of('/')));
}
const std::string tempFilePath = getParentDirFromPath(destination) + "." + getFileName(destination) + ".tmp";
// Alternative method of opening file (depending on your platform, like using POSIX open())
std::unique_ptr<FILE, FileDeleter> file(fopen(tempFilePath.c_str(), "wb"));
if (!file) {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Error opening file: " + tempFilePath);
#endif
return false;
}
// ADD THIS: Set up write buffer for better performance
std::unique_ptr<char[]> writeBuffer;
if (DOWNLOAD_WRITE_BUFFER > 0) {
writeBuffer = std::make_unique<char[]>(DOWNLOAD_WRITE_BUFFER);
// _IOFBF = full buffering, _IOLBF = line buffering, _IONBF = no buffering
setvbuf(file.get(), writeBuffer.get(), _IOFBF, DOWNLOAD_WRITE_BUFFER);
}
static constexpr SocketInitConfig socketInitConfig = {
// TCP buffers
.tcp_tx_buf_size = 16 * 1024, // 16 KB default
.tcp_rx_buf_size = 16 * 1024*2, // 16 KB default
.tcp_tx_buf_max_size = 64 * 1024, // 64 KB default max
.tcp_rx_buf_max_size = 64 * 1024*2, // 64 KB default max
// UDP buffers
.udp_tx_buf_size = 512, // 512 B default
.udp_rx_buf_size = 512, // 512 B default
// Socket buffer efficiency
.sb_efficiency = 1, // 0 = default, balanced memory vs CPU
// 1 = prioritize memory efficiency (smaller internal allocations)
.bsd_service_type = BsdServiceType_Auto // Auto-select service
};
if (!noSocketInit) {
if (!R_SUCCEEDED(socketInitialize(&socketInitConfig))) {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Failed to initialize socket.");
#endif
return false;
}
}
std::unique_ptr<CURL, CurlDeleter> curl(curl_easy_init());
if (!curl) {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Error initializing curl.");
#endif
file.reset();
writeBuffer.reset();
return false;
}
// Only initialize downloadPercentage if we're tracking progress
if (!noPercentagePolling) {
downloadPercentage.store(0, std::memory_order_release);
}
curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str());
curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, writeCallback);
curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, file.get());
// Conditionally set up progress callback based on noPercentagePolling
if (noPercentagePolling) {
// Disable progress function entirely
curl_easy_setopt(curl.get(), CURLOPT_NOPROGRESS, 1L);
} else {
// Enable progress callback for percentage updates
curl_easy_setopt(curl.get(), CURLOPT_NOPROGRESS, 0L);
curl_easy_setopt(curl.get(), CURLOPT_XFERINFOFUNCTION, progressCallback);
curl_easy_setopt(curl.get(), CURLOPT_XFERINFODATA, &downloadPercentage);
}
curl_easy_setopt(curl.get(), CURLOPT_USERAGENT, userAgent);
curl_easy_setopt(curl.get(), CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); // Enable HTTP/2
curl_easy_setopt(curl.get(), CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
curl_easy_setopt(curl.get(), CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); // Force TLS 1.2
curl_easy_setopt(curl.get(), CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl.get(), CURLOPT_BUFFERSIZE, DOWNLOAD_READ_BUFFER); // Increase buffer size
// Add timeout options
curl_easy_setopt(curl.get(), CURLOPT_CONNECTTIMEOUT, 4L); // 10 seconds to connect
curl_easy_setopt(curl.get(), CURLOPT_LOW_SPEED_LIMIT, 1L); // 1 byte/s (virtually any progress)
curl_easy_setopt(curl.get(), CURLOPT_LOW_SPEED_TIME, 60L); // 1 minutes of no progress
//curl_easy_setopt(curl.get(), CURLOPT_DNS_USE_GLOBAL_CACHE, 0L);
//curl_easy_setopt(curl.get(), CURLOPT_FORBID_REUSE, 1L);
//curl_easy_setopt(curl.get(), CURLOPT_CLOSESOCKETDATA, NULL); // ensure no dangling sockets
//curl_easy_setopt(curl.get(), CURLOPT_TCP_NODELAY, 1L);
CURLcode result = curl_easy_perform(curl.get());
// Check HTTP response code BEFORE closing file/curl
long http_code = 0;
curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &http_code);
file.reset();
writeBuffer.reset();
curl.reset();
if (!noSocketInit) {
socketExit();
}
// Check for HTTP errors (404, 500, etc.)
if (result == CURLE_OK && (http_code < 200 || http_code >= 300)) {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("HTTP error " + std::to_string(http_code) + " downloading: " + url);
#endif
deleteFileOrDirectory(tempFilePath);
if (!noPercentagePolling) {
downloadPercentage.store(-1, std::memory_order_release);
}
return false;
}
if (result != CURLE_OK) {
#if USING_LOGGING_DIRECTIVE
if (result == CURLE_ABORTED_BY_CALLBACK) {
if (!disableLogging)
logMessage("Download aborted by user: " + url);
} else if (result == CURLE_OPERATION_TIMEDOUT) {
if (!disableLogging)
logMessage("Download timed out: " + url);
} else if (result == CURLE_COULDNT_CONNECT) {
if (!disableLogging)
logMessage("Could not connect to: " + url);
} else {
if (!disableLogging)
logMessage("Error downloading file: " + std::string(curl_easy_strerror(result)));
}
#endif
deleteFileOrDirectory(tempFilePath);
if (!noPercentagePolling) {
downloadPercentage.store(-1, std::memory_order_release);
}
return false;
}
// Alternative method for checking if the file is empty (POSIX example)
struct stat fileStat;
if (stat(tempFilePath.c_str(), &fileStat) != 0 || fileStat.st_size == 0) {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Error downloading file: Empty file");
#endif
deleteFileOrDirectory(tempFilePath);
// Only update percentage if we're tracking it
if (!noPercentagePolling) {
downloadPercentage.store(-1, std::memory_order_release);
}
return false;
}
// Only update percentage if we're tracking it
if (!noPercentagePolling) {
downloadPercentage.store(100, std::memory_order_release);
}
// CHECK FOR PROTECTED FILES AND ADD .ultra EXTENSION IF NEEDED
if (PROTECTED_FILES.find(destination) != PROTECTED_FILES.end()) {
destination += ".ultra";
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Protected file detected, renaming download to: " + destination);
#endif
}
moveFile(tempFilePath, destination);
return true;
}
/**
* @brief Custom I/O function for opening files with larger buffer
*/
static voidpf ZCALLBACK fopen64_file_func_custom(voidpf opaque, const void* filename, int mode) {
FILE* file = nullptr;
const char* mode_fopen = nullptr;
if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER) == ZLIB_FILEFUNC_MODE_READ)
mode_fopen = "rb";
else if (mode & ZLIB_FILEFUNC_MODE_EXISTING)
mode_fopen = "r+b";
else if (mode & ZLIB_FILEFUNC_MODE_CREATE)
mode_fopen = "wb";
if ((filename != nullptr) && (mode_fopen != nullptr)) {
file = fopen((const char*)filename, mode_fopen);
if (file && ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER) == ZLIB_FILEFUNC_MODE_READ)) {
// Set 64KB buffer for reading the ZIP file - reduces syscalls
setvbuf(file, nullptr, _IOFBF, UNZIP_READ_BUFFER);
}
}
return file;
}
static int ZCALLBACK fclose64_file_func_custom(voidpf opaque, voidpf stream) {
int ret = EOF;
if (stream != nullptr) {
ret = fclose((FILE*)stream);
}
return ret;
}
/**
* @brief Extracts files from a ZIP archive to a specified destination.
*
* Ultra-optimized single-pass extraction with smooth byte-based progress reporting
* using miniz with proper 64-bit file support and streaming extraction.
* Fixed memory leaks with RAII for proper resource cleanup on abort.
*
* @param zipFilePath The path to the ZIP archive file.
* @param toDestination The destination directory where files should be extracted.
* @return True if the extraction was successful, false otherwise.
*/
bool unzipFile(const std::string& zipFilePath, const std::string& toDestination) {
abortUnzip.store(false, std::memory_order_release);
unzipPercentage.store(0, std::memory_order_release);
// Time-based abort checking - pre-calculated constants
bool success = true;
// RAII wrapper for unzFile
struct UnzFileManager {
unzFile file = nullptr;
UnzFileManager(const std::string& path) {
zlib_filefunc64_def ffunc;
fill_fopen64_filefunc(&ffunc);
ffunc.zopen64_file = fopen64_file_func_custom;
ffunc.zclose_file = fclose64_file_func_custom;
file = unzOpen2_64(path.c_str(), &ffunc);
}
~UnzFileManager() {
if (file) {
unzClose(file);
file = nullptr;
}
}
bool is_valid() const { return file != nullptr; }
operator unzFile() const { return file; }
};
// RAII wrapper for output file
struct OutputFileManager {
FILE* file = nullptr;
std::unique_ptr<char[]> buffer;
size_t bufferSize;
OutputFileManager(size_t bufSize) : bufferSize(bufSize) {
buffer = std::make_unique<char[]>(bufferSize);
}
bool open(const std::string& path) {
close();
file = fopen(path.c_str(), "wb");
if (file) {
setvbuf(file, buffer.get(), _IOFBF, bufferSize);
}
return file != nullptr;
}
void close() {
if (file) {
fclose(file);
file = nullptr;
}
}
bool is_open() const { return file != nullptr; }
size_t write(const void* data, size_t size) {
return file ? fwrite(data, 1, size, file) : 0;
}
~OutputFileManager() { close(); }
};
UnzFileManager zipFile(zipFilePath);
if (!zipFile.is_valid()) {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Failed to open zip file: " + zipFilePath);
#endif
return false;
}
// Get global info about the ZIP file
unz_global_info64 globalInfo;
if (unzGetGlobalInfo64(zipFile, &globalInfo) != UNZ_OK) {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Failed to get zip file info");
#endif
return false;
}
const uLong numFiles = globalInfo.number_entry;
if (numFiles == 0) {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("No files found in archive");
#endif
return false;
}
// ALWAYS calculate total size for accurate byte-based progress
ZPOS64_T totalUncompressedSize = 0;
char tempFilenameBuffer[512];
unz_file_info64 fileInfo;
// First pass: calculate total uncompressed size
int result = unzGoToFirstFile(zipFile);
while (result == UNZ_OK) {
if (abortUnzip.load(std::memory_order_relaxed)) {
unzipPercentage.store(-1, std::memory_order_release);
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Extraction aborted during size calculation");
#endif
abortUnzip.store(false, std::memory_order_release);
return false;
}
if (unzGetCurrentFileInfo64(zipFile, &fileInfo, tempFilenameBuffer, sizeof(tempFilenameBuffer),
nullptr, 0, nullptr, 0) == UNZ_OK) {
const size_t nameLen = strlen(tempFilenameBuffer);
if (nameLen > 0 && tempFilenameBuffer[nameLen - 1] != '/') {
totalUncompressedSize += std::max(fileInfo.uncompressed_size, static_cast<ZPOS64_T>(1));
}
}
result = unzGoToNextFile(zipFile);
}
// Fallback to 1 if no actual data (avoid division by zero)
if (totalUncompressedSize == 0) {
totalUncompressedSize = 1;
}
#if USING_LOGGING_DIRECTIVE
if (!disableLogging) {
logMessage("Processing " + std::to_string(numFiles) + " files, " +
std::to_string(totalUncompressedSize) + " total bytes from archive");
}
#endif
// Pre-allocate ALL reusable strings and variables outside the main loop
std::string fileName, extractedFilePath, directoryPath;
// Single large buffer for extraction - reused for all files
const size_t bufferSize = UNZIP_WRITE_BUFFER;
std::unique_ptr<char[]> writeBuffer = std::make_unique<char[]>(bufferSize);
char filenameBuffer[512]; // Stack allocated for filename reading
// Progress tracking variables - OPTIMIZED for smooth byte-based tracking
ZPOS64_T totalBytesProcessed = 0;
uLong filesProcessed = 0;
int currentProgress = 0; // Current percentage (0-100)
// Create output file manager
OutputFileManager outputFile(bufferSize);
// Loop variables moved outside
bool extractSuccess;
ZPOS64_T fileBytesProcessed;
int bytesRead;
// String operation variables
const char* filename;
size_t nameLen;
size_t lastSlashPos;
size_t invalid_pos;
size_t start_pos;
// Ensure destination directory exists
createDirectory(toDestination);
// Ensure destination ends with '/' - pre-allocate final string
std::string destination;
destination = toDestination;
if (!destination.empty() && destination.back() != '/') {
destination += '/';
}
int newProgress;;
// Extract files
result = unzGoToFirstFile(zipFile);
while (result == UNZ_OK && success) {
if (abortUnzip.load(std::memory_order_relaxed)) {
success = false;
break; // RAII will handle cleanup
}
// Get current file info - reuse fileInfo variable
if (unzGetCurrentFileInfo64(zipFile, &fileInfo, filenameBuffer, sizeof(filenameBuffer),
nullptr, 0, nullptr, 0) != UNZ_OK) {
result = unzGoToNextFile(zipFile);
continue;
}
filename = filenameBuffer;
// Quick filename validation
if (!filename || filename[0] == '\0') {
result = unzGoToNextFile(zipFile);
continue;
}
nameLen = strlen(filename);
if (nameLen > 0 && filename[nameLen - 1] == '/') { // Skip directories
result = unzGoToNextFile(zipFile);
continue;
}
// Build extraction path - reuse allocated strings
fileName.assign(filename, nameLen);
extractedFilePath = destination;
extractedFilePath += fileName;
// Optimized character cleaning - only if needed
invalid_pos = extractedFilePath.find_first_of(":*?\"<>|");
if (invalid_pos != std::string::npos) {
start_pos = std::min(extractedFilePath.find(ROOT_PATH) + 5, extractedFilePath.size());
auto it = extractedFilePath.begin() + start_pos;
extractedFilePath.erase(std::remove_if(it, extractedFilePath.end(), [](char c) {
return c == ':' || c == '*' || c == '?' || c == '\"' || c == '<' || c == '>' || c == '|';
}), extractedFilePath.end());
}
// CHECK FOR PROTECTED FILES AND ADD .ultra EXTENSION IF NEEDED
if (PROTECTED_FILES.find(extractedFilePath) != PROTECTED_FILES.end()) {
extractedFilePath += ".ultra";
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Protected file detected, renaming to: " + extractedFilePath);
#endif
}
// Open the current file in the ZIP
if (unzOpenCurrentFile(zipFile) != UNZ_OK) {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Could not open file in ZIP: " + fileName);
#endif
result = unzGoToNextFile(zipFile);
continue;
}
// Create directory if needed
lastSlashPos = extractedFilePath.find_last_of('/');
if (lastSlashPos != std::string::npos) {
directoryPath.assign(extractedFilePath, 0, lastSlashPos + 1);
createDirectory(directoryPath);
}
// Open output file
if (!outputFile.open(extractedFilePath)) {
unzCloseCurrentFile(zipFile);
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Error creating file: " + extractedFilePath);
#endif
result = unzGoToNextFile(zipFile);
continue;
}
// Extract file data in chunks
extractSuccess = true;
fileBytesProcessed = 0;
while ((bytesRead = unzReadCurrentFile(zipFile, writeBuffer.get(), bufferSize)) > 0) {
if (abortUnzip.load(std::memory_order_relaxed)) {
extractSuccess = false;
break; // RAII will handle cleanup
}
// Write data to file
if (outputFile.write(writeBuffer.get(), bytesRead) != static_cast<size_t>(bytesRead)) {
extractSuccess = false;
break;
}
// Update progress tracking
fileBytesProcessed += bytesRead;
totalBytesProcessed += bytesRead;
// FIXED: Allow progress to reach 100% naturally during processing
if (totalUncompressedSize > 0) {
newProgress = static_cast<int>((totalBytesProcessed * 100) / totalUncompressedSize);
if (newProgress > currentProgress && newProgress <= 100) {
currentProgress = newProgress;
unzipPercentage.store(currentProgress, std::memory_order_release);
#if USING_LOGGING_DIRECTIVE
// Only log at 10% intervals to avoid spam
if (currentProgress % 10 == 0) {
if (!disableLogging) {
logMessage("Progress: " + std::to_string(currentProgress) + "% (" +
std::to_string(totalBytesProcessed) + "/" +
std::to_string(totalUncompressedSize) + " bytes)");
}
}
#endif
}
}
}
// CRITICAL FIX: Handle 0-byte files that don't enter the while loop
if (bytesRead == 0 && fileBytesProcessed == 0 && extractSuccess) {
// This is a 0-byte file - update progress by 1 byte equivalent
totalBytesProcessed += 1;
// Update progress for 0-byte files
if (totalUncompressedSize > 0) {
newProgress = static_cast<int>((totalBytesProcessed * 100) / totalUncompressedSize);
if (newProgress > currentProgress && newProgress <= 100) {
currentProgress = newProgress;
unzipPercentage.store(currentProgress, std::memory_order_release);
#if USING_LOGGING_DIRECTIVE
if (currentProgress % 10 == 0) {
if (!disableLogging)
logMessage("Progress: " + std::to_string(currentProgress) + "% (0-byte file processed)");
}
#endif
}
}
}
// Check for read errors
if (bytesRead < 0) {
extractSuccess = false;
}
// Close current file handles
outputFile.close();
unzCloseCurrentFile(zipFile);
if (!extractSuccess) {
deleteFileOrDirectory(extractedFilePath);
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Failed to extract: " + fileName);
#endif
if (abortUnzip.load(std::memory_order_relaxed)) {
success = false;
break;
}
} else {
filesProcessed++;
}
// Move to next file
result = unzGoToNextFile(zipFile);
}
writeBuffer.reset();
// Check final abort state
if (abortUnzip.load(std::memory_order_relaxed)) {
unzipPercentage.store(-1, std::memory_order_release);
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Extraction aborted by user");
#endif
abortUnzip.store(false, std::memory_order_release);
return false;
}
if (success && filesProcessed > 0) {
abortUnzip.store(false, std::memory_order_release);
unzipPercentage.store(100, std::memory_order_release);
#if USING_LOGGING_DIRECTIVE
if (!disableLogging) {
logMessage("Extraction completed: " + std::to_string(filesProcessed) + " files, " +
std::to_string(totalBytesProcessed) + " bytes");
}
#endif
return true;
} else {
abortUnzip.store(false, std::memory_order_release);
unzipPercentage.store(-1, std::memory_order_release);
return false;
}
}
}

View File

@@ -0,0 +1,424 @@
/********************************************************************************
* File: get_funcs.cpp
* Author: ppkantorski
* Description:
* This source file provides the implementations of functions declared in
* get_funcs.hpp. These functions are responsible for retrieving and handling
* data from the file system and JSON files, including parsing overlay module
* information, reading file contents, and accessing structured data used
* in the Ultrahand Overlay project.
*
* For the latest updates and contributions, visit the project's GitHub repository.
* (GitHub Repository: https://github.com/ppkantorski/Ultrahand-Overlay)
*
* Note: Please be aware that this notice cannot be altered or removed. It is a part
* of the project's documentation and must remain intact.
*
* Licensed under both GPLv2 and CC-BY-4.0
* Copyright (c) 2023-2026 ppkantorski
********************************************************************************/
#include "get_funcs.hpp"
namespace ult {
/**
* @brief Concatenates the provided directory and file names to form a destination path.
*
* @param destinationDir The directory where the file should be placed.
* @param fileName The name of the file.
* @return The destination path as a string.
*/
std::string getDestinationPath(const std::string& destinationDir,
const std::string& fileName)
{
// e.g. "foo/bar" + "/" + "baz.txt" → "foo/bar/baz.txt", but if destinationDir ended in '/',
// youd get "foo/bar//baz.txt" → collapse again:
std::string combined = destinationDir + "/" + fileName;
preprocessPath(combined);
return combined;
}
/**
* @brief Extracts the value part from a string line containing a key-value pair.
*
* @param line The string line containing a key-value pair (e.g., "key=value").
* @return The extracted value as a string. If no value is found, an empty string is returned.
*/
std::string getValueFromLine(const std::string& line) {
const size_t equalsPos = line.rfind('=');
if (equalsPos == std::string::npos || equalsPos + 1 >= line.size()) {
return "";
}
// OPTIMIZATION: Find trim boundaries directly - no temporary string
size_t start = equalsPos + 1;
size_t end = line.size() - 1;
// Skip leading whitespace
while (start <= end && (line[start] == ' ' || line[start] == '\t' ||
line[start] == '\n' || line[start] == '\r' ||
line[start] == '\f' || line[start] == '\v')) {
++start;
}
// Skip trailing whitespace
while (end >= start && (line[end] == ' ' || line[end] == '\t' ||
line[end] == '\n' || line[end] == '\r' ||
line[end] == '\f' || line[end] == '\v')) {
--end;
}
// Return trimmed substring directly
return (start <= end) ? line.substr(start, end - start + 1) : "";
}
/**
* @brief Extracts the name from a file path, including handling directories.
*
* @param path The file path from which to extract the name.
* @return The extracted name as a string. If the path indicates a directory, it extracts the last directory name.
* If the path is empty or no name is found, an empty string is returned.
*/
std::string getNameFromPath(const std::string& path) {
const size_t lastNonSlash = path.find_last_not_of('/');
if (lastNonSlash == std::string::npos) {
return ""; // All slashes, or empty string effectively
}
const size_t lastSlash = path.find_last_of('/', lastNonSlash);
if (lastSlash == std::string::npos) {
return path.substr(0, lastNonSlash + 1); // No slashes, the entire path is a filename
}
return path.substr(lastSlash + 1, lastNonSlash - lastSlash); // Standard case, efficiently handled
}
/**
* @brief Extracts the file name from a full file path.
*
* This function takes a filesystem path and returns only the file name,
* stripping away any directory paths that precede it.
*
* @param path The full path to the file.
* @return The file name extracted from the full path.
*/
std::string getFileName(const std::string& path) {
const size_t pos = path.find_last_of('/');
return (pos != std::string::npos) ? path.substr(pos + 1) : "";
}
/**
* @brief Extracts the name of the parent directory from a given file path at a specified level.
*
* @param path The file path from which to extract the parent directory name.
* @param level The level of the parent directory to extract (0 for immediate parent, 1 for grandparent, and so on).
* @return The name of the parent directory at the specified level.
*/
std::string getParentDirNameFromPath(const std::string& path, size_t level) {
if (path.empty()) return "";
// Start from the end of the string and move backwards to find the slashes
size_t endPos = path.find_last_not_of('/');
if (endPos == std::string::npos) return "";
size_t pos = path.rfind('/', endPos);
if (pos == std::string::npos || pos == 0) return "";
// Navigate up the specified number of levels
while (level-- > 0 && pos != std::string::npos) {
endPos = pos - 1;
pos = path.rfind('/', endPos);
if (pos == std::string::npos || pos == 0) return "";
}
size_t start = path.rfind('/', pos - 1);
if (start == std::string::npos) start = 0;
else start += 1;
// OPTIMIZATION 1: Use find_first_of instead of manual loop - much faster
const bool hasWhitespace = (path.find_first_of(" \t\n\r\f\v", start, pos - start) != std::string::npos);
if (hasWhitespace) {
// OPTIMIZATION 2: Pre-allocate exact size and build efficiently
const size_t dirNameLen = pos - start;
std::string result;
result.reserve(dirNameLen + 2); // +2 for quotes
result = '"';
result.append(path, start, dirNameLen);
result += '"';
return result;
} else {
return path.substr(start, pos - start);
}
}
/**
* @brief Extracts the parent directory path from a given file path.
*
* @param path The file path from which to extract the parent directory path.
* @return The parent directory path.
*/
std::string getParentDirFromPath(const std::string& path) {
const size_t lastSlash = path.find_last_of('/');
if (lastSlash != std::string::npos) {
return path.substr(0, lastSlash + 1);
}
return path;
}
/**
* @brief Check if a directory entry is a directory (no caching).
* Fast path for known types, stat() only when necessary.
*/
inline bool isEntryDirectory(struct dirent* entry, const std::string& path) {
// Fast path - most filesystems populate d_type correctly
if (entry->d_type == DT_DIR) {
return true;
} else if (entry->d_type != DT_UNKNOWN) {
return false; // DT_REG, DT_LNK, etc.
}
// Only stat when d_type is unknown (rare on modern filesystems)
struct stat entryStat;
return (stat(path.c_str(), &entryStat) == 0) && S_ISDIR(entryStat.st_mode);
}
/**
* @brief Gets a list of subdirectories in a directory.
*
* @param directoryPath The path of the directory to search.
* @return A vector of strings containing the names of subdirectories.
*/
std::vector<std::string> getSubdirectories(const std::string& directoryPath) {
std::vector<std::string> subdirectories;
std::unique_ptr<DIR, DirCloser> dir(opendir(directoryPath.c_str()));
if (!dir) return subdirectories;
struct dirent* entry;
while ((entry = readdir(dir.get())) != nullptr) {
const std::string entryName = entry->d_name;
// Skip . and ..
if (entryName == "." || entryName == "..") continue;
const std::string fullPath = directoryPath + "/" + entryName;
if (isEntryDirectory(entry, fullPath)) {
subdirectories.emplace_back(entryName);
}
}
return subdirectories;
}
/**
* @brief Iteratively retrieves a list of files from a directory.
*
* @param directoryPath The path of the directory to search.
* @return A vector of strings containing the paths of the files.
*/
std::vector<std::string> getFilesListFromDirectory(const std::string& directoryPath) {
std::vector<std::string> fileList;
std::vector<std::string> dirsToProcess;
// Initialize with the starting directory
dirsToProcess.emplace_back(directoryPath);
// Pre-allocate string buffer to avoid repeated allocations
std::string fullPath;
std::string currentDir;
size_t dirIndex = 0;
while (dirIndex < dirsToProcess.size()) {
currentDir = std::move(dirsToProcess[dirIndex]);
dirsToProcess[dirIndex].shrink_to_fit();
dirIndex++;
std::unique_ptr<DIR, DirCloser> dir(opendir(currentDir.c_str()));
if (!dir) continue;
// Cache directory path info
const bool needsSlash = currentDir.back() != '/';
struct dirent* entry;
while ((entry = readdir(dir.get())) != nullptr) {
const char* entryName = entry->d_name;
// Direct comparison without string creation
if (entryName[0] == '.' && (entryName[1] == '\0' || (entryName[1] == '.' && entryName[2] == '\0'))) {
continue;
}
// More efficient path building
fullPath.clear();
fullPath.assign(currentDir);
if (needsSlash) fullPath += '/';
fullPath += entryName;
if (entry->d_type == DT_REG) {
// Definitely a regular file
fileList.emplace_back(fullPath);
} else if (isEntryDirectory(entry, fullPath)) {
// Add directory to processing queue
dirsToProcess.emplace_back(fullPath);
}
}
}
return fileList;
}
// Iterative function to handle wildcard directories and file patterns
void handleDirectory(const std::string& basePath,
const std::vector<std::string>& parts,
size_t partIndex,
std::vector<std::string>& results,
bool directoryOnly,
size_t maxLines) {
std::vector<std::pair<std::string, size_t>> stack;
stack.emplace_back(basePath, partIndex);
// Pre-declare strings to avoid repeated allocations
std::string fullPath;
std::string result;
std::string currentPath;
struct stat st;
bool isDir;
size_t currentPartIndex;
while (!stack.empty()) {
if (maxLines > 0 && results.size() >= maxLines) return;
std::tie(currentPath, currentPartIndex) = stack.back();
stack.pop_back();
if (currentPartIndex >= parts.size()) continue;
DIR* dirPtr = opendir(currentPath.c_str());
if (!dirPtr) continue;
std::unique_ptr<DIR, DirCloser> dir(dirPtr);
const std::string& pattern = parts[currentPartIndex];
const bool isLastPart = (currentPartIndex == parts.size() - 1);
const bool needsSlash = currentPath.back() != '/';
struct dirent* entry;
while ((entry = readdir(dir.get())) != nullptr) {
if (maxLines > 0 && results.size() >= maxLines) return;
const char* name = entry->d_name;
if (name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0'))) continue;
if (fnmatch(pattern.c_str(), name, FNM_NOESCAPE) != 0) continue;
if (entry->d_type != DT_UNKNOWN) {
isDir = (entry->d_type == DT_DIR);
} else {
// More efficient path building for stat check
fullPath.clear();
fullPath.assign(currentPath);
if (needsSlash) fullPath += '/';
fullPath += name;
isDir = (stat(fullPath.c_str(), &st) == 0) && S_ISDIR(st.st_mode);
}
if (isLastPart) {
if (!directoryOnly || isDir) {
// More efficient result building
result.clear();
result.assign(currentPath);
if (needsSlash) result += '/';
result += name;
if (isDir) result += '/';
results.emplace_back(std::move(result));
if (maxLines > 0 && results.size() >= maxLines) return;
}
} else if (isDir) {
// More efficient path building for stack
fullPath.clear();
fullPath.assign(currentPath);
if (needsSlash) fullPath += '/';
fullPath += name;
stack.emplace_back(std::move(fullPath), currentPartIndex + 1);
}
}
}
}
/**
* @brief Gets a list of files and folders based on a wildcard pattern.
*
* This function searches for files and folders in a directory that match the
* specified wildcard pattern.
*
* @param pathPattern The wildcard pattern to match files and folders.
* @return A vector of strings containing the paths of matching files and folders.
*/
std::vector<std::string> getFilesListByWildcards(const std::string& pathPattern, size_t maxLines) {
std::vector<std::string> results;
if (pathPattern.empty()) return results;
// Disallow any `/**/` or ending with `/**`
if (pathPattern.find("**") != std::string::npos || pathPattern.find("*null") != std::string::npos || pathPattern.find("null*") != std::string::npos) {
return results; // Exclude invalid patterns
}
const bool directoryOnly = pathPattern.back() == '/';
const size_t prefixEnd = pathPattern.find(":/");
if (prefixEnd == std::string::npos) return results;
const std::string basePath = pathPattern.substr(0, prefixEnd + 2);
std::vector<std::string> parts;
size_t start = prefixEnd + 2;
size_t pos = start;
const size_t pathLen = pathPattern.length();
while (pos <= pathLen) {
if (pos == pathLen || pathPattern[pos] == '/') {
if (pos > start) {
parts.emplace_back(pathPattern.data() + start, pos - start);
}
start = pos + 1;
}
++pos;
}
if (start < pathLen && !directoryOnly) {
parts.emplace_back(pathPattern.data() + start, pathLen - start);
}
// Extra: check parsed parts to disallow "**"
for (size_t i = 0; i + 1 < parts.size(); ++i) {
if (parts[i] == "**" && parts[i + 1] == "**") {
return results; // invalid, exclude
}
}
if (!parts.empty()) {
handleDirectory(basePath, parts, 0, results, directoryOnly, maxLines);
}
return results;
}
}

View File

@@ -0,0 +1,194 @@
/********************************************************************************
* File: global_vars.cpp
* Author: ppkantorski
* Description:
* This source file provides the definitions of global constants and paths used
* throughout the Ultrahand Overlay project. These constants are essential for
* file management and configuration settings within the application.
*
* For the latest updates and contributions, visit the project's GitHub repository:
* GitHub Repository: https://github.com/ppkantorski/Ultrahand-Overlay
*
* Note: This notice is integral to the project's documentation and must not be
* altered or removed.
*
* Licensed under both GPLv2 and CC-BY-4.0
* Copyright (c) 2023-2026 ppkantorski
********************************************************************************/
#include "global_vars.hpp"
namespace ult {
// Base paths
const std::string ROOT_PATH = "sdmc:/";
const std::string BASE_CONFIG_PATH = ROOT_PATH + "config/ultrahand/";
const std::string TESLA_CONFIG_PATH = ROOT_PATH + "config/tesla/";
const std::string SWITCH_PATH = ROOT_PATH + "switch/";
const std::string NX_OVLLOADER_PATH = ROOT_PATH + "config/nx-ovlloader/";
const std::string OVL_HEAP_CONFIG_PATH = NX_OVLLOADER_PATH + "heap_size.bin";
const std::string OVL_EXIT_FLAG_PATH = NX_OVLLOADER_PATH + "exit_flag.bin";
// Filenames
CONSTEXPR_STRING std::string CONFIG_FILENAME = "config.ini";
const std::string BOOT_PACKAGE_FILENAME = "boot_package.ini";
const std::string EXIT_PACKAGE_FILENAME = "exit_package.ini";
const std::string PACKAGE_FILENAME = "package.ini";
const std::string THEME_FILENAME = "theme.ini";
const std::string WALLPAPER_FILENAME = "wallpaper.rgba";
const std::string FUSE_FILENAME = "fuse.ini";
const std::string OVERLAYS_INI_FILENAME = "overlays.ini";
const std::string PACKAGES_INI_FILENAME = "packages.ini";
const std::string NOTIFICATIONS_FLAG_FILENAME = "NOTIFICATIONS.flag";
const std::string RELOADING_FLAG_FILENAME = "RELOADING.flag";
// Project names
CONSTEXPR_STRING std::string ULTRAHAND_PROJECT_NAME = "ultrahand";
CONSTEXPR_STRING std::string CAPITAL_ULTRAHAND_PROJECT_NAME = "Ultrahand";
CONSTEXPR_STRING std::string SPLIT_PROJECT_NAME_1 = "Ultra";
CONSTEXPR_STRING std::string SPLIT_PROJECT_NAME_2 = "hand";
// Paths
const std::string SETTINGS_PATH = BASE_CONFIG_PATH;
const std::string ULTRAHAND_CONFIG_INI_PATH = BASE_CONFIG_PATH + CONFIG_FILENAME;
const std::string TESLA_CONFIG_INI_PATH = TESLA_CONFIG_PATH + CONFIG_FILENAME;
const std::string LANG_PATH = BASE_CONFIG_PATH + "lang/";
const std::string THEMES_PATH = BASE_CONFIG_PATH + "themes/";
const std::string WALLPAPERS_PATH = BASE_CONFIG_PATH + "wallpapers/";
const std::string ASSETS_PATH = BASE_CONFIG_PATH + "assets/";
const std::string SOUNDS_PATH = BASE_CONFIG_PATH + ".sounds/";
const std::string LOADED_SOUNDS_PATH = BASE_CONFIG_PATH + "sounds/";
const std::string FLAGS_PATH = BASE_CONFIG_PATH + "flags/";
const std::string NOTIFICATIONS_PATH = BASE_CONFIG_PATH + "notifications/";
const std::string NOTIFICATIONS_ICONS_PATH = ASSETS_PATH + "notifications/";
const std::string NOTIFICATIONS_FLAGS_PATH = FLAGS_PATH + "notifications/";
const std::string PAYLOADS_PATH = BASE_CONFIG_PATH + "payloads/";
const std::string HB_APPSTORE_JSON = SWITCH_PATH + "appstore/.get/packages/UltrahandOverlay/info.json";
std::string THEME_CONFIG_INI_PATH = BASE_CONFIG_PATH + THEME_FILENAME;
std::string WALLPAPER_PATH = BASE_CONFIG_PATH + WALLPAPER_FILENAME;
const std::string DOWNLOADS_PATH = BASE_CONFIG_PATH + "downloads/";
const std::string FUSE_DATA_INI_PATH = BASE_CONFIG_PATH + FUSE_FILENAME;
const std::string PACKAGE_PATH = SWITCH_PATH + ".packages/";
const std::string OVERLAY_PATH = SWITCH_PATH + ".overlays/";
const std::string OVERLAYS_INI_FILEPATH = BASE_CONFIG_PATH + OVERLAYS_INI_FILENAME;
const std::string PACKAGES_INI_FILEPATH = BASE_CONFIG_PATH + PACKAGES_INI_FILENAME;
const std::string NOTIFICATIONS_FLAG_FILEPATH = FLAGS_PATH + NOTIFICATIONS_FLAG_FILENAME;
const std::string RELOADING_FLAG_FILEPATH = FLAGS_PATH + RELOADING_FLAG_FILENAME;
// Protected files
const std::set<std::string> PROTECTED_FILES = {
ROOT_PATH + "atmosphere/package3",
ROOT_PATH + "atmosphere/stratosphere.romfs"
};
// GitHub URLs
const std::string GITHUB_BASE_URL = "https://github.com/ppkantorski/";
const std::string GITHUB_RAW_BASE_URL = "https://raw.githubusercontent.com/ppkantorski/";
const std::string ULTRAHAND_REPO_URL = GITHUB_BASE_URL + "Ultrahand-Overlay/";
const std::string INCLUDED_THEME_FOLDER_URL = GITHUB_RAW_BASE_URL + "Ultrahand-Overlay/main/themes/";
const std::string LATEST_RELEASE_INFO_URL = GITHUB_RAW_BASE_URL + "Ultrahand-Overlay/main/RELEASE.ini";
const std::string LATEST_UPDATER_INI_URL = ULTRAHAND_REPO_URL + "releases/latest/download/update.ini";
const std::string UPDATER_PAYLOAD_URL = GITHUB_RAW_BASE_URL + "Ultrahand-Overlay/main/payloads/ultrahand_updater.bin";
// Launch options
const std::string LAUNCH_ARGS_STR = "launch_args";
const std::string USE_LAUNCH_ARGS_STR = "use_launch_args";
const std::string USE_QUICK_LAUNCH_STR = "use_quick_launch";
const std::string USE_BOOT_PACKAGE_STR = "use_boot_package";
const std::string USE_EXIT_PACKAGE_STR = "use_exit_package";
const std::string USE_LOGGING_STR = "use_logging";
// Combos
CONSTEXPR_STRING std::string TESLA_COMBO_STR = "L+DDOWN+RS";
CONSTEXPR_STRING std::string ULTRAHAND_COMBO_STR = "ZL+ZR+DDOWN";
// System / mode strings
CONSTEXPR_STRING std::string FUSE_STR = "fuse";
CONSTEXPR_STRING std::string TESLA_STR = "tesla";
CONSTEXPR_STRING std::string ERISTA_STR = "erista";
CONSTEXPR_STRING std::string MARIKO_STR = "mariko";
CONSTEXPR_STRING std::string HANDHELD_STR = "handheld";
CONSTEXPR_STRING std::string DOCKED_STR = "docked";
CONSTEXPR_STRING std::string KEY_COMBO_STR = "key_combo";
CONSTEXPR_STRING std::string DEFAULT_LANG_STR = "default_lang";
// Generic strings
CONSTEXPR_STRING std::string LIST_STR = "list";
CONSTEXPR_STRING std::string LIST_FILE_STR = "list_file";
CONSTEXPR_STRING std::string JSON_STR = "json";
CONSTEXPR_STRING std::string JSON_FILE_STR = "json_file";
CONSTEXPR_STRING std::string INI_FILE_STR = "ini_file";
CONSTEXPR_STRING std::string HEX_FILE_STR = "hex_file";
CONSTEXPR_STRING std::string PACKAGE_STR = "package";
CONSTEXPR_STRING std::string PACKAGES_STR = "packages";
CONSTEXPR_STRING std::string OVERLAY_STR = "overlay";
CONSTEXPR_STRING std::string OVERLAYS_STR = "overlays";
const std::string IN_OVERLAY_STR = "in_overlay";
const std::string IN_HIDDEN_OVERLAY_STR = "in_hidden_overlay";
const std::string IN_HIDDEN_PACKAGE_STR = "in_hidden_package";
CONSTEXPR_STRING std::string FILE_STR = "file";
CONSTEXPR_STRING std::string SYSTEM_STR = "system";
CONSTEXPR_STRING std::string MODE_STR = "mode";
CONSTEXPR_STRING std::string GROUPING_STR = "grouping";
CONSTEXPR_STRING std::string FOOTER_STR = "footer";
const std::string FOOTER_HIGHLIGHT_STR = "footer_highlight";
CONSTEXPR_STRING std::string TOGGLE_STR = "toggle";
CONSTEXPR_STRING std::string LEFT_STR = "left";
CONSTEXPR_STRING std::string RIGHT_STR = "right";
CONSTEXPR_STRING std::string CENTER_STR = "center";
CONSTEXPR_STRING std::string CHAR_STR = "char";
CONSTEXPR_STRING std::string WORD_STR = "word";
CONSTEXPR_STRING std::string NONE_STR = "none";
CONSTEXPR_STRING std::string HIDE_STR = "hide";
CONSTEXPR_STRING std::string STAR_STR = "star";
CONSTEXPR_STRING std::string PRIORITY_STR = "priority";
CONSTEXPR_STRING std::string ON_STR = "on";
CONSTEXPR_STRING std::string OFF_STR = "off";
CONSTEXPR_STRING std::string CAPITAL_ON_STR = "On";
CONSTEXPR_STRING std::string CAPITAL_OFF_STR = "Off";
CONSTEXPR_STRING std::string TRUE_STR = "true";
CONSTEXPR_STRING std::string FALSE_STR = "false";
CONSTEXPR_STRING std::string GLOBAL_STR = "global";
CONSTEXPR_STRING std::string DEFAULT_STR = "default";
CONSTEXPR_STRING std::string HOLD_STR = "hold";
CONSTEXPR_STRING std::string SLOT_STR = "slot";
CONSTEXPR_STRING std::string OPTION_STR = "option";
CONSTEXPR_STRING std::string FORWARDER_STR = "forwarder";
CONSTEXPR_STRING std::string TEXT_STR = "text";
CONSTEXPR_STRING std::string TABLE_STR = "table";
CONSTEXPR_STRING std::string TRACKBAR_STR = "trackbar";
CONSTEXPR_STRING std::string STEP_TRACKBAR_STR = "step_trackbar";
const std::string NAMED_STEP_TRACKBAR_STR = "named_step_trackbar";
CONSTEXPR_STRING std::string NULL_STR = "null";
CONSTEXPR_STRING std::string THEME_STR = "theme";
CONSTEXPR_STRING std::string NOT_AVAILABLE_STR = "Not available";
CONSTEXPR_STRING std::string MEMORY_STR = "memory";
// Pre-defined symbols
CONSTEXPR_STRING std::string OPTION_SYMBOL = "\u22EF";
CONSTEXPR_STRING std::string DROPDOWN_SYMBOL = "\uE14A";
CONSTEXPR_STRING std::string CHECKMARK_SYMBOL = "\uE14B";
CONSTEXPR_STRING std::string CROSSMARK_SYMBOL = "\uE14C";
CONSTEXPR_STRING std::string DOWNLOAD_SYMBOL = "\u2193";
CONSTEXPR_STRING std::string UNZIP_SYMBOL = "\u2191";
CONSTEXPR_STRING std::string COPY_SYMBOL = "\u2192";
CONSTEXPR_STRING std::string INPROGRESS_SYMBOL = "\u25CF";
CONSTEXPR_STRING std::string STAR_SYMBOL = "\u2605";
CONSTEXPR_STRING std::string DIVIDER_SYMBOL = "";
CONSTEXPR_STRING std::string NOTIFY_HEADER = "";
CONSTEXPR_STRING std::string HOLD_A_SYMBOL = "";
const std::vector<std::string> THROBBER_SYMBOLS = {"", "", "", "", "", "", "", ""};
// Atomic variables for progress tracking
std::atomic<int> displayPercentage(0); // for interpreter percentage progress
void resetPercentages() {
displayPercentage.store(-1, std::memory_order_release);
downloadPercentage.store(-1, std::memory_order_release);
unzipPercentage.store(-1, std::memory_order_release);
copyPercentage.store(-1, std::memory_order_release);
}
} // namespace ult

View File

@@ -0,0 +1,199 @@
/********************************************************************************
* File: haptics.cpp
* Author: ppkantorski
* Description:
* This source file provides implementations for the functions declared in
* haptics.hpp. These functions manage haptic feedback for the Ultrahand Overlay
* using libnxs vibration interfaces. It includes routines for initializing
* rumble devices, sending vibration patterns, and handling single or double
* click feedback with timing control. Thread safety is maintained through
* atomic operations and synchronization mechanisms.
*
* For the latest updates and contributions, visit the project's GitHub repository.
* (GitHub Repository: https://github.com/ppkantorski/Ultrahand-Overlay)
*
* Note: Please be aware that this notice cannot be altered or removed. It is a part
* of the project's documentation and must remain intact.
*
* Licensed under both GPLv2 and CC-BY-4.0
* Copyright (c) 2025-2026 ppkantorski
********************************************************************************/
#include "haptics.hpp"
namespace ult {
// ===== Internal state (private to this file) =====
static HidVibrationDeviceHandle vibHandheldLeft;
static HidVibrationDeviceHandle vibHandheldRight;
static HidVibrationDeviceHandle vibPlayer1Left;
static HidVibrationDeviceHandle vibPlayer1Right;
static u64 rumbleStartTick = 0;
static u64 doubleClickTick = 0;
static u8 doubleClickPulse = 0;
static u32 cachedHandheldStyle = 0;
static u32 cachedPlayer1Style = 0;
// ===== Shared flags (accessible globally) =====
std::atomic<bool> clickActive{false};
std::atomic<bool> doubleClickActive{false};
// ===== Constants =====
static constexpr u64 RUMBLE_DURATION_NS = 30'000'000ULL;
static constexpr u64 DOUBLE_CLICK_PULSE_DURATION_NS = 30'000'000ULL;
static constexpr u64 DOUBLE_CLICK_GAP_NS = 100'000'000ULL;
static constexpr HidVibrationValue hapticsPreset = {
.amp_low = 0.20f,
.freq_low = 100.0f,
.amp_high = 0.80f,
.freq_high = 300.0f
};
static constexpr HidVibrationValue vibrationStop{0};
// ===== Internal helpers =====
static inline void sendVibration(const HidVibrationValue* value) {
if (cachedHandheldStyle) {
hidSendVibrationValue(vibHandheldLeft, value);
hidSendVibrationValue(vibHandheldRight, value);
}
if (cachedPlayer1Style) {
hidSendVibrationValue(vibPlayer1Left, value);
hidSendVibrationValue(vibPlayer1Right, value);
}
}
static inline void sendVibration2x(const HidVibrationValue* value) {
sendVibration(value);
sendVibration(value);
}
// ===== Public API =====
void initHaptics() {
const u32 handheldStyle = hidGetNpadStyleSet(HidNpadIdType_Handheld);
const u32 player1Style = hidGetNpadStyleSet(HidNpadIdType_No1);
vibHandheldLeft = vibHandheldRight = vibPlayer1Left = vibPlayer1Right =
(HidVibrationDeviceHandle)0;
static HidVibrationDeviceHandle tmp[2];
if (handheldStyle) {
hidInitializeVibrationDevices(tmp, 2, HidNpadIdType_Handheld,
(HidNpadStyleTag)handheldStyle);
vibHandheldLeft = tmp[0];
vibHandheldRight = tmp[1];
}
if (player1Style) {
hidInitializeVibrationDevices(tmp, 2, HidNpadIdType_No1,
(HidNpadStyleTag)player1Style);
vibPlayer1Left = tmp[0];
vibPlayer1Right = tmp[1];
}
cachedHandheldStyle = handheldStyle;
cachedPlayer1Style = player1Style;
}
void checkAndReinitHaptics() {
static u32 lastHandheldStyle = 0;
static u32 lastPlayer1Style = 0;
const u32 currentHandheldStyle = hidGetNpadStyleSet(HidNpadIdType_Handheld);
const u32 currentPlayer1Style = hidGetNpadStyleSet(HidNpadIdType_No1);
// Reinitialize only if something changed (appearance/disappearance or style change)
if ((currentHandheldStyle != lastHandheldStyle) || (currentPlayer1Style != lastPlayer1Style)) {
initHaptics();
}
// Update last-known styles for change detection
lastHandheldStyle = currentHandheldStyle;
lastPlayer1Style = currentPlayer1Style;
// Update cached styles used by sendVibration()/rumble paths
cachedHandheldStyle = currentHandheldStyle;
cachedPlayer1Style = currentPlayer1Style;
}
void rumbleClick() {
// Use cached style bit instead of querying hid each call
sendVibration(&vibrationStop);
sendVibration2x(&hapticsPreset);
clickActive.store(true, std::memory_order_release);
rumbleStartTick = armGetSystemTick();
}
void rumbleDoubleClick() {
sendVibration(&vibrationStop);
sendVibration2x(&hapticsPreset);
doubleClickActive.store(true, std::memory_order_release);
doubleClickPulse = 1;
doubleClickTick = armGetSystemTick(); // Set ONCE
}
void processRumbleStop(u64 nowNs) {
if (clickActive.load(std::memory_order_acquire) &&
nowNs - armTicksToNs(rumbleStartTick) >= RUMBLE_DURATION_NS) {
sendVibration(&vibrationStop);
clickActive.store(false, std::memory_order_release);
}
}
void processRumbleDoubleClick(u64 nowNs) {
if (!doubleClickActive.load(std::memory_order_acquire)) return;
const u64 elapsed = nowNs - armTicksToNs(doubleClickTick); // Always from original start
switch (doubleClickPulse) {
case 1:
if (elapsed >= DOUBLE_CLICK_PULSE_DURATION_NS) {
sendVibration(&vibrationStop);
doubleClickPulse = 2;
// Don't reset tick!
}
break;
case 2:
if (elapsed >= DOUBLE_CLICK_PULSE_DURATION_NS + DOUBLE_CLICK_GAP_NS) {
sendVibration2x(&hapticsPreset);
doubleClickPulse = 3;
// Don't reset tick!
}
break;
case 3:
if (elapsed >= (DOUBLE_CLICK_PULSE_DURATION_NS * 2) + DOUBLE_CLICK_GAP_NS) {
sendVibration(&vibrationStop);
doubleClickActive.store(false, std::memory_order_release);
doubleClickPulse = 0;
}
break;
}
}
void rumbleDoubleClickStandalone() {
// Standalone uses sleeps, but still use cached style for decision
sendVibration(&vibrationStop);
sendVibration2x(&hapticsPreset);
svcSleepThread(DOUBLE_CLICK_PULSE_DURATION_NS);
sendVibration(&vibrationStop);
svcSleepThread(DOUBLE_CLICK_GAP_NS);
sendVibration2x(&hapticsPreset);
svcSleepThread(DOUBLE_CLICK_PULSE_DURATION_NS);
sendVibration(&vibrationStop);
}
}

View File

@@ -0,0 +1,773 @@
/********************************************************************************
* File: hex_funcs.cpp
* Author: ppkantorski
* Description:
* This source file implements the functions declared in hex_funcs.hpp.
* These functions provide support for manipulating hexadecimal data,
* including conversions between ASCII and hexadecimal strings,
* locating specific hex patterns within files, and editing file contents
* at hex offsets.
*
* For the latest updates and contributions, visit the project's GitHub repository.
* (GitHub Repository: https://github.com/ppkantorski/Ultrahand-Overlay)
*
* Note: Please be aware that this notice cannot be altered or removed. It is a part
* of the project's documentation and must remain intact.
*
* Licensed under both GPLv2 and CC-BY-4.0
* Copyright (c) 2023-2026 ppkantorski
********************************************************************************/
#include "hex_funcs.hpp"
namespace ult {
size_t HEX_BUFFER_SIZE = 4096;//65536/4;
// Thread-safe cache and file operation mutexes
std::shared_mutex cacheMutex; // Allows multiple readers, single writer
std::mutex fileWriteMutex; // Protects file write operations
// For improving the speed of hexing consecutively with the same file and asciiPattern.
std::unordered_map<std::string, std::string> hexSumCache;
/**
* @brief Thread-safe cache management functions
*/
void clearHexSumCache() {
std::lock_guard<std::shared_mutex> writeLock(cacheMutex);
hexSumCache = {};
}
size_t getHexSumCacheSize() {
std::shared_lock<std::shared_mutex> readLock(cacheMutex);
return hexSumCache.size();
}
/**
* @brief Converts an ASCII string to a hexadecimal string.
*
* This function takes an ASCII string as input and converts it into a hexadecimal string.
*
* @param asciiStr The ASCII string to convert.
* @return The corresponding hexadecimal string.
*/
std::string asciiToHex(const std::string& asciiStr) {
std::string hexStr;
for (unsigned char c : asciiStr) {
hexStr.push_back(hexLookup[c >> 4]); // High nibble
hexStr.push_back(hexLookup[c & 0x0F]); // Low nibble
}
return hexStr;
}
/**
* @brief Converts a decimal string to a fixed-width hexadecimal string.
*
* @param decimalStr The decimal string to convert.
* @param byteGroupSize The number of hex digits to output (must be even for byte alignment).
* @return Hex string of exactly 'byteGroupSize' digits, or empty string if value doesn't fit.
*/
std::string decimalToHex(const std::string& decimalStr, int byteGroupSize) {
const int decimalValue = ult::stoi(decimalStr);
if (decimalValue < 0 || byteGroupSize <= 0 || (byteGroupSize % 2) != 0) {
// Invalid input: negative number, or byteGroupSize <= 0, or odd byteGroupSize
return "";
}
// Special case: zero
if (decimalValue == 0) {
return std::string(byteGroupSize, '0');
}
// Convert decimalValue to hex (uppercase, minimal length)
std::string hex;
int tempValue = decimalValue;
int remainder;
char hexChar;
while (tempValue > 0) {
remainder = tempValue % 16;
hexChar = (remainder < 10) ? ('0' + remainder) : ('A' + remainder - 10);
hex.insert(hex.begin(), hexChar);
tempValue /= 16;
}
// Ensure hex length is even by adding leading zero if needed
if (hex.length() % 2 != 0) {
hex.insert(hex.begin(), '0');
}
// Minimum size needed to fit hex string
const size_t hexLen = hex.length();
// Adjust minimum byteGroupSize to be at least hexLen
size_t minByteGroupSize = std::max(static_cast<size_t>(byteGroupSize), hexLen);
// If byteGroupSize was too small, adjust to hex length (must be even)
if (minByteGroupSize % 2 != 0) {
minByteGroupSize++;
}
// If minByteGroupSize is less than hex length, number doesn't fit
if (minByteGroupSize < hexLen) {
return ""; // can't fit
}
// Pad with leading zeros to match minByteGroupSize
if (hexLen < minByteGroupSize) {
hex.insert(hex.begin(), minByteGroupSize - hexLen, '0');
}
return hex;
}
/**
* @brief Converts a hexadecimal string to a decimal string.
*
* This function takes a hexadecimal string as input and converts it into a decimal string.
*
* @param hexStr The hexadecimal string to convert.
* @return The corresponding decimal string.
*/
std::string hexToDecimal(const std::string& hexStr) {
// Convert hexadecimal string to integer
int decimalValue = 0;
const size_t len = hexStr.length();
char hexChar;
int value;
// Iterate over each character in the hexadecimal string
for (size_t i = 0; i < len; ++i) {
hexChar = hexStr[i];
// Convert hex character to its decimal value
if (hexChar >= '0' && hexChar <= '9') {
value = hexChar - '0';
} else if (hexChar >= 'A' && hexChar <= 'F') {
value = 10 + (hexChar - 'A');
} else if (hexChar >= 'a' && hexChar <= 'f') {
value = 10 + (hexChar - 'a');
} else {
break;
}
// Update the decimal value
decimalValue = decimalValue * 16 + value;
}
// Convert the decimal value to a string
return ult::to_string(decimalValue);
}
std::string hexToReversedHex(const std::string& hexadecimal, int order) {
// Reverse the hexadecimal string in groups of order
std::string reversedHex;
for (int i = hexadecimal.length() - order; i >= 0; i -= order) {
reversedHex += hexadecimal.substr(i, order);
}
return reversedHex;
}
/**
* @brief Converts a decimal string to a reversed hexadecimal string.
*
* This function takes a decimal string as input, converts it into a hexadecimal
* string, and reverses the resulting hexadecimal string in groups of byteGroupSize.
*
* @param decimalStr The decimal string to convert.
* @param byteGroupSize The grouping byteGroupSize for reversing the hexadecimal string.
* @return The reversed hexadecimal string.
*/
std::string decimalToReversedHex(const std::string& decimalStr, int byteGroupSize) {
return hexToReversedHex(decimalToHex(decimalStr, byteGroupSize));
}
/**
* @brief Finds the offsets of hexadecimal data in a file.
*
* This function searches for occurrences of hexadecimal data in a binary file
* and returns the file offsets where the data is found.
*
* @param filePath The path to the binary file.
* @param hexData The hexadecimal data to search for.
* @return A vector of strings containing the file offsets where the data is found.
*/
std::vector<std::string> findHexDataOffsets(const std::string& filePath, const std::string& hexData) {
std::vector<std::string> offsets;
FILE* file = fopen(filePath.c_str(), "rb");
if (!file) {
return offsets;
}
fseek(file, 0, SEEK_END);
const size_t fileSize = ftell(file);
fseek(file, 0, SEEK_SET);
if (hexData.length() % 2 != 0) {
fclose(file);
return offsets;
}
const size_t hexLen = hexData.length();
const size_t patternLen = hexLen / 2;
// Use heap allocation for the buffer to avoid stack overflow with large buffer sizes
std::unique_ptr<unsigned char[]> binaryData(new unsigned char[patternLen]);
const unsigned char* hexPtr = reinterpret_cast<const unsigned char*>(hexData.c_str());
// Unrolled hex conversion loop
size_t i = 0;
for (; i + 4 <= hexLen; i += 4) {
binaryData[i/2] = (hexTable[hexPtr[i]] << 4) | hexTable[hexPtr[i + 1]];
binaryData[i/2 + 1] = (hexTable[hexPtr[i + 2]] << 4) | hexTable[hexPtr[i + 3]];
}
// Handle remaining bytes
for (; i < hexLen; i += 2) {
binaryData[i/2] = (hexTable[hexPtr[i]] << 4) | hexTable[hexPtr[i + 1]];
}
// Optimized search variables
const unsigned char* patternPtr = binaryData.get();
const unsigned char firstByte = patternPtr[0];
// Use heap allocation for the buffer to avoid stack overflow with large buffer sizes
std::unique_ptr<unsigned char[]> buffer(new unsigned char[HEX_BUFFER_SIZE]);
size_t bytesRead = 0;
size_t offset = 0;
while ((bytesRead = fread(buffer.get(), 1, HEX_BUFFER_SIZE, file)) > 0) {
const unsigned char* bufPtr = buffer.get();
// Optimized search with first-byte filtering and loop unrolling
i = 0;
const size_t searchEnd = bytesRead;
// Process 4 bytes at a time for better cache usage
for (; i + 4 <= searchEnd; i += 4) {
// Check 4 positions at once
if (bufPtr[i] == firstByte) {
if (offset + i + patternLen <= fileSize &&
memcmp(bufPtr + i, patternPtr, patternLen) == 0) {
offsets.emplace_back(ult::to_string(offset + i));
}
}
if (bufPtr[i + 1] == firstByte) {
if (offset + i + 1 + patternLen <= fileSize &&
memcmp(bufPtr + i + 1, patternPtr, patternLen) == 0) {
offsets.emplace_back(ult::to_string(offset + i + 1));
}
}
if (bufPtr[i + 2] == firstByte) {
if (offset + i + 2 + patternLen <= fileSize &&
memcmp(bufPtr + i + 2, patternPtr, patternLen) == 0) {
offsets.emplace_back(ult::to_string(offset + i + 2));
}
}
if (bufPtr[i + 3] == firstByte) {
if (offset + i + 3 + patternLen <= fileSize &&
memcmp(bufPtr + i + 3, patternPtr, patternLen) == 0) {
offsets.emplace_back(ult::to_string(offset + i + 3));
}
}
}
// Handle remaining bytes
for (; i < searchEnd; ++i) {
if (bufPtr[i] == firstByte) {
if (offset + i + patternLen <= fileSize &&
memcmp(bufPtr + i, patternPtr, patternLen) == 0) {
offsets.emplace_back(ult::to_string(offset + i));
}
}
}
offset += bytesRead;
}
fclose(file);
return offsets;
}
/**
* @brief Edits hexadecimal data in a file at a specified offset.
*
* This function opens a binary file, seeks to a specified offset, and replaces
* the data at that offset with the provided hexadecimal data.
*
* @param filePath The path to the binary file.
* @param offsetStr The offset in the file to perform the edit.
* @param hexData The hexadecimal data to replace at the offset.
*/
void hexEditByOffset(const std::string& filePath, const std::string& offsetStr, const std::string& hexData) {
// Lock file writes to prevent concurrent modifications to the same file
std::lock_guard<std::mutex> fileWriteLock(fileWriteMutex);
const std::streampos offset = std::stoll(offsetStr);
// Open the file for both reading and writing in binary mode
FILE* file = fopen(filePath.c_str(), "rb+");
if (!file) {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Failed to open the file.");
#endif
return;
}
// Retrieve the file size
fseek(file, 0, SEEK_END);
const std::streampos fileSize = ftell(file);
if (offset >= fileSize) {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Offset exceeds file size.");
#endif
fclose(file);
return;
}
// Validate hex data length
const size_t hexLen = hexData.length();
if (hexLen % 2 != 0) {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Invalid hex data length.");
#endif
fclose(file);
return;
}
// Convert the hex string to binary data using optimized lookup table
const size_t dataLen = hexLen / 2;
std::unique_ptr<unsigned char[]> binaryData(new unsigned char[dataLen]);
const unsigned char* hexPtr = reinterpret_cast<const unsigned char*>(hexData.c_str());
// Unrolled hex conversion loop (same as findHexDataOffsets)
size_t i = 0;
for (; i + 4 <= hexLen; i += 4) {
binaryData[i/2] = (hexTable[hexPtr[i]] << 4) | hexTable[hexPtr[i + 1]];
binaryData[i/2 + 1] = (hexTable[hexPtr[i + 2]] << 4) | hexTable[hexPtr[i + 3]];
}
// Handle remaining bytes
for (; i < hexLen; i += 2) {
binaryData[i/2] = (hexTable[hexPtr[i]] << 4) | hexTable[hexPtr[i + 1]];
}
// Move to the specified offset and write the binary data directly to the file
fseek(file, offset, SEEK_SET);
const size_t bytesWritten = fwrite(binaryData.get(), sizeof(unsigned char), dataLen, file);
if (bytesWritten != dataLen) {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Failed to write data to the file.");
#endif
fclose(file);
return;
}
fclose(file);
}
/**
* @brief Edits a specific offset in a file with custom hexadecimal data.
*
* This function searches for a custom pattern in the file and calculates a new offset
* based on user-provided offsetStr and the found pattern. It then replaces the data
* at the calculated offset with the provided hexadecimal data.
*
* @param filePath The path to the binary file.
* @param offsetStr The user-provided offset for the edit.
* @param customPattern The custom pattern to search for in the file.
* @param hexDataReplacement The hexadecimal data to replace at the calculated offset.
* @param occurrence The occurrence/index of the data to replace (default is "0" to replace all occurrences).
*/
void hexEditByCustomOffset(const std::string& filePath, const std::string& customAsciiPattern, const std::string& offsetStr, const std::string& hexDataReplacement, size_t occurrence) {
// Create a cache key based on filePath and customAsciiPattern
const std::string cacheKey = filePath + '?' + customAsciiPattern + '?' + ult::to_string(occurrence);
int hexSum = -1;
// Thread-safe cache access
{
std::shared_lock<std::shared_mutex> readLock(cacheMutex);
const auto cachedResult = hexSumCache.find(cacheKey);
if (cachedResult != hexSumCache.end()) {
hexSum = ult::stoi(cachedResult->second);
}
}
if (hexSum == -1) {
std::string customHexPattern;
if (customAsciiPattern[0] == '#') {
// remove #
customHexPattern = customAsciiPattern.substr(1);
} else {
// Convert custom ASCII pattern to a custom hex pattern
customHexPattern = asciiToHex(customAsciiPattern);
}
// Find hex data offsets in the file
const std::vector<std::string> offsets = findHexDataOffsets(filePath, customHexPattern);
if (!offsets.empty()) {
hexSum = ult::stoi(offsets[occurrence]);
// Thread-safe cache write
{
std::lock_guard<std::shared_mutex> writeLock(cacheMutex);
hexSumCache[cacheKey] = ult::to_string(hexSum);
}
} else {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Offset not found.");
#endif
return;
}
}
if (hexSum != -1) {
// Calculate the total offset to seek in the file
hexEditByOffset(filePath, ult::to_string(hexSum + ult::stoi(offsetStr)), hexDataReplacement);
} else {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Failed to find " + customAsciiPattern + ".");
#endif
}
}
/**
* @brief Finds and replaces hexadecimal data in a file.
*
* This function searches for occurrences of hexadecimal data in a binary file
* and replaces them with a specified hexadecimal replacement data.
*
* @param filePath The path to the binary file.
* @param hexDataToReplace The hexadecimal data to search for and replace.
* @param hexDataReplacement The hexadecimal data to replace with.
* @param occurrence The occurrence/index of the data to replace (default is "0" to replace all occurrences).
*/
void hexEditFindReplace(const std::string& filePath, const std::string& hexDataToReplace, const std::string& hexDataReplacement, size_t occurrence) {
const std::vector<std::string> offsetStrs = findHexDataOffsets(filePath, hexDataToReplace);
if (!offsetStrs.empty()) {
if (occurrence == 0) {
// Replace all occurrences
for (const std::string& offsetStr : offsetStrs) {
hexEditByOffset(filePath, offsetStr, hexDataReplacement);
}
} else {
// Convert the occurrence string to an integer
if (occurrence > 0 && occurrence <= offsetStrs.size()) {
// Replace the specified occurrence/index
hexEditByOffset(filePath, offsetStrs[occurrence - 1], hexDataReplacement);
} else {
// Invalid occurrence/index specified
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Invalid hex occurrence/index specified.");
#endif
}
}
}
}
/**
* @brief Finds and replaces hexadecimal data in a file.
*
* This function searches for occurrences of hexadecimal data in a binary file
* and replaces them with a specified hexadecimal replacement data.
*
* @param filePath The path to the binary file.
* @param hexDataToReplace The hexadecimal data to search for and replace.
* @param hexDataReplacement The hexadecimal data to replace with.
* @param occurrence The occurrence/index of the data to replace (default is "0" to replace all occurrences).
*/
std::string parseHexDataAtCustomOffset(const std::string& filePath, const std::string& customAsciiPattern,
const std::string& offsetStr, size_t length, size_t occurrence) {
const std::string cacheKey = filePath + '?' + customAsciiPattern + '?' + ult::to_string(occurrence);
int hexSum = -1;
// Thread-safe cache read
{
std::shared_lock<std::shared_mutex> readLock(cacheMutex);
const auto cachedResult = hexSumCache.find(cacheKey);
if (cachedResult != hexSumCache.end()) {
hexSum = ult::stoi(cachedResult->second);
}
}
if (hexSum == -1) {
const std::string customHexPattern = asciiToHex(customAsciiPattern);
const std::vector<std::string> offsets = findHexDataOffsets(filePath, customHexPattern);
if (!offsets.empty() && offsets.size() > occurrence) {
hexSum = ult::stoi(offsets[occurrence]);
// Thread-safe cache write
{
std::lock_guard<std::shared_mutex> writeLock(cacheMutex);
hexSumCache[cacheKey] = ult::to_string(hexSum);
}
} else {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Offset not found.");
#endif
return "";
}
}
const std::streampos totalOffset = hexSum + std::stoll(offsetStr);
// Pre-allocate final string size to avoid reallocation
std::string result;
result.reserve(length * 2);
FILE* file = fopen(filePath.c_str(), "rb");
if (!file) {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Failed to open the file.");
#endif
return "";
}
if (fseek(file, totalOffset, SEEK_SET) != 0) {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Error seeking to offset.");
#endif
fclose(file);
return "";
}
// Use heap allocation for the buffer to avoid stack overflow with large buffer sizes
std::unique_ptr<unsigned char[]> buffer(new unsigned char[length]);
const size_t bytesRead = fread(buffer.get(), 1, length, file);
fclose(file);
if (bytesRead != length) {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Error reading data from file or end of file reached.");
#endif
return "";
}
// Optimized hex conversion - directly build uppercase string
static constexpr char hexDigits[] = "0123456789ABCDEF";
result.resize(length * 2);
// Unrolled loop for better performance
size_t i = 0;
for (; i + 4 <= length; i += 4) {
result[i * 2] = hexDigits[(buffer[i] >> 4) & 0xF];
result[i * 2 + 1] = hexDigits[buffer[i] & 0xF];
result[i * 2 + 2] = hexDigits[(buffer[i + 1] >> 4) & 0xF];
result[i * 2 + 3] = hexDigits[buffer[i + 1] & 0xF];
result[i * 2 + 4] = hexDigits[(buffer[i + 2] >> 4) & 0xF];
result[i * 2 + 5] = hexDigits[buffer[i + 2] & 0xF];
result[i * 2 + 6] = hexDigits[(buffer[i + 3] >> 4) & 0xF];
result[i * 2 + 7] = hexDigits[buffer[i + 3] & 0xF];
}
// Handle remaining bytes
for (; i < length; ++i) {
result[i * 2] = hexDigits[(buffer[i] >> 4) & 0xF];
result[i * 2 + 1] = hexDigits[buffer[i] & 0xF];
}
return result;
}
/**
* @brief Finds and replaces hexadecimal data in a file.
*
* This function searches for occurrences of hexadecimal data in a binary file
* and replaces them with a specified hexadecimal replacement data.
*
* @param filePath The path to the binary file.
* @param hexDataToReplace The hexadecimal data to search for and replace.
* @param hexDataReplacement The hexadecimal data to replace with.
* @param occurrence The occurrence/index of the data to replace (default is "0" to replace all occurrences).
*/
std::string replaceHexPlaceholder(const std::string& arg, const std::string& hexPath) {
std::string replacement = arg;
const std::string searchString = "{hex_file(";
const size_t startPos = replacement.find(searchString);
const size_t endPos = replacement.find(")}");
if (startPos != std::string::npos && endPos != std::string::npos && endPos > startPos) {
const std::string placeholderContent = replacement.substr(startPos + searchString.length(), endPos - startPos - searchString.length());
// Split the placeholder content into its components (customAsciiPattern, offsetStr, length)
std::vector<std::string> components;
// Use StringStream instead of std::istringstream
StringStream componentStream(placeholderContent);
std::string component;
while (componentStream.getline(component, ',')) {
trim(component);
components.push_back(component);
}
if (components.size() == 3) {
// Extract individual components
const std::string customAsciiPattern = components[0];
const std::string offsetStr = components[1];
const size_t length = std::stoul(components[2]);
// Call the parsing function and replace the placeholder
const std::string parsedResult = parseHexDataAtCustomOffset(hexPath, customAsciiPattern, offsetStr, length);
// Only replace if parsedResult returns a non-empty string
if (!parsedResult.empty()) {
// Replace the entire placeholder with the parsed result
replacement.replace(startPos, endPos - startPos + searchString.length() + 2, parsedResult);
}
}
}
return replacement;
}
/**
* @brief Extracts the version string from a binary file.
*
* This function reads a binary file and searches for a version pattern
* in the format "v#.#.#" (e.g., "v1.2.3").
*
* @param filePath The path to the binary file.
* @return The version string if found; otherwise, an empty string.
*/
std::string extractVersionFromBinary(const std::string &filePath) {
// Step 1: Open the binary file
FILE* file = fopen(filePath.c_str(), "rb");
if (!file) {
return ""; // Return empty string if file cannot be opened
}
// Get the file size
fseek(file, 0, SEEK_END);
const std::streamsize size = ftell(file);
fseek(file, 0, SEEK_SET);
// Read the entire file into a buffer
std::vector<uint8_t> buffer(size);
const size_t bytesRead = fread(buffer.data(), sizeof(uint8_t), size, file);
fclose(file); // Close the file after reading
if (bytesRead != static_cast<size_t>(size)) {
return ""; // Return empty string if reading fails
}
// Step 2: Search for the pattern "v#.#.#"
const char* data = reinterpret_cast<const char*>(buffer.data());
for (std::streamsize i = 0; i < size; ++i) {
if (data[i] == 'v' && i + 5 < size &&
std::isdigit(data[i + 1]) && data[i + 2] == '.' &&
std::isdigit(data[i + 3]) && data[i + 4] == '.' &&
std::isdigit(data[i + 5])) {
// Extract the version string
return std::string(data + i, 6); // Return the version string
}
}
return ""; // Return empty string if no match is found
}
// 1. Table optimization: Mark as constexpr for compile-time evaluation
static constexpr uint8_t b64_table[256] = {
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,62, 0xFF,0xFF,0xFF,63,
52,53,54,55,56,57,58,59,60,61,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF
};
// 2. Optimized decode: Pre-calculate output size, reduce bounds checking
static size_t base64_decode(const char* src, uint8_t* out) {
size_t outLen = 0;
const char* p = src;
// Process 4 chars at a time (unrolled loop for better instruction pipelining)
while (*p) {
uint8_t a = b64_table[static_cast<uint8_t>(*p++)];
if (a == 0xFF) break;
uint8_t b = b64_table[static_cast<uint8_t>(*p++)];
if (b == 0xFF) break;
out[outLen++] = (a << 2) | (b >> 4);
uint8_t cChar = *p++;
if (cChar == '=' || cChar == '\0') break;
uint8_t c = b64_table[cChar];
if (c == 0xFF) break;
out[outLen++] = (b << 4) | (c >> 2);
uint8_t dChar = *p++;
if (dChar == '=' || dChar == '\0') break;
uint8_t d = b64_table[dChar];
if (d == 0xFF) break;
out[outLen++] = (c << 6) | d;
}
return outLen;
}
// 3. Optimized wrapper: Pre-calculate exact output size, avoid vector overhead
std::string decodeBase64ToString(const std::string& b64) {
// Base64 decodes to ~3/4 original size
const size_t maxOutSize = (b64.size() * 3) / 4 + 3;
std::string result(maxOutSize, '\0');
const size_t len = base64_decode(b64.c_str(), reinterpret_cast<uint8_t*>(result.data()));
result.resize(len);
return result;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,350 @@
/********************************************************************************
* File: json_funcs.cpp
* Author: ppkantorski
* Description:
* This source file provides functions for working with JSON files in C++ using
* the `cJSON` library. It includes a function to read JSON data from a file.
*
* For the latest updates and contributions, visit the project's GitHub repository.
* (GitHub Repository: https://github.com/ppkantorski/Ultrahand-Overlay)
*
* Note: Please be aware that this notice cannot be altered or removed. It is a part
* of the project's documentation and must remain intact.
*
* Licensed under both GPLv2 and CC-BY-4.0
* Copyright (c) 2023-2026 ppkantorski
********************************************************************************/
#include "json_funcs.hpp"
namespace ult {
static std::mutex json_access_mutex;
/**
* @brief Reads JSON data from a file and returns it as a `json_t` object.
*
* @param filePath The path to the JSON file.
* @return A `json_t` object representing the parsed JSON data. Returns `nullptr` on error.
*/
json_t* readJsonFromFile(const std::string& filePath) {
std::lock_guard<std::mutex> lock(json_access_mutex);
FILE* file = fopen(filePath.c_str(), "rb");
if (!file) {
return nullptr;
}
// Get file size
fseek(file, 0, SEEK_END);
const long fileSize = ftell(file);
fseek(file, 0, SEEK_SET);
// Check for reasonable file size
if (fileSize <= 0 || fileSize > 6 * 1024 * 1024) { // 6MB limit
fclose(file);
return nullptr;
}
// Use vector<char> for better performance and explicit null termination
std::vector<char> buffer;
buffer.resize(static_cast<size_t>(fileSize) + 1); // +1 for null terminator
// Read the file in one operation
const size_t bytesRead = fread(buffer.data(), 1, static_cast<size_t>(fileSize), file);
fclose(file);
if (bytesRead != static_cast<size_t>(fileSize)) {
return nullptr;
}
// Ensure null termination for cJSON
buffer[bytesRead] = '\0';
// Parse the JSON content - pass buffer directly to avoid string copy
cJSON* root = cJSON_Parse(buffer.data());
if (!root) {
#if USING_LOGGING_DIRECTIVE
const char* error_ptr = cJSON_GetErrorPtr();
if (error_ptr) {
if (!disableLogging)
logMessage("JSON parsing error: " + std::string(error_ptr));
}
#endif
return nullptr;
}
return reinterpret_cast<json_t*>(root);
}
/**
* @brief Parses a JSON string into a json_t object.
*
* This function takes a JSON string as input and parses it into a json_t object using cJSON library's `cJSON_Parse` function.
* If parsing fails, it logs the error and returns nullptr.
*
* @param input The input JSON string to parse.
* @return A json_t object representing the parsed JSON, or nullptr if parsing fails.
*/
json_t* stringToJson(const std::string& input) {
cJSON* jsonObj = cJSON_Parse(input.c_str());
if (!jsonObj) {
#if USING_LOGGING_DIRECTIVE
const char* error_ptr = cJSON_GetErrorPtr();
if (error_ptr != nullptr) {
if (!disableLogging)
logMessage("Failed to parse JSON: " + std::string(error_ptr));
}
#endif
return nullptr; // Return nullptr to indicate failure clearly
}
return reinterpret_cast<json_t*>(jsonObj);
}
// Function to get a string from a JSON object
std::string getStringFromJson(const json_t* root, const char* key) {
const cJSON* croot = reinterpret_cast<const cJSON*>(root);
const cJSON* value = cJSON_GetObjectItemCaseSensitive(croot, key);
if (value && cJSON_IsString(value) && value->valuestring) {
return value->valuestring;
} else {
return ""; // Key not found or not a string, return empty string/char*
}
}
/**
* @brief Loads a JSON file from the specified path and retrieves a string value for a given key.
*
* This function combines the functionality of loading a JSON file and retrieving a string value associated
* with a given key in a single step.
*
* @param filePath The path to the JSON file.
* @param key The key whose associated string value is to be retrieved.
* @return A string containing the value associated with the given key, or an empty string if the key is not found.
*/
std::string getStringFromJsonFile(const std::string& filePath, const std::string& key) {
// Load JSON from file using a smart pointer
std::unique_ptr<json_t, JsonDeleter> root(readJsonFromFile(filePath), JsonDeleter());
if (!root) {
#if USING_LOGGING_DIRECTIVE
logMessage("Failed to load JSON file from path: " + filePath);
#endif
return "";
}
// Retrieve the string value associated with the key
cJSON* croot = reinterpret_cast<cJSON*>(root.get());
cJSON* jsonKey = cJSON_GetObjectItemCaseSensitive(croot, key.c_str());
const char* value = (cJSON_IsString(jsonKey) && jsonKey->valuestring) ? jsonKey->valuestring : nullptr;
// Check if the value was found and return it
if (value) {
return std::string(value);
} else {
#if USING_LOGGING_DIRECTIVE
logMessage("Key not found or not a string in JSON: " + key);
#endif
return "";
}
}
/**
* @brief Sets a value in a JSON file, creating the file if it doesn't exist.
*
* @param filePath The path to the JSON file.
* @param key The key to set.
* @param value The value to set (auto-detected type).
* @param createIfNotExists Whether to create the file if it doesn't exist.
* @return true if successful, false otherwise.
*/
bool setJsonValue(const std::string& filePath, const std::string& key, const std::string& value, bool createIfNotExists) {
// Try to load existing file
std::unique_ptr<json_t, JsonDeleter> root(readJsonFromFile(filePath), JsonDeleter());
cJSON* croot = nullptr;
// If file doesn't exist, create new JSON object if allowed
if (!root) {
if (!createIfNotExists) {
return false;
}
croot = cJSON_CreateObject();
if (!croot) {
return false;
}
root.reset(reinterpret_cast<json_t*>(croot));
} else {
croot = reinterpret_cast<cJSON*>(root.get());
}
// FIXED: Better value type detection
cJSON* jsonValue = nullptr;
// Remove trimming to preserve leading/trailing spaces for all languages
// std::string trimmedValue = value;
// trimmedValue.erase(0, trimmedValue.find_first_not_of(" \t\n\r"));
// trimmedValue.erase(trimmedValue.find_last_not_of(" \t\n\r") + 1);
if (value.empty()) {
jsonValue = cJSON_CreateString("");
} else if (value == "true") {
jsonValue = cJSON_CreateBool(1);
} else if (value == "false") {
jsonValue = cJSON_CreateBool(0);
} else if (value == "null") {
jsonValue = cJSON_CreateNull();
} else {
// Try parsing as number (integer or float)
char* endPtr = nullptr;
errno = 0;
// Try as integer first
const long longValue = std::strtol(value.c_str(), &endPtr, 10);
if (endPtr == value.c_str() + value.length() && errno == 0) {
// Successfully parsed as integer
jsonValue = cJSON_CreateNumber(static_cast<double>(longValue));
} else {
// Try as float
endPtr = nullptr;
errno = 0;
const double doubleValue = std::strtod(value.c_str(), &endPtr);
if (endPtr == value.c_str() + value.length() && errno == 0) {
// Successfully parsed as float
jsonValue = cJSON_CreateNumber(doubleValue);
} else {
// Treat as string
jsonValue = cJSON_CreateString(value.c_str());
}
}
}
if (!jsonValue) {
return false;
}
// Delete existing item if it exists
cJSON_DeleteItemFromObject(croot, key.c_str());
// Add the new value
cJSON_AddItemToObject(croot, key.c_str(), jsonValue);
// Save to file with formatted output for better readability
char* jsonString = cJSON_Print(croot); // Use formatted output instead of PrintUnformatted
if (!jsonString) {
return false;
}
bool success = false;
{
std::lock_guard<std::mutex> lock(json_access_mutex);
FILE* file = fopen(filePath.c_str(), "w"); // Use text mode for JSON
if (file) {
const size_t jsonLength = std::strlen(jsonString);
const size_t bytesWritten = fwrite(jsonString, 1, jsonLength, file);
success = (bytesWritten == jsonLength);
fclose(file);
}
}
cJSON_free(jsonString);
return success;
}
/**
* @brief Renames a key in a JSON file.
*
* @param filePath The path to the JSON file.
* @param oldKey The current key name.
* @param newKey The new key name.
* @return true if successful, false otherwise.
*/
bool renameJsonKey(const std::string& filePath, const std::string& oldKey, const std::string& newKey) {
// Try to load existing file
std::unique_ptr<json_t, JsonDeleter> root(readJsonFromFile(filePath), JsonDeleter());
if (!root) {
return false;
}
cJSON* croot = reinterpret_cast<cJSON*>(root.get());
// Check if old key exists
cJSON* value = cJSON_GetObjectItemCaseSensitive(croot, oldKey.c_str());
if (!value) {
return false;
}
// Detach the value from the object
cJSON_DetachItemFromObject(croot, oldKey.c_str());
// Add it back with the new key
cJSON_AddItemToObject(croot, newKey.c_str(), value);
// Save to file
char* jsonString = cJSON_Print(croot); // Use formatted output
if (!jsonString) {
return false;
}
bool success = false;
{
std::lock_guard<std::mutex> lock(json_access_mutex);
FILE* file = fopen(filePath.c_str(), "w"); // Use text mode
if (file) {
const size_t jsonLength = std::strlen(jsonString);
const size_t bytesWritten = fwrite(jsonString, 1, jsonLength, file);
success = (bytesWritten == jsonLength);
fclose(file);
}
}
cJSON_free(jsonString);
return success;
}
void pushNotificationJson(const std::string& appID, const std::string& text, size_t fontSize) {
u64 tick = armGetSystemTick();
std::string filename = appID + "-" + std::to_string(tick) + ".notify"; // priority 20 default
// Build full path
const std::string fullPath = NOTIFICATIONS_PATH + filename;
// Create JSON object
cJSON* notif = cJSON_CreateObject();
if (!notif) return;
cJSON_AddStringToObject(notif, "text", text.c_str());
cJSON_AddNumberToObject(notif, "font_size", static_cast<double>(fontSize));
// Serialize JSON
char* rendered = cJSON_PrintUnformatted(notif);
if (!rendered) {
cJSON_Delete(notif);
return;
}
// Write to file (C-style)
FILE* file = fopen(fullPath.c_str(), "wb");
if (file) {
fwrite(rendered, 1, strlen(rendered), file);
fclose(file);
}
cJSON_free(rendered);
cJSON_Delete(notif);
}
}

View File

@@ -0,0 +1,400 @@
/********************************************************************************
* File: list_funcs.cpp
* Author: ppkantorski
* Description:
* This source file contains function declarations and utility functions related
* to working with lists and vectors of strings. These functions are used in the
* Ultrahand Overlay project to perform various operations on lists, such as
* removing entries, filtering, and more.
*
* For the latest updates and contributions, visit the project's GitHub repository.
* (GitHub Repository: https://github.com/ppkantorski/Ultrahand-Overlay)
*
* Note: Please be aware that this notice cannot be altered or removed. It is a part
* of the project's documentation and must remain intact.
*
* Licensed under both GPLv2 and CC-BY-4.0
* Copyright (c) 2023-2026 ppkantorski
********************************************************************************/
#include <list_funcs.hpp>
#include <mutex>
namespace ult {
static constexpr const char* UNABLE_TO_OPEN_FILE = "Unable to open file: ";
// Thread-safe file access mutex
static std::mutex file_access_mutex;
std::vector<std::string> splitIniList(const std::string& value) {
std::vector<std::string> result;
std::string trimmed = value;
trim(trimmed);
if (trimmed.size() > 2 && trimmed.front() == '(' && trimmed.back() == ')') {
trimmed = trimmed.substr(1, trimmed.size() - 2);
ult::StringStream ss(trimmed);
std::string token;
while (ss.getline(token, ',')) {
trim(token);
result.push_back(token);
}
}
return result;
}
std::string joinIniList(const std::vector<std::string>& list) {
std::string result = "";
for (size_t i = 0; i < list.size(); ++i) {
result += list[i];
if (i + 1 < list.size()) {
result += ", ";
}
}
return result;
}
/**
* @brief Removes entries from a vector of strings that match a specified entry.
*
* This function removes entries from the `itemsList` vector of strings that match the `entry`.
*
* @param entry The entry to be compared against the elements in `itemsList`.
* @param itemsList The vector of strings from which matching entries will be removed.
*/
void removeEntryFromList(const std::string& entry, std::vector<std::string>& itemsList) {
itemsList.erase(std::remove_if(itemsList.begin(), itemsList.end(), [&](const std::string& item) {
return item.compare(0, entry.length(), entry) == 0;
}), itemsList.end());
}
/**
* @brief Filters a list of strings based on a specified filter list.
*
* This function filters a list of strings (`itemsList`) by removing entries that match any
* of the criteria specified in the `filterList`. It uses the `removeEntryFromList` function
* to perform the removal.
*
* @param filterList The list of entries to filter by. Entries in `itemsList` matching any entry in this list will be removed.
* @param itemsList The list of strings to be filtered.
*/
void filterItemsList(const std::vector<std::string>& filterList, std::vector<std::string>& itemsList) {
for (const auto& entry : filterList) {
removeEntryFromList(entry, itemsList);
}
}
// Function to read file into a vector of strings with optional cap and newline preservation
std::vector<std::string> readListFromFile(const std::string& filePath, size_t maxLines, bool preserveNewlines) {
std::lock_guard<std::mutex> lock(file_access_mutex);
std::vector<std::string> lines;
FILE* file = fopen(filePath.c_str(), "r");
if (!file) {
#if USING_LOGGING_DIRECTIVE
logMessage(UNABLE_TO_OPEN_FILE + filePath);
#endif
return lines;
}
static constexpr size_t BUFFER_SIZE = 8192;
char buffer[BUFFER_SIZE];
size_t len;
while (fgets(buffer, BUFFER_SIZE, file)) {
// Check cap before processing
if (maxLines > 0 && lines.size() >= maxLines) {
break;
}
if (preserveNewlines) {
// Keep the line as-is, including newlines
lines.emplace_back(buffer);
} else {
// Remove newlines
len = strlen(buffer);
if (len > 0 && buffer[len - 1] == '\n') {
buffer[len - 1] = '\0';
--len;
// Also remove carriage return if present
if (len > 0 && buffer[len - 1] == '\r') {
buffer[len - 1] = '\0';
}
}
lines.emplace_back(buffer);
}
}
fclose(file);
return lines;
}
// Function to get an entry from the list based on the index
std::string getEntryFromListFile(const std::string& listPath, size_t listIndex) {
std::lock_guard<std::mutex> lock(file_access_mutex);
FILE* file = fopen(listPath.c_str(), "r");
if (!file) {
#if USING_LOGGING_DIRECTIVE
logMessage(UNABLE_TO_OPEN_FILE + listPath);
#endif
return "";
}
static constexpr size_t BUFFER_SIZE = 8192;
char buffer[BUFFER_SIZE];
// Skip lines until reaching the desired index
for (size_t i = 0; i < listIndex; ++i) {
if (!fgets(buffer, BUFFER_SIZE, file)) {
fclose(file);
return ""; // Index out of bounds
}
}
// Read the target line
if (!fgets(buffer, BUFFER_SIZE, file)) {
fclose(file);
return ""; // Index out of bounds
}
fclose(file);
// Efficiently remove newline character
const size_t len = strlen(buffer);
if (len > 0 && buffer[len - 1] == '\n') {
buffer[len - 1] = '\0';
if (len > 1 && buffer[len - 2] == '\r') {
buffer[len - 2] = '\0';
}
}
return std::string(buffer);
}
/**
* @brief Splits a string into a vector of strings using a delimiter.
*
* This function splits the input string into multiple strings using the specified delimiter.
*
* @param str The input string to split.
* @return A vector of strings containing the split values.
*/
std::vector<std::string> stringToList(const std::string& str) {
std::vector<std::string> result;
if (str.empty()) {
return result;
}
// Check if the input string starts and ends with '(' and ')' or '[' and ']'
if ((str.front() == '(' && str.back() == ')') || (str.front() == '[' && str.back() == ']')) {
// Work directly with the original string using indices instead of creating substring
size_t start = 1; // Skip opening bracket/paren
size_t end = 1;
const size_t values_end = str.size() - 1; // Skip closing bracket/paren
// Pre-declare item string to avoid repeated allocations
std::string item;
// Iterate through the string manually to split by commas
while ((end = str.find(',', start)) != std::string::npos && end < values_end) {
// Extract item directly from original string without creating substring
item.assign(str, start, end - start);
// Trim leading and trailing spaces
trim(item);
// Remove quotes from each token if necessary
removeQuotes(item);
result.push_back(std::move(item));
start = end + 1;
}
// Handle the last item after the last comma
if (start < values_end) {
item.assign(str, start, values_end - start);
trim(item);
removeQuotes(item);
result.push_back(std::move(item));
}
}
return result;
}
// Function to read file into a set of strings
std::unordered_set<std::string> readSetFromFile(const std::string& filePath, const std::string& packagePath) {
std::lock_guard<std::mutex> lock(file_access_mutex);
std::unordered_set<std::string> lines;
FILE* file = fopen(filePath.c_str(), "r");
if (!file) {
#if USING_LOGGING_DIRECTIVE
logMessage(UNABLE_TO_OPEN_FILE + filePath);
#endif
return lines;
}
static constexpr size_t BUFFER_SIZE = 8192;
char buffer[BUFFER_SIZE];
size_t len;
while (fgets(buffer, BUFFER_SIZE, file)) {
// Remove trailing newline character if it exists
len = strlen(buffer);
if (len > 0 && buffer[len - 1] == '\n') {
buffer[len - 1] = '\0';
}
std::string line = buffer;
if (!packagePath.empty()) {
preprocessPath(line, packagePath);
}
lines.insert(std::move(line));
}
fclose(file);
return lines;
}
// Function to write a set to a file
void writeSetToFile(const std::unordered_set<std::string>& fileSet, const std::string& filePath) {
std::lock_guard<std::mutex> lock(file_access_mutex);
FILE* file = fopen(filePath.c_str(), "w");
if (!file) {
#if USING_LOGGING_DIRECTIVE
logMessage(UNABLE_TO_OPEN_FILE + filePath);
#endif
return;
}
for (const auto& entry : fileSet) {
fprintf(file, "%s\n", entry.c_str());
}
fclose(file);
}
// Helper function for streaming comparison
void streamCompareAndWrite(const std::string& streamFilePath,
const std::unordered_set<std::string>& compareSet,
const std::string& outputTxtFilePath) {
std::lock_guard<std::mutex> lock(file_access_mutex);
FILE* streamFile = fopen(streamFilePath.c_str(), "r");
if (!streamFile) return;
FILE* outputFile = fopen(outputTxtFilePath.c_str(), "w");
if (!outputFile) {
fclose(streamFile);
return;
}
static constexpr size_t BUFFER_SIZE = 8192;
char buffer[BUFFER_SIZE];
size_t len;
while (fgets(buffer, BUFFER_SIZE, streamFile)) {
len = strlen(buffer);
if (len > 0 && buffer[len - 1] == '\n') {
buffer[len - 1] = '\0';
}
if (compareSet.count(std::string(buffer))) {
fprintf(outputFile, "%s\n", buffer);
}
}
fclose(streamFile);
fclose(outputFile);
}
// Function to compare two file lists and save duplicates to an output file
void compareFilesLists(const std::string& txtFilePath1, const std::string& txtFilePath2, const std::string& outputTxtFilePath) {
// Read files into sets
std::unordered_set<std::string> fileSet1 = readSetFromFile(txtFilePath1);
std::unordered_set<std::string> fileSet2 = readSetFromFile(txtFilePath2);
// Always work with the smaller set for better performance
if (fileSet1.size() <= fileSet2.size()) {
// fileSet1 is smaller or equal - modify it
for (auto it = fileSet1.begin(); it != fileSet1.end();) {
if (fileSet2.count(*it) == 0) {
it = fileSet1.erase(it);
} else {
++it;
}
}
writeSetToFile(fileSet1, outputTxtFilePath);
} else {
// fileSet2 is smaller - modify it instead
for (auto it = fileSet2.begin(); it != fileSet2.end();) {
if (fileSet1.count(*it) == 0) {
it = fileSet2.erase(it);
} else {
++it;
}
}
writeSetToFile(fileSet2, outputTxtFilePath);
}
}
void compareWildcardFilesLists(
const std::string& wildcardPatternFilePath,
const std::string& txtFilePath,
const std::string& outputTxtFilePath
) {
std::unordered_set<std::string> targetLines = readSetFromFile(txtFilePath);
std::unordered_set<std::string> duplicates;
auto wildcardFiles = getFilesListByWildcards(wildcardPatternFilePath);
static constexpr size_t BUFFER_SIZE = 8192;
for (auto& filePath : wildcardFiles) {
if (filePath == txtFilePath || targetLines.empty()) {
filePath = ""; // Clear early
continue;
}
std::lock_guard<std::mutex> lock(file_access_mutex);
FILE* file = fopen(filePath.c_str(), "r");
if (!file) {
#if USING_LOGGING_DIRECTIVE
logMessage(UNABLE_TO_OPEN_FILE + filePath);
#endif
continue;
}
char buffer[BUFFER_SIZE];
while (fgets(buffer, BUFFER_SIZE, file) && !targetLines.empty()) { // Early exit!
char* newlinePos = strchr(buffer, '\n');
if (newlinePos) *newlinePos = '\0';
auto it = targetLines.find(std::string(buffer));
if (it != targetLines.end()) {
duplicates.emplace(std::move(*it)); // Move instead of copy
targetLines.erase(it);
}
}
fclose(file);
filePath = ""; // Clear after processing - reduces vector memory footprint
}
writeSetToFile(duplicates, outputTxtFilePath);
}
}

View File

@@ -0,0 +1,530 @@
/********************************************************************************
* File: mod_funcs.cpp
* Author: ppkantorski
* Description:
* This source file provides the implementations of functions declared in
* mod_funcs.hpp. These functions handle the conversion of `.pchtxt` mod files
* into `.ips` binary patches used by the Ultrahand Overlay project. This includes
* parsing, validating, and encoding patch data into the IPS format.
*
* For the latest updates and contributions, visit the project's GitHub repository.
* (GitHub Repository: https://github.com/ppkantorski/Ultrahand-Overlay)
*
* Note: Please be aware that this notice cannot be altered or removed. It is a part
* of the project's documentation and must remain intact.
*
* Licensed under both GPLv2 and CC-BY-4.0
* Copyright (c) 2024-2026 ppkantorski
********************************************************************************/
#include <mod_funcs.hpp>
namespace ult {
static const std::string CHEAT_TYPE = "04000000";
static const std::string CHEAT_EXT = ".txt";
static const std::string CHEAT_ENCODING = "ascii";
/**
* @brief Checks if a cheat already exists in the cheat file.
* @param cheatFilePath The path to the cheat file.
* @param newCheat The new cheat to check.
* @return True if the cheat exists, otherwise false.
*/
bool cheatExists(const std::string& cheatFilePath, const std::string& newCheat) {
#if !USING_FSTREAM_DIRECTIVE
FILE* cheatFile = fopen(cheatFilePath.c_str(), "r"); // Open the cheat file in read mode
if (!cheatFile) {
return false; // Return false if the file cannot be opened
}
char buffer[1024]; // Buffer to store each line
while (fgets(buffer, sizeof(buffer), cheatFile)) {
// Remove newline character, if present
const size_t len = strlen(buffer);
if (len > 0 && buffer[len - 1] == '\n') {
buffer[len - 1] = '\0';
}
if (newCheat == buffer) {
fclose(cheatFile); // Close the file before returning
return true; // Cheat exists
}
}
fclose(cheatFile); // Close the file after processing
return false; // Cheat does not exist
#else
std::ifstream cheatFile(cheatFilePath);
if (!cheatFile) {
return false; // Return false if the file cannot be opened
}
std::string line;
while (std::getline(cheatFile, line)) {
if (line == newCheat) {
return true; // Cheat exists
}
}
return false; // Cheat does not exist
#endif
}
/**
* @brief Appends a new cheat to the cheat file.
* @param cheatFilePath The path to the cheat file.
* @param newCheat The new cheat to append.
*/
void appendCheatToFile(const std::string& cheatFilePath, const std::string& newCheat) {
#if !USING_FSTREAM_DIRECTIVE
FILE* cheatFile = fopen(cheatFilePath.c_str(), "a"); // Open the cheat file in append mode
if (!cheatFile) {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Failed to open cheat file for appending: " + cheatFilePath);
#endif
return; // Handle the error accordingly
}
fprintf(cheatFile, "%s\n", newCheat.c_str()); // Write the new cheat followed by a newline
fclose(cheatFile); // Close the file
#else
std::ofstream cheatFile(cheatFilePath, std::ios::app);
if (!cheatFile) {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Failed to open cheat file for appending: " + cheatFilePath);
#endif
return; // Handle the error accordingly
}
cheatFile << newCheat << std::endl; // Write the new cheat
#endif
}
/**
* @brief Extracts the cheat name from the given file path.
* @param filePath The full file path.
* @return The extracted cheat name.
*/
std::string extractCheatName(const std::string &filePath) {
const size_t lastSlash = filePath.find_last_of("/\\");
if (lastSlash == std::string::npos) {
return "";
}
const size_t secondLastSlash = filePath.find_last_of("/\\", lastSlash - 1);
if (secondLastSlash == std::string::npos) {
return "";
}
const std::string lastDir = filePath.substr(secondLastSlash + 1, lastSlash - secondLastSlash - 1);
std::string fileName = filePath.substr(lastSlash + 1);
const size_t dotPos = fileName.find_last_of('.');
if (dotPos != std::string::npos) {
fileName = fileName.substr(0, dotPos);
}
// If lastDir contains " - ", extract the part after " - "
const size_t dashPos = lastDir.find(" - ");
std::string cheatName = lastDir;
if (dashPos != std::string::npos) {
cheatName = lastDir.substr(dashPos + 3);
}
return cheatName + " " + fileName;
}
// Helper function to determine if a string is a valid title ID
bool isValidTitleID(const std::string &str) {
if (str.length() != 16) return false;
for (char c : str) {
if (!std::isxdigit(c)) return false; // Check if each character is a hexadecimal digit
}
return true;
}
// Function to find the title ID in the text, avoiding the @nsobid- line
std::string findTitleID(const std::string &text) {
const size_t nsobidPos = text.find("@nsobid-");
const size_t startPos = (nsobidPos != std::string::npos) ? nsobidPos + 40 + 8 : 0; // Skip past @nsobid- and its value
std::string potentialID;
for (size_t i = startPos; i <= text.length() - 16; ++i) {
potentialID = text.substr(i, 16);
if (isValidTitleID(potentialID)) {
return potentialID;
}
}
return "";
}
/**
* @brief Converts a .pchtxt file to a cheat file.
* @param pchtxtPath The file path to the .pchtxt file.
* @param cheatName The name of the cheat.
* @param outCheatPath The file path for the output cheat file.
* @return True if the conversion was successful, false otherwise.
*/
bool pchtxt2cheat(const std::string &pchtxtPath, std::string cheatName, std::string outCheatPath) {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Starting pchtxt2cheat with pchtxtPath: " + pchtxtPath);
#endif
FILE* pchtxtFile = fopen(pchtxtPath.c_str(), "r");
if (!pchtxtFile) {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Error: Unable to open file " + pchtxtPath);
#endif
return false;
}
// Read the entire file into a string
std::string pchtxt;
char buffer[4096];
while (fgets(buffer, sizeof(buffer), pchtxtFile)) {
pchtxt += buffer;
}
fclose(pchtxtFile);
const size_t nsobidPos = pchtxt.find("@nsobid-");
if (nsobidPos == std::string::npos) {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Error: Could not find bid in pchtxt file, the file is likely invalid.");
#endif
return false;
}
const std::string bid = pchtxt.substr(nsobidPos + 8, 40);
const std::string bidShort = bid.substr(0, 16);
const std::string tid = findTitleID(pchtxt);
if (tid.empty()) {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Error: Could not find TID in pchtxt file, the file is likely invalid.");
#endif
return false;
}
std::string cheatFilePath;
if (outCheatPath.empty()) {
const std::string folderPath = "sdmc:/atmosphere/contents/" + tid + "/cheats/";
createDirectory(folderPath);
cheatFilePath = folderPath + bidShort + CHEAT_EXT;
} else {
cheatFilePath = outCheatPath;
}
FILE* existingCheatFile = fopen(cheatFilePath.c_str(), "r");
bool cheatNameExists = false;
if (existingCheatFile) {
char line[256];
while (fgets(line, sizeof(line), existingCheatFile)) {
if (std::string(line) == "[" + cheatName + "]\n") {
cheatNameExists = true;
break;
}
}
fclose(existingCheatFile);
}
// Open output cheat file
FILE* outCheatFile = fopen(cheatFilePath.c_str(), "a");
if (!outCheatFile) {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Error: Unable to create cheat file " + cheatFilePath);
#endif
return false;
}
if (!cheatNameExists) {
fprintf(outCheatFile, "[%s]\n", cheatName.c_str());
}
int offset = 0;
bool enabled = true;
int validCheatsProcessed = 0; // ADDED: Track number of valid cheats processed
StringStream iss(pchtxt); // Use your custom StringStream
std::string line;
std::string addrStr, valStr;
size_t spacePos;
int codeOffset;
std::string cheatLine;
char offsetBuffer[9];
// Use your custom getline method instead of std::getline
while (iss.getline(line, '\n')) { // Custom getline with newline as the delimiter
// strip inline C++-style comments
const auto slashPos = line.find("//");
if (slashPos != std::string::npos)
line = line.substr(0, slashPos);
// strip inline hash comments (but leave full-line # for headers)
const auto hashPos = line.find('#');
if (hashPos != std::string::npos && hashPos > 0)
line = line.substr(0, hashPos);
trim(line);
if (line.empty() || line[0] == '#') continue;
if (line.find("@flag offset_shift ") == 0) {
const std::string offsetStr = line.substr(19);
offset = (offsetStr.find("0x") == 0 ? std::strtol(offsetStr.c_str(), nullptr, 16) : std::strtol(offsetStr.c_str(), nullptr, 10)) - 0x100;
continue;
}
if (line.find("@enabled") == 0) {
enabled = true;
continue;
}
if (line.find("@disabled") == 0) {
enabled = false;
continue;
}
if (line.find("@stop") == 0) {
break;
}
if (!enabled) {
continue;
}
spacePos = line.find(' ');
if (spacePos == std::string::npos) {
continue;
}
addrStr = line.substr(0, spacePos);
valStr = line.substr(spacePos + 1);
if (addrStr.find_first_not_of("0123456789abcdefABCDEF") != std::string::npos || valStr.find_first_not_of("0123456789abcdefABCDEF") != std::string::npos) {
continue;
}
codeOffset = std::strtol(valStr.c_str(), nullptr, 16) + offset;
snprintf(offsetBuffer, sizeof(offsetBuffer), "%08X", codeOffset);
cheatLine = CHEAT_TYPE + " " + addrStr + " " + hexToReversedHex(offsetBuffer);
// Check if cheat already exists
FILE* checkFile = fopen(cheatFilePath.c_str(), "r");
bool exists = false;
if (checkFile) {
char checkLine[256];
while (fgets(checkLine, sizeof(checkLine), checkFile)) {
if (std::string(checkLine) == cheatLine + "\n") {
exists = true;
break;
}
}
fclose(checkFile);
}
if (!exists) {
fprintf(outCheatFile, "%s\n", cheatLine.c_str());
validCheatsProcessed++; // ADDED: Increment counter for new cheats
}
}
fclose(outCheatFile);
// ADDED: Check if any valid cheats were processed
if (validCheatsProcessed == 0) {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Warning: No valid patch data found to convert to cheats in " + pchtxtPath);
#endif
return false;
}
return true;
}
/**
* @brief Converts a .pchtxt file to an IPS file using fstream.
*
* This function reads the contents of a .pchtxt file, extracts the address-value pairs,
* and generates an IPS file with the provided output folder.
*
* @param pchtxtPath The file path to the .pchtxt file.
* @param outputFolder The folder path for the output IPS file.
* @return True if the conversion was successful, false otherwise.
*/
bool pchtxt2ips(const std::string& pchtxtPath, const std::string& outputFolder) {
// Use FILE* for reading
FILE* pchtxtFile = fopen(pchtxtPath.c_str(), "r");
if (!pchtxtFile) {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Error: Unable to open file " + pchtxtPath);
#endif
return false;
}
std::vector<std::pair<uint32_t, std::vector<uint8_t>>> patches;
char lineBuffer[512]; // Fixed: Use char buffer for fgets
std::string line;
uint32_t lineNum = 0;
std::string nsobid;
int offset = 0; // Default offset
bool enabled = true;
uint32_t address;
uint8_t byte;
std::vector<uint8_t> valueBytes;
std::string offsetStr;
while (fgets(lineBuffer, sizeof(lineBuffer), pchtxtFile) != nullptr) {
line = lineBuffer; // Convert to string
++lineNum;
if (line.empty() || line.front() == '@' || !enabled) {
if (line.find("@nsobid-") == 0) {
nsobid = line.substr(8);
}
if (line.find("@flag offset_shift ") == 0) {
offsetStr = line.substr(19);
offset = (offsetStr.find("0x") == 0 ? std::strtol(offsetStr.c_str(), nullptr, 16) : std::strtol(offsetStr.c_str(), nullptr, 10));
}
if (line.find("@enabled") == 0) {
enabled = true;
continue;
}
if (line.find("@disabled") == 0) {
enabled = false;
continue;
}
if (line.find("@stop") == 0) {
break;
}
continue; // Skip empty lines and lines starting with '@'
}
StringStream iss(line);
std::string addressStr, valueStr;
if (!(iss >> addressStr >> valueStr)) {
continue;
}
char* endPtr;
address = std::strtoul(addressStr.c_str(), &endPtr, 16) + offset; // Adjust address by offset
if (*endPtr != '\0') {
continue;
}
for (size_t i = 0; i < valueStr.length(); i += 2) {
byte = ult::stoi(valueStr.substr(i, 2), nullptr, 16);
valueBytes.push_back(byte);
}
if (valueBytes.empty()) {
continue;
}
patches.push_back(std::make_pair(address, valueBytes));
valueBytes.clear();
}
fclose(pchtxtFile);
// CHECK: Return false if no patches were found
if (patches.empty()) {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Warning: No valid patches found in " + pchtxtPath);
#endif
return false;
}
if (nsobid.empty()) {
nsobid = pchtxtPath.substr(pchtxtPath.find_last_of("/\\") + 1);
nsobid = nsobid.substr(0, nsobid.find_last_of("."));
}
// Trim any newline characters from nsobid
trim(nsobid);
trimNewline(nsobid);
const std::string ipsFileName = nsobid + ".ips";
const std::string ipsFilePath = outputFolder + ipsFileName;
// Use FILE* for writing
FILE* ipsFile = fopen(ipsFilePath.c_str(), "wb");
if (!ipsFile) {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Error: Unable to create IPS file " + ipsFilePath);
#endif
return false;
}
fwrite(IPS32_HEAD_MAGIC, sizeof(char), std::strlen(IPS32_HEAD_MAGIC), ipsFile);
uint16_t valueLength;
uint32_t bigEndianAddress;
for (const auto& patch : patches) {
bigEndianAddress = toBigEndian(patch.first); // Convert address to big-endian
fwrite(&bigEndianAddress, sizeof(bigEndianAddress), 1, ipsFile); // Write address
valueLength = toBigEndian(static_cast<uint16_t>(patch.second.size())); // Convert length to big-endian
fwrite(&valueLength, sizeof(valueLength), 1, ipsFile); // Write length of value
fwrite(patch.second.data(), sizeof(uint8_t), patch.second.size(), ipsFile); // Write value
}
fwrite(IPS32_FOOT_MAGIC, sizeof(char), std::strlen(IPS32_FOOT_MAGIC), ipsFile);
fclose(ipsFile);
FILE* _pchtxtFile = fopen(pchtxtPath.c_str(), "r");
if (!_pchtxtFile) {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Error: Unable to open file " + pchtxtPath);
#endif
return false; // Changed: Return false if we can't read the file for title ID
}
// Read the entire file into a string
std::string pchtxt;
char buffer[4096];
while (fgets(buffer, sizeof(buffer), _pchtxtFile)) {
pchtxt += buffer;
}
fclose(_pchtxtFile);
const std::string tid = findTitleID(pchtxt);
if (!tid.empty()) {
const std::string tidFilePath = outputFolder + tid;
FILE* tidFile = fopen(tidFilePath.c_str(), "w");
if (tidFile) {
fclose(tidFile); // Creates an empty file
}
} else {
#if USING_LOGGING_DIRECTIVE
if (!disableLogging)
logMessage("Warning: Could not find Title ID in " + pchtxtPath);
#endif
}
return true;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,567 @@
/********************************************************************************
* File: string_funcs.cpp
* Author: ppkantorski
* Description:
* This source file provides implementations for the string manipulation
* functions declared in string_funcs.hpp. These utility functions support
* operations such as trimming whitespace, removing quotes, normalizing
* slashes, and performing other string cleanup tasks used throughout
* the Ultrahand Overlay project.
*
* For the latest updates and contributions, visit the project's GitHub repository.
* (GitHub Repository: https://github.com/ppkantorski/Ultrahand-Overlay)
*
* Note: Please be aware that this notice cannot be altered or removed. It is a part
* of the project's documentation and must remain intact.
*
* Licensed under both GPLv2 and CC-BY-4.0
* Copyright (c) 2023-2026 ppkantorski
********************************************************************************/
#include "string_funcs.hpp"
namespace ult {
// Custom string conversion methods in place of std::
std::string to_string(int value) {
char buffer[12]; // Sufficient for 32-bit int
snprintf(buffer, sizeof(buffer), "%d", value);
return std::string(buffer);
}
int stoi(const std::string& str, std::size_t* pos, int base) {
char* end;
const long result = std::strtol(str.c_str(), &end, base);
if (pos) {
*pos = end - str.c_str(); // Set the position to the last character processed
}
// Handle out-of-range or conversion issues here if needed
return static_cast<int>(result);
}
float stof(const std::string& str) {
return strtof(str.c_str(), nullptr);
}
// Mimics std::getline() with a delimiter
bool StringStream::getline(std::string& output, char delimiter) {
if (position >= data.size()) {
return false;
}
const size_t nextPos = data.find(delimiter, position);
if (nextPos != std::string::npos) {
output.assign(data, position, nextPos - position); // No temporary string creation
position = nextPos + 1;
} else {
output.assign(data, position, data.size() - position); // No temporary string creation
position = data.size();
}
return true;
}
// Mimics operator >> to split by whitespace
StringStream& StringStream::operator>>(std::string& output) {
// Skip leading whitespace
while (position < data.size() && std::isspace(data[position])) {
++position;
}
if (position >= data.size()) {
output.clear();
validState = false;
return *this;
}
size_t nextPos = position;
while (nextPos < data.size() && !std::isspace(data[nextPos])) {
++nextPos;
}
output.assign(data, position, nextPos - position); // Replace substr() with assign()
position = nextPos;
validState = true;
return *this;
}
// Overload << operator for std::string
StringStream& StringStream::operator<<(const std::string& input) {
data += input;
return *this;
}
// Overload << operator for const char*
StringStream& StringStream::operator<<(const char* input) {
data += input;
return *this;
}
// Overload << operator for char
StringStream& StringStream::operator<<(char input) {
data += input;
return *this;
}
// Overload << operator for int (handles hex mode)
StringStream& StringStream::operator<<(int input) {
if (hexMode) {
char buffer[20]; // Buffer large enough for hex conversion
sprintf(buffer, "%x", input); // Convert integer to hex string
data += buffer;
} else {
data += ult::to_string(input);
}
return *this;
}
// Define the new overload for long long
StringStream& StringStream::operator<<(long long input) {
data += std::to_string(input);
return *this;
}
// Return the current buffer content
std::string StringStream::str() const {
return data;
}
/**
* @brief Trims leading and trailing whitespaces from a string.
*
* This function removes leading and trailing whitespaces, tabs, newlines, carriage returns, form feeds,
* and vertical tabs from the input string.
*
* @param str The input string to trim.
* @return The trimmed string.
*/
void trim(std::string& str) {
const size_t first = str.find_first_not_of(" \t\n\r\f\v");
if (first == std::string::npos) {
str.clear(); // Fix: clear all-whitespace strings
return;
}
const size_t last = str.find_last_not_of(" \t\n\r\f\v");
// True in-place modification - no temporary string creation
if (last + 1 < str.length()) {
str.erase(last + 1); // Remove trailing whitespace
}
if (first > 0) {
str.erase(0, first); // Remove leading whitespace
}
}
// Function to trim newline characters from the end of a string
void trimNewline(std::string& str) {
const size_t end = str.find_last_not_of("\n");
if (end == std::string::npos) {
str.clear(); // If the string consists entirely of newlines, clear it
} else {
str.erase(end + 1); // Remove all characters after the last non-newline character
}
}
/**
* @brief Removes all white spaces from a string.
*
* This function removes all white spaces, including spaces, tabs, newlines, carriage returns, form feeds,
* and vertical tabs from the input string.
*
* @param str The input string to remove white spaces from.
* @return The string with white spaces removed.
*/
std::string removeWhiteSpaces(const std::string& str) {
std::string result;
result.reserve(str.size()); // Reserve space for the result to avoid reallocations
std::remove_copy_if(str.begin(), str.end(), std::back_inserter(result), [](unsigned char c) {
return std::isspace(c);
});
return result;
}
/**
* @brief Removes quotes from a string.
*
* This function removes single and double quotes from the beginning and end of the input string.
*
* @param str The input string to remove quotes from.
* @return The string with quotes removed.
*/
void removeQuotes(std::string& str) {
const size_t len = str.size();
if (len >= 2) {
const char front = str[0];
const char back = str[len - 1];
if ((front == '\'' && back == '\'') || (front == '"' && back == '"')) {
std::memmove(&str[0], &str[1], len - 2);
str.resize(len - 2);
}
}
}
/**
* @brief Preprocesses a path string by replacing multiple slashes and adding "sdmc:" prefix.
*
* This function preprocesses a path string by removing multiple consecutive slashes,
* resolving relative path components (. and ..), adding the "sdmc:" prefix if not present,
* and modifying the input string in place.
*
* @param path The input path string to preprocess, passed by reference.
* @param packagePath The base package path to resolve relative paths against.
*/
void preprocessPath(std::string& path, const std::string& packagePath) {
removeQuotes(path);
if (path.empty())
return;
// In-place multiple slash removal
size_t writePos = 0;
bool previousSlash = false;
for (size_t i = 0, len = path.length(); i < len; ++i) {
const char c = path[i];
if (c == '/') {
if (!previousSlash) {
path[writePos++] = '/';
previousSlash = true;
}
} else {
path[writePos++] = c;
previousSlash = false;
}
}
path.resize(writePos);
// Handle "./" replacement if present
if (!packagePath.empty() && path.length() >= 2 && path[0] == '.' && path[1] == '/') {
path.replace(0, 2, packagePath);
}
// Handle "../" sequences
size_t dotDotPos;
size_t searchEnd;
while ((dotDotPos = path.find("../")) != std::string::npos) {
// Check if there's a trailing slash before "../"
searchEnd = dotDotPos;
if (searchEnd > 0 && path[searchEnd - 1] == '/') {
--searchEnd;
}
// Find the previous slash
const size_t lastSlash = (searchEnd > 0) ? path.rfind('/', searchEnd - 1) : std::string::npos;
if (lastSlash != std::string::npos) {
// Erase from after lastSlash to after "../"
path.erase(lastSlash + 1, dotDotPos + 3 - lastSlash - 1);
} else {
// No slash found, replace with root
path.erase(0, dotDotPos);
path.insert(0, "/");
}
}
// Check for sdmc: prefix
if (path.length() < 5 ||
path[0] != 's' || path[1] != 'd' || path[2] != 'm' || path[3] != 'c' || path[4] != ':') {
path.insert(0, "sdmc:");
}
}
/**
* @brief Preprocesses a URL string by adding "https://" prefix.
*
* This function preprocesses a URL string by adding the "https://" prefix if not already present.
*
* @param path The input URL string to preprocess, passed by reference and modified in-place.
*/
void preprocessUrl(std::string& path) {
removeQuotes(path);
if (path.size() >= 7 && path[0] == 'h' && path[1] == 't' &&
path[2] == 't' && path[3] == 'p') {
if ((path.size() >= 8 && path[4] == 's' && path[5] == ':') ||
(path[4] == ':')) {
return;
}
}
path.insert(0, "https://");
}
/**
* @brief Drops the file extension from a filename.
*
* This function removes the file extension (characters after the last dot) from the input filename string.
*
* @param filename The input filename from which to drop the extension, passed by reference and modified in-place.
*/
void dropExtension(std::string& filename) {
const size_t lastDotPos = filename.rfind('.');
if (lastDotPos != std::string::npos) {
filename.resize(lastDotPos);
}
}
/**
* @brief Checks if a string starts with a given prefix.
*
* This function checks if the input string starts with the specified prefix.
*
* @param str The input string to check.
* @param prefix The prefix to check for.
* @return True if the string starts with the prefix, false otherwise.
*/
bool startsWith(const std::string& str, const std::string& prefix) {
return str.compare(0, prefix.length(), prefix) == 0;
}
// Helper function to check if a string is a valid integer
bool isValidNumber(const std::string& str) {
if (str.empty()) {
return false;
}
size_t start = 0;
if (str[0] == '-' || str[0] == '+') {
if (str.length() == 1) return false;
start = 1;
}
for (size_t i = start; i < str.length(); ++i) {
if (!std::isdigit(static_cast<unsigned char>(str[i]))) {
return false;
}
}
return true;
}
std::string returnOrNull(const std::string& value) {
return value.empty() ? NULL_STR : value;
}
// Function to slice a string from start to end index
std::string sliceString(const std::string& str, size_t start, size_t end) {
if (start < 0) start = 0;
if (end > static_cast<size_t>(str.length())) end = str.length();
if (start > end) start = end;
return str.substr(start, end - start);
}
/**
* @brief Converts a string to lowercase.
*
* This function takes a string as input and returns a lowercase version of that string.
*
* @param str The input string to convert to lowercase.
* @return The lowercase version of the input string.
*/
std::string stringToLowercase(const std::string& str) {
std::string result = str;
for (char& c : result) {
if (c >= 'A' && c <= 'Z') {
c += 32;
}
}
return result;
}
/**
* @brief Converts a string to uppercase.
*
* This function takes a string as input and returns an uppercase version of that string.
*
* @param str The input string to convert to uppercase.
* @return The uppercase version of the input string.
*/
std::string stringToUppercase(const std::string& str) {
std::string result = str;
for (char& c : result) {
if (c >= 'a' && c <= 'z') {
c -= 32;
}
}
return result;
}
/**
* @brief Formats a priority string to a desired width.
*
* This function takes a priority string and formats it to a specified desired width by padding with '0's if it's shorter
* or truncating with '9's if it's longer.
*
* @param priority The input priority string to format.
* @param desiredWidth The desired width of the formatted string (default is 4).
* @return A formatted priority string.
*/
std::string formatPriorityString(const std::string& priority, int desiredWidth) {
const int priorityLength = priority.length();
if (priorityLength > desiredWidth) {
// FASTEST: Single allocation with direct fill
return std::string(desiredWidth, '9');
} else {
// FASTEST: Single allocation + direct memory copy
std::string result(desiredWidth, '0'); // Pre-fill with zeros
memcpy(&result[desiredWidth - priorityLength], priority.data(), priorityLength);
return result;
}
}
/**
* @brief Removes the part of the string after the first occurrence of '?' character.
*
* This function takes a string and removes the portion of the string that appears after
* the first '?' character, if found. If no '?' character is present, the string remains unchanged.
*
* @param input The input string from which to remove the tag, passed by reference and modified in-place.
*/
void removeTag(std::string &input) {
const char* pos = static_cast<const char*>(
std::memchr(input.data(), '?', input.size())
);
if (pos) {
input.resize(pos - input.data());
}
}
std::string getFirstLongEntry(const std::string& input, size_t minLength) {
StringStream iss(input); // Use custom StringStream
std::string word;
// Split the input string based on spaces and get the first word
if (iss >> word) {
// Check if the first word's length is greater than the specified length
if (word.length() > minLength) {
return word;
}
}
// Return an empty string if the first word is not longer than minLength
return input;
}
// This will take a string like "v1.3.5-abasdfasdfa" and output "1.3.5". string could also look like "test-1.3.5-1" or "v1.3.5" and we will only want "1.3.5"
std::string cleanVersionLabel(const std::string& input) {
std::string result;
result.reserve(input.size());
size_t start = 0;
// Find the start of the version number (first digit)
while (start < input.size() && !std::isdigit(input[start])) {
start++;
}
if (start == input.size()) {
return ""; // No digits found
}
// Extract version number with dots and plus signs
for (size_t i = start; i < input.size(); ++i) {
const char c = input[i];
if (std::isdigit(c) || c == '.' || c == '+') {
result += c;
} else {
break; // Stop at first character that's not digit, dot, or plus
}
}
return result;
}
std::string extractTitle(const std::string& input) {
const size_t spacePos = input.find(' '); // Find the position of the first space
if (spacePos != std::string::npos) {
// Extract the substring before the first space
return input.substr(0, spacePos);
} else {
// If no space is found, return the original string
return input;
}
}
std::vector<std::string> splitString(const std::string& str, const std::string& delimiter) {
std::vector<std::string> tokens;
// OPTIMIZATION: Pre-allocate space to avoid reallocations
tokens.reserve(str.length() / (delimiter.length() + 1) + 1);
size_t start = 0;
size_t end = str.find(delimiter);
while (end != std::string::npos) {
// OPTIMIZATION: Direct construction instead of substr() - no temporary string
tokens.emplace_back(str, start, end - start);
start = end + delimiter.length();
end = str.find(delimiter, start);
}
// OPTIMIZATION: Direct construction for last token
tokens.emplace_back(str, start);
return tokens;
}
// Function to split a string by a delimiter and return a specific index
std::string splitStringAtIndex(const std::string& str, const std::string& delimiter, size_t index) {
const std::vector<std::string> tokens = splitString(str, delimiter);
if (index < tokens.size()) {
return tokens[index];
} else {
return ""; // Return empty string if index is out of bounds
}
}
std::string customAlign(int number) {
const std::string numStr = ult::to_string(number);
const int paddingSpaces = (4 - numStr.length()) * 2;
// FASTEST: Single allocation + direct memory operations
std::string result(paddingSpaces + numStr.length(), ' ');
memcpy(&result[paddingSpaces], numStr.data(), numStr.length());
return result;
}
}

File diff suppressed because it is too large Load Diff