fix: harden native recorder review paths

This commit is contained in:
EtienneLescot
2026-05-05 22:13:27 +02:00
parent 826790fe52
commit 82bffefa54
3 changed files with 56 additions and 185 deletions
+23 -3
View File
@@ -69,6 +69,19 @@ function isPathAllowed(filePath: string): boolean {
return getAllowedReadDirs().some((dir) => isPathWithinDir(resolved, dir));
}
function resolveApprovedVideoPath(videoPath?: string | null): string | null {
const normalizedPath = normalizeVideoSourcePath(videoPath);
if (!normalizedPath) {
return null;
}
if (!hasAllowedImportVideoExtension(normalizedPath) || !isPathAllowed(normalizedPath)) {
return null;
}
return normalizedPath;
}
/**
* Helper function to build dialog options with a parent window only when it's valid.
* This prevents passing stale or destroyed BrowserWindow references to dialog calls.
@@ -1363,7 +1376,7 @@ export function registerIpcHandlers(
});
ipcMain.handle("get-cursor-telemetry", async (_, videoPath?: string) => {
const targetVideoPath = normalizeVideoSourcePath(
const targetVideoPath = resolveApprovedVideoPath(
videoPath ?? currentRecordingSession?.screenVideoPath,
);
if (!targetVideoPath) {
@@ -1494,11 +1507,18 @@ export function registerIpcHandlers(
return { success: false, canceled: true };
}
approveFilePath(result.filePaths[0]);
const normalizedPath = await approveReadableVideoPath(result.filePaths[0]);
if (!normalizedPath) {
return {
success: false,
message: "Selected file is not a supported readable video file",
};
}
currentProjectPath = null;
return {
success: true,
path: result.filePaths[0],
path: normalizedPath,
};
} catch (error) {
console.error("Failed to open file picker:", error);
@@ -6,7 +6,7 @@
#include <algorithm>
#include <chrono>
#include <cwctype>
#include <exception>
#include <iostream>
namespace {
@@ -43,115 +43,6 @@ bool succeeded(HRESULT hr, const char* label) {
return false;
}
std::wstring readPropertyString(IPropertyBag* bag, LPCOLESTR key) {
VARIANT value;
VariantInit(&value);
if (FAILED(bag->Read(key, &value, nullptr)) || value.vt != VT_BSTR || !value.bstrVal) {
VariantClear(&value);
return {};
}
std::wstring result(value.bstrVal);
VariantClear(&value);
return result;
}
bool containsInsensitive(const std::wstring& haystack, const std::wstring& needle) {
if (haystack.empty() || needle.empty()) {
return false;
}
std::wstring lowerHaystack = haystack;
std::wstring lowerNeedle = needle;
std::transform(lowerHaystack.begin(), lowerHaystack.end(), lowerHaystack.begin(), ::towlower);
std::transform(lowerNeedle.begin(), lowerNeedle.end(), lowerNeedle.begin(), ::towlower);
return lowerHaystack.find(lowerNeedle) != std::wstring::npos ||
lowerNeedle.find(lowerHaystack) != std::wstring::npos;
}
std::wstring normalizeDeviceName(const std::wstring& value) {
std::wstring normalized;
normalized.reserve(value.size());
bool lastWasSpace = true;
for (const wchar_t ch : value) {
if (std::iswalnum(ch)) {
normalized.push_back(static_cast<wchar_t>(std::towlower(ch)));
lastWasSpace = false;
continue;
}
if (!lastWasSpace) {
normalized.push_back(L' ');
lastWasSpace = true;
}
}
while (!normalized.empty() && normalized.back() == L' ') {
normalized.pop_back();
}
return normalized;
}
std::vector<std::wstring> splitWords(const std::wstring& value) {
std::vector<std::wstring> words;
size_t start = 0;
while (start < value.size()) {
const size_t end = value.find(L' ', start);
const auto word = value.substr(start, end == std::wstring::npos ? std::wstring::npos : end - start);
if (word.size() > 1 && word != L"camera" && word != L"webcam" && word != L"video" && word != L"input") {
words.push_back(word);
}
if (end == std::wstring::npos) {
break;
}
start = end + 1;
}
return words;
}
int deviceMatchScore(
const std::wstring& candidateName,
const std::wstring& candidatePath,
const std::wstring& requestedName,
const std::wstring& requestedId) {
int score = 0;
const auto normalizedName = normalizeDeviceName(candidateName);
const auto normalizedPath = normalizeDeviceName(candidatePath);
const auto normalizedRequestedName = normalizeDeviceName(requestedName);
const auto normalizedRequestedId = normalizeDeviceName(requestedId);
if (!normalizedRequestedName.empty()) {
if (normalizedName == normalizedRequestedName) {
score = std::max(score, 1000);
}
if (containsInsensitive(normalizedName, normalizedRequestedName)) {
score = std::max(score, 900);
}
if (containsInsensitive(normalizedPath, normalizedRequestedName)) {
score = std::max(score, 800);
}
int wordScore = 0;
for (const auto& word : splitWords(normalizedRequestedName)) {
if (normalizedName.find(word) != std::wstring::npos) {
wordScore += 100;
} else if (normalizedPath.find(word) != std::wstring::npos) {
wordScore += 50;
}
}
score = std::max(score, wordScore);
}
if (!normalizedRequestedId.empty()) {
if (containsInsensitive(normalizedPath, normalizedRequestedId)) {
score = std::max(score, 700);
}
if (containsInsensitive(normalizedName, normalizedRequestedId)) {
score = std::max(score, 600);
}
}
return score;
}
void freeMediaType(AM_MEDIA_TYPE& type) {
if (type.cbFormat != 0) {
CoTaskMemFree(type.pbFormat);
@@ -164,70 +55,6 @@ void freeMediaType(AM_MEDIA_TYPE& type) {
}
}
bool readRegistryString(HKEY key, const wchar_t* valueName, std::wstring& value) {
DWORD type = 0;
DWORD size = 0;
if (RegGetValueW(key, nullptr, valueName, RRF_RT_REG_SZ, &type, nullptr, &size) != ERROR_SUCCESS || size == 0) {
return false;
}
std::wstring buffer(size / sizeof(wchar_t), L'\0');
if (RegGetValueW(key, nullptr, valueName, RRF_RT_REG_SZ, &type, buffer.data(), &size) != ERROR_SUCCESS) {
return false;
}
while (!buffer.empty() && buffer.back() == L'\0') {
buffer.pop_back();
}
value = buffer;
return true;
}
bool findRegisteredVideoInput(
const std::wstring& deviceId,
const std::wstring& deviceName,
CLSID& selectedClsid,
std::wstring& selectedName,
int& bestScore) {
HKEY instanceKey = nullptr;
if (RegOpenKeyExW(
HKEY_CLASSES_ROOT,
L"CLSID\\{860BB310-5D01-11D0-BD3B-00A0C911CE86}\\Instance",
0,
KEY_READ,
&instanceKey) != ERROR_SUCCESS) {
return false;
}
DWORD index = 0;
wchar_t subkeyName[128];
DWORD subkeyNameLength = ARRAYSIZE(subkeyName);
bool found = false;
while (RegEnumKeyExW(instanceKey, index, subkeyName, &subkeyNameLength, nullptr, nullptr, nullptr, nullptr) == ERROR_SUCCESS) {
HKEY filterKey = nullptr;
if (RegOpenKeyExW(instanceKey, subkeyName, 0, KEY_READ, &filterKey) == ERROR_SUCCESS) {
std::wstring friendlyName;
std::wstring clsidText;
readRegistryString(filterKey, L"FriendlyName", friendlyName);
readRegistryString(filterKey, L"CLSID", clsidText);
const int score = deviceMatchScore(friendlyName, clsidText, deviceName, deviceId);
std::wcerr << L"INFO: Registered DirectShow webcam candidate name=\"" << friendlyName << L"\" score=" << score << std::endl;
CLSID clsid{};
if (!clsidText.empty() && SUCCEEDED(CLSIDFromString(clsidText.c_str(), &clsid)) && (!found || score > bestScore)) {
selectedClsid = clsid;
selectedName = friendlyName;
bestScore = score;
found = true;
}
RegCloseKey(filterKey);
}
index += 1;
subkeyNameLength = ARRAYSIZE(subkeyName);
}
RegCloseKey(instanceKey);
return found;
}
} // namespace
struct DirectShowWebcamCapture::Impl {
@@ -254,6 +81,7 @@ bool DirectShowWebcamCapture::initialize(
int requestedWidth,
int requestedHeight,
int requestedFps) {
(void)deviceId;
stop();
delete impl_;
impl_ = nullptr;
@@ -371,9 +199,21 @@ bool DirectShowWebcamCapture::start() {
if (!succeeded(hr, "Run(DirectShow webcam)")) {
return false;
}
impl_->running = true;
stopRequested_ = false;
thread_ = std::thread(&DirectShowWebcamCapture::captureLoop, this);
try {
thread_ = std::thread(&DirectShowWebcamCapture::captureLoop, this);
} catch (const std::exception& error) {
stopRequested_ = true;
impl_->mediaControl->Stop();
std::cerr << "ERROR: Failed to start DirectShow webcam capture thread: " << error.what() << std::endl;
return false;
} catch (...) {
stopRequested_ = true;
impl_->mediaControl->Stop();
std::cerr << "ERROR: Failed to start DirectShow webcam capture thread" << std::endl;
return false;
}
impl_->running = true;
return true;
}
+17 -6
View File
@@ -402,24 +402,30 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
}
activeNativeRecording.finalizing = true;
nativeWindowsRecording.current = null;
setRecording(false);
setPaused(false);
setElapsedSeconds(0);
accumulatedDurationMs.current = 0;
segmentStartedAt.current = null;
const clearNativeRecordingState = () => {
nativeWindowsRecording.current = null;
setRecording(false);
setPaused(false);
setElapsedSeconds(0);
accumulatedDurationMs.current = 0;
segmentStartedAt.current = null;
};
try {
const result = await window.electronAPI.stopNativeWindowsRecording(discard);
if (discard || result.discarded) {
clearNativeRecordingState();
return true;
}
if (!result.success) {
console.error("Failed to stop native Windows recording:", result.error);
toast.error(result.error ?? "Failed to stop native Windows recording");
activeNativeRecording.finalizing = false;
return true;
}
clearNativeRecordingState();
if (result.session) {
await window.electronAPI.setCurrentRecordingSession(result.session);
} else if (result.path) {
@@ -433,6 +439,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
toast.error(
error instanceof Error ? error.message : "Failed to save native Windows recording",
);
activeNativeRecording.finalizing = false;
return true;
} finally {
if (discardRecordingId.current === activeNativeRecording.recordingId) {
@@ -582,6 +589,10 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
const availability = await window.electronAPI.isNativeWindowsCaptureAvailable();
if (!availability.success || !availability.available) {
if (availability.reason === "unsupported-os") {
return false;
}
throw new Error(
availability.reason === "missing-helper"
? "Native Windows capture helper is not available."