const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const AdmZip = require('adm-zip'); const CryptoUtils = require('./crypto-utils'); class Patcher { constructor(appDataPath) { this.appDataPath = appDataPath; } getCurrentVersion() { const versionFile = path.join(this.appDataPath, 'version.json'); if (fs.existsSync(versionFile)) { try { return JSON.parse(fs.readFileSync(versionFile, 'utf8')).version || '0.0.0'; } catch (e) { return '0.0.0'; } } return '0.0.0'; } getCurrentSha() { const versionFile = path.join(this.appDataPath, 'version.json'); if (fs.existsSync(versionFile)) { try { return JSON.parse(fs.readFileSync(versionFile, 'utf8')).sha256 || null; } catch (e) { return null; } } return null; } bridgeCredentials(credentialManager) { const encryptedPath = path.join(this.appDataPath, 'encrypted-credentials.json'); if (!fs.existsSync(encryptedPath)) { return false; } const buildKey = CryptoUtils.deriveKey('vyla-home-build-key', 'vyla-home-payload-salt-v1'); const encryptedData = fs.readFileSync(encryptedPath, 'utf8'); const credentials = CryptoUtils.decryptObject(encryptedData, buildKey); for (const [service, keys] of Object.entries(credentials)) { credentialManager.setServiceCredentials(service, keys); } fs.unlinkSync(encryptedPath); return true; } saveCurrentVersion(version, sha256 = null) { const versionFile = path.join(this.appDataPath, 'version.json'); fs.mkdirSync(this.appDataPath, { recursive: true }); fs.writeFileSync(versionFile, JSON.stringify({ version, sha256 }, null, 2), 'utf8'); } purgeOldCode() { const dirs = ['frontend', 'player', 'stream-api', 'auth-proxy', 'config']; for (const dir of dirs) { const fullPath = path.join(this.appDataPath, dir); if (fs.existsSync(fullPath)) { try { const trashPath = path.join(this.appDataPath, `.trash_${dir}_${Date.now()}`); fs.renameSync(fullPath, trashPath); fs.rmSync(trashPath, { recursive: true, force: true }); } catch (e) { try { fs.rmSync(fullPath, { recursive: true, force: true }); } catch (err2) { } } } } try { const files = fs.readdirSync(this.appDataPath); for (const file of files) { if (file.startsWith('.trash_')) { fs.rmSync(path.join(this.appDataPath, file), { recursive: true, force: true }); } } } catch (e) { } } extractZipPayload(zipPath) { const AdmZip = require('adm-zip'); const zip = new AdmZip(zipPath); const entries = zip.getEntries(); if (!entries || entries.length === 0) { throw new Error('Downloaded archive is empty, invalid, or truncated.'); } zip.extractAllTo(this.appDataPath, true); const testPath = path.join(this.appDataPath, 'frontend', 'index.html'); if (!fs.existsSync(testPath)) { const { execSync } = require('child_process'); if (process.platform === 'win32') { execSync(`powershell.exe -NoProfile -NonInteractive -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${this.appDataPath}' -Force"`, { windowsHide: true }); } else { execSync(`unzip -o "${zipPath}" -d "${this.appDataPath}"`); } } } verifyChecksum(zipPath, expectedSha256) { if (!expectedSha256) return true; const hash = crypto.createHash('sha256'); const data = fs.readFileSync(zipPath); hash.update(data); const calculated = hash.digest('hex'); return calculated === expectedSha256; } verifyExtraction() { const requiredFiles = [ 'frontend/index.html', 'player/server.js' ]; for (const file of requiredFiles) { const fullPath = path.join(this.appDataPath, file); if (!fs.existsSync(fullPath)) { return false; } const stat = fs.statSync(fullPath); if (stat.size === 0) { return false; } try { const sampleSize = Math.min(stat.size, 4096); const fd = fs.openSync(fullPath, 'r'); const buffer = Buffer.alloc(sampleSize); fs.readSync(fd, buffer, 0, sampleSize, 0); fs.closeSync(fd); if (buffer.every(byte => byte === 0)) { return false; } } catch (e) { return false; } } return true; } processDirectory(dir, replacementList = []) { const files = fs.readdirSync(dir); for (const file of files) { const fullPath = path.join(dir, file); const stat = fs.statSync(fullPath); if (stat.isDirectory()) { if (file === 'node_modules' || file === '.git') continue; this.processDirectory(fullPath, replacementList); } else if (stat.isFile()) { const ext = path.extname(fullPath).toLowerCase(); let content = fs.readFileSync(fullPath, 'utf8'); let modified = false; for (const rep of replacementList) { if (content.includes(rep.find)) { content = content.split(rep.find).join(rep.replace); modified = true; } } if (modified) { fs.writeFileSync(fullPath, content, 'utf8'); } } } } applyPatches(replacements) { const targetDirs = ['frontend', 'player']; for (const dirName of targetDirs) { const dirPath = path.join(this.appDataPath, dirName); if (!fs.existsSync(dirPath)) continue; const replacementList = replacements[dirName] || []; this.processDirectory(dirPath, replacementList); } } } module.exports = Patcher;