sysclk: remove sysclk old and libultrahand old
This commit is contained in:
@@ -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: 0–256 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
|
||||
@@ -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(¤tTime);
|
||||
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
|
||||
}
|
||||
@@ -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(¤t_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 '/',
|
||||
// you’d 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 libnx’s 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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
Reference in New Issue
Block a user