Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37158c1ff0 | ||
|
|
02f81327fd | ||
|
|
2834510680 |
No files matched your search
+5
-1
@@ -35,7 +35,8 @@ foreach(utilityTarget StayHere StayHereTest)
|
||||
target_link_options(${utilityTarget} PRIVATE -municode -static -static-libgcc -static-libstdc++)
|
||||
endif()
|
||||
endforeach()
|
||||
add_library(kscore src/lan_channel.cpp src/lan_input.cpp src/lan.cpp src/lan_pairing.cpp src/core.cpp src/platform.cpp src/autostart.cpp src/autocorrect.cpp src/storage.cpp src/ocr.cpp src/richtext.cpp src/update.cpp src/sync.cpp)
|
||||
add_library(kscore src/lan_files.cpp src/lan_channel.cpp src/lan_input.cpp src/lan.cpp src/lan_pairing.cpp src/core.cpp src/platform.cpp src/autostart.cpp src/autocorrect.cpp src/storage.cpp src/ocr.cpp src/richtext.cpp src/update.cpp src/sync.cpp)
|
||||
target_link_libraries(KSwitcherFiles PRIVATE kscore)
|
||||
target_include_directories(kscore PUBLIC src PRIVATE vendor)
|
||||
target_compile_definitions(kscore PUBLIC UNICODE _UNICODE WIN32_LEAN_AND_MEAN NOMINMAX _WIN32_WINNT=0x0A00)
|
||||
target_link_libraries(kscore PUBLIC ws2_32 bcrypt iphlpapi winhttp user32 shell32 shlwapi advapi32 crypt32 imm32 comctl32 gdi32 ole32 oleaut32 taskschd comdlg32 uuid windowscodecs runtimeobject uiautomationcore)
|
||||
@@ -113,6 +114,9 @@ else()
|
||||
target_link_options(KSwitcherLanTests PRIVATE -static -static-libgcc -static-libstdc++)
|
||||
endif()
|
||||
add_test(NAME lan-pairing-regressions COMMAND KSwitcherLanTests)
|
||||
add_executable(KSwitcherLanFilesTests tests/lan_files.cpp)
|
||||
target_link_libraries(KSwitcherLanFilesTests PRIVATE kscore)
|
||||
add_test(NAME lan-files-regressions COMMAND KSwitcherLanFilesTests)
|
||||
add_executable(KSwitcherLanInputTests tests/lan_input.cpp)
|
||||
target_link_libraries(KSwitcherLanInputTests PRIVATE kscore)
|
||||
if(MSVC)
|
||||
|
||||
@@ -4,6 +4,12 @@ Far Manager є зразком для оформлення файлового м
|
||||
|
||||
## Панелі
|
||||
|
||||
- Меню Change drive показує літеру, тип, загальний обсяг і вільне місце. Відкриття меню запускає нове LAN-виявлення. Секція KSwitcher LAN показує збережені пари, зокрема офлайн. Після кожного відкриття меню асинхронно запитує диски й показує їх під назвою відповідного ПК. Поки триває запит, видно стан завантаження. Якщо запит не вдався, видно помилку. Повторне відкриття запускає новий запит. Кожна панель має власний диск і шлях, а в заголовку віддаленої панелі перед шляхом показано назву ПК.
|
||||
|
||||
- Список дисків і файлові операції використовують права облікового запису Windows, під яким працює віддалений KSwitcher. Достатньо спарювання й доступного TCP 45885, спільні папки налаштовувати не потрібно.
|
||||
- F5, F6, F8 і Shift+Delete у віддалених панелях використовують спільні підтвердження, попередження й прогрес. F5/F6 працюють із файлами та деревами папок між локальними й віддаленими панелями, зокрема між двома різними ПК. Між шляхами одного віддаленого ПК вміст копіюється на ньому. F8 використовує кошик того ПК, на якому лежить файл.
|
||||
- Після розриву з’єднання повтори F5 або F6 до того самого призначення. Віддалене копіювання відновлюється з підтвердженої позиції, якщо збережені байти відповідають початку вихідного файла. Підтверджене скасування видаляє частковий запис поточного файла. Завершені файли лишаються на місці. Наявний файл призначення замінюється після повного запису, а при переміщенні джерело видаляється після підтвердження запису призначення.
|
||||
|
||||
- Після F5 або F6 успішно оброблені елементи коротко підсвічуються в панелі, що показує папку призначення. Підсвітка тексту відрізняється від курсора й не переносить фокус.
|
||||
|
||||
- У широкому режимі ширина під вирівняні розширення залежить від найдовшого розширення в поточній панелі. За вузької панелі залишай місце для імені та колонки Size.
|
||||
@@ -48,6 +54,7 @@ Far Manager є зразком для оформлення файлового м
|
||||
## Видалення
|
||||
|
||||
- F8 показує сіре підтвердження з дією Move. Shift+Delete і підтвердження непорожньої папки мають темно-червоне тло. Вікно прогресу видалення має сіре тло, як у Copy.
|
||||
- Escape у початковому підтвердженні повертає фокус до панелі, з якої викликано видалення. Поточний рядок зберігається, а стрілки одразу працюють.
|
||||
- F8 переміщує підтверджені елементи в кошик Windows. Shift+Delete видаляє підтверджене дерево від вкладених елементів до батьківської папки. Прогрес і підсумок окремо показують успішні, пропущені й невдалі дії.
|
||||
- Cancel і Esc у вікні прогресу відкривають спільне червоне підтвердження скасування. Yes зупиняє операцію, а No або Esc у підтвердженні повертає до прогресу. Початковий фокус стоїть на No.
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ try {
|
||||
& $compiler @common -Werror -mwindows -municode -static -s utilities/StayHere/main.cpp -luser32 -lshell32 -lgdi32 -lwtsapi32 -o $stayHere
|
||||
if ($LASTEXITCODE) { throw 'StayHere build failed' }
|
||||
Copy-Item -LiteralPath $stayHere -Destination build/release/tools/StayHere.exe -Force
|
||||
foreach ($name in @('lan_channel','lan_input','lan','lan_pairing','core','platform','autostart','autocorrect','storage','sync','ocr','richtext','update','screenshot','screenshot_main','screenshot_notifications','screenshot_activation','screenshot_launcher','app','ui','main','configuration','config_main')) {
|
||||
foreach ($name in @('lan_files','lan_channel','lan_input','lan','lan_pairing','core','platform','autostart','autocorrect','storage','sync','ocr','richtext','update','screenshot','screenshot_main','screenshot_notifications','screenshot_activation','screenshot_launcher','app','ui','main','configuration','config_main')) {
|
||||
& $compiler @common -c "src/$name.cpp" -o "build/$name.o"
|
||||
if ($LASTEXITCODE) { throw "Compilation failed: $name" }
|
||||
}
|
||||
@@ -35,15 +35,15 @@ try {
|
||||
& windres --codepage=65001 -DKS_SCREENSHOT_RESOURCES resources/app.rc -O coff -o build/screenshot-res.o
|
||||
if ($LASTEXITCODE) { throw 'Screenshot resource compilation failed' }
|
||||
New-Item -ItemType Directory -Force build/release | Out-Null
|
||||
& $compiler -mwindows -municode -static -s '-Wl,--gc-sections' build/lan_channel.o build/lan_input.o build/lan.o build/lan_pairing.o build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/sync.o build/ocr.o build/richtext.o build/update.o build/screenshot_launcher.o build/screenshot_activation.o build/screenshot_notifications.o build/app.o build/configuration.o build/ui-resident.o build/main.o build/app-res.o @libs -o $executablePath
|
||||
& $compiler -mwindows -municode -static -s '-Wl,--gc-sections' build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/sync.o build/ocr.o build/richtext.o build/update.o build/screenshot_launcher.o build/screenshot_activation.o build/screenshot_notifications.o build/app.o build/configuration.o build/ui-resident.o build/main.o build/app-res.o @libs -o $executablePath
|
||||
if ($LASTEXITCODE) { throw 'Link failed' }
|
||||
Copy-Item -LiteralPath $executablePath -Destination build/release/KSwitcher.exe -Force
|
||||
$configPath = Join-Path (Split-Path $executablePath) 'KSwitcher.Config.exe'
|
||||
& $compiler -mwindows -municode -static -s '-Wl,--gc-sections' build/lan_channel.o build/lan_input.o build/lan.o build/lan_pairing.o build/core.o build/platform.o build/storage.o build/sync.o build/ocr.o build/configuration.o build/config_main.o build/ui-config.o build/app-res.o @libs -o $configPath
|
||||
& $compiler -mwindows -municode -static -s '-Wl,--gc-sections' build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/storage.o build/sync.o build/ocr.o build/configuration.o build/config_main.o build/ui-config.o build/app-res.o @libs -o $configPath
|
||||
if ($LASTEXITCODE) { throw 'Configurator link failed' }
|
||||
Copy-Item -LiteralPath $configPath -Destination build/release/KSwitcher.Config.exe -Force
|
||||
$screenshotPath = Join-Path (Split-Path $executablePath) 'KSwitcher.Screenshot.exe'
|
||||
& $compiler -mwindows -municode -static -s '-Wl,--gc-sections' build/lan_channel.o build/lan_input.o build/lan.o build/lan_pairing.o build/core.o build/platform.o build/storage.o build/ocr.o build/screenshot.o build/screenshot_notifications.o build/screenshot_activation.o build/screenshot_main.o build/screenshot-res.o @libs -o $screenshotPath
|
||||
& $compiler -mwindows -municode -static -s '-Wl,--gc-sections' build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/storage.o build/ocr.o build/screenshot.o build/screenshot_notifications.o build/screenshot_activation.o build/screenshot_main.o build/screenshot-res.o @libs -o $screenshotPath
|
||||
if ($LASTEXITCODE) { throw 'Screenshot link failed' }
|
||||
Copy-Item -LiteralPath $screenshotPath -Destination build/release/KSwitcher.Screenshot.exe -Force
|
||||
$filesPath = Join-Path (Split-Path $executablePath) 'KSwitcher.Files.exe'
|
||||
@@ -51,7 +51,7 @@ try {
|
||||
& "$root/tools/stage-files-terminal.ps1" -Destination build/release | Out-Null
|
||||
& windres resources/files.rc -O coff -o build/files-res.o
|
||||
if ($LASTEXITCODE) { throw 'File manager resource compilation failed' }
|
||||
& $compiler @common "-I$terminalIncludes" -mwindows -municode -static -s src/files_main.cpp build/files-res.o -luser32 -lgdi32 -lcomctl32 -lshell32 -lcomdlg32 -ladvapi32 -lole32 -luuid -o $filesPath
|
||||
& $compiler @common "-I$terminalIncludes" -mwindows -municode -static -s src/files_main.cpp build/files-res.o build/lan_files.o build/lan_channel.o build/lan_pairing.o build/platform.o build/core.o '-Wl,--gc-sections' @libs -o $filesPath
|
||||
if ($LASTEXITCODE) { throw 'File manager build failed' }
|
||||
Copy-Item -LiteralPath $filesPath -Destination build/release/KSwitcher.Files.exe -Force
|
||||
if ($Test) {
|
||||
@@ -67,10 +67,14 @@ try {
|
||||
if ($LASTEXITCODE) { throw 'LAN input test build failed' }
|
||||
& .\build\KSwitcherLanInputTests.exe
|
||||
if ($LASTEXITCODE) { throw 'LAN input tests failed' }
|
||||
& $compiler @common -Werror tests/lan.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_pairing.o build/core.o build/platform.o @libs -static -s -o build/KSwitcherLanTests.exe
|
||||
& $compiler @common -Werror tests/lan.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o @libs -static -s -o build/KSwitcherLanTests.exe
|
||||
if ($LASTEXITCODE) { throw 'LAN test build failed' }
|
||||
& .\build\KSwitcherLanTests.exe
|
||||
if ($LASTEXITCODE) { throw 'LAN pairing tests failed' }
|
||||
& $compiler @common -Werror tests/lan_files.cpp build/lan_files.o build/lan_channel.o build/lan_pairing.o build/core.o build/platform.o @libs -static -s -o build/KSwitcherLanFilesTests.exe
|
||||
if ($LASTEXITCODE) { throw 'LAN file test build failed' }
|
||||
& .\build\KSwitcherLanFilesTests.exe
|
||||
if ($LASTEXITCODE) { throw 'LAN file tests failed' }
|
||||
& $compiler @common -Werror -DSTAYHERE_TEST_INSTANCE -mwindows -municode -static -s utilities/StayHere/main.cpp -luser32 -lshell32 -lgdi32 -lwtsapi32 -o build/StayHereTest.exe
|
||||
if ($LASTEXITCODE) { throw 'StayHere fixture build failed' }
|
||||
& "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File tests/stayhere.ps1 -Executable (Join-Path $outDir 'StayHereTest.exe')
|
||||
@@ -79,11 +83,11 @@ try {
|
||||
if ($LASTEXITCODE) { throw 'Utilities test build failed' }
|
||||
& .\build\KSwitcherUtilitiesTests.exe
|
||||
if ($LASTEXITCODE) { throw 'Utilities tests failed' }
|
||||
& $compiler @common tests/dictionary.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_pairing.o build/core.o build/platform.o build/autocorrect.o @libs -lpsapi -static -s -o build/KSwitcherDictionaryTests.exe
|
||||
& $compiler @common tests/dictionary.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/autocorrect.o @libs -lpsapi -static -s -o build/KSwitcherDictionaryTests.exe
|
||||
if ($LASTEXITCODE) { throw 'Dictionary test build failed' }
|
||||
& .\build\KSwitcherDictionaryTests.exe
|
||||
if ($LASTEXITCODE) { throw 'Dictionary tests failed' }
|
||||
& $compiler @common tests/sync.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_pairing.o build/core.o build/platform.o build/storage.o build/sync.o build/ocr.o @libs -static -s -o build/KSwitcherSyncTests.exe
|
||||
& $compiler @common tests/sync.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/storage.o build/sync.o build/ocr.o @libs -static -s -o build/KSwitcherSyncTests.exe
|
||||
if ($LASTEXITCODE) { throw 'Sync test build failed' }
|
||||
& .\build\KSwitcherSyncTests.exe
|
||||
if ($LASTEXITCODE) { throw 'Sync tests failed' }
|
||||
@@ -91,19 +95,19 @@ try {
|
||||
if ($LASTEXITCODE) { throw 'Scroll test build failed' }
|
||||
& .\build\KSwitcherScrollTests.exe
|
||||
if ($LASTEXITCODE) { throw 'Scroll tests failed' }
|
||||
& $compiler @common tests/tests.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_pairing.o build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/sync.o build/ocr.o build/richtext.o build/update.o @libs -static -s -o build/KSwitcherTests.exe
|
||||
& $compiler @common tests/tests.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/sync.o build/ocr.o build/richtext.o build/update.o @libs -static -s -o build/KSwitcherTests.exe
|
||||
if ($LASTEXITCODE) { throw 'Test build failed' }
|
||||
& .\build\KSwitcherTests.exe
|
||||
if ($LASTEXITCODE) { throw 'Tests failed' }
|
||||
& $compiler @common tests/input.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_pairing.o build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/sync.o build/ocr.o build/richtext.o build/update.o build/screenshot.o build/screenshot_launcher.o build/screenshot_activation.o build/screenshot_notifications.o build/app.o build/configuration.o build/ui.o build/screenshot-res.o @libs -static -s -o build/KSwitcherInputTests.exe
|
||||
& $compiler @common tests/input.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/sync.o build/ocr.o build/richtext.o build/update.o build/screenshot.o build/screenshot_launcher.o build/screenshot_activation.o build/screenshot_notifications.o build/app.o build/configuration.o build/ui.o build/screenshot-res.o @libs -static -s -o build/KSwitcherInputTests.exe
|
||||
if ($LASTEXITCODE) { throw 'Input test build failed' }
|
||||
& .\build\KSwitcherInputTests.exe
|
||||
if ($LASTEXITCODE) { throw 'Input tests failed' }
|
||||
& $compiler @common tests/chat_profiles.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_pairing.o build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/sync.o build/ocr.o build/richtext.o build/update.o build/screenshot.o build/screenshot_launcher.o build/screenshot_activation.o build/screenshot_notifications.o build/app.o build/configuration.o build/ui.o build/screenshot-res.o @libs -static -s -o build/KSwitcherChatProfileTests.exe
|
||||
& $compiler @common tests/chat_profiles.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/sync.o build/ocr.o build/richtext.o build/update.o build/screenshot.o build/screenshot_launcher.o build/screenshot_activation.o build/screenshot_notifications.o build/app.o build/configuration.o build/ui.o build/screenshot-res.o @libs -static -s -o build/KSwitcherChatProfileTests.exe
|
||||
if ($LASTEXITCODE) { throw 'Chat profile test build failed' }
|
||||
& .\build\KSwitcherChatProfileTests.exe
|
||||
if ($LASTEXITCODE) { throw 'Chat profile tests failed' }
|
||||
& $compiler @common tests/screenshot_ui.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_pairing.o build/core.o build/platform.o build/ocr.o build/screenshot-res.o @libs -static -s -o build/KSwitcherScreenshotUITests.exe
|
||||
& $compiler @common tests/screenshot_ui.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/ocr.o build/screenshot-res.o @libs -static -s -o build/KSwitcherScreenshotUITests.exe
|
||||
if ($LASTEXITCODE) { throw 'Screenshot UI test build failed' }
|
||||
& .\build\KSwitcherScreenshotUITests.exe
|
||||
if ($LASTEXITCODE) { throw 'Screenshot UI tests failed' }
|
||||
@@ -111,7 +115,7 @@ try {
|
||||
if ($LASTEXITCODE) { throw 'Screenshot save tests failed' }
|
||||
$registrationTest = Join-Path (Split-Path $executablePath) 'KSwitcherRegistrationTests.exe'
|
||||
try {
|
||||
& $compiler @common tests/screenshot_registration.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_pairing.o build/core.o build/platform.o @libs -static '-Wl,--gc-sections' -s -o $registrationTest
|
||||
& $compiler @common tests/screenshot_registration.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o @libs -static '-Wl,--gc-sections' -s -o $registrationTest
|
||||
if ($LASTEXITCODE) { throw 'Screenshot registration test build failed' }
|
||||
& $registrationTest
|
||||
if ($LASTEXITCODE) { throw 'Screenshot registration tests failed' }
|
||||
@@ -123,7 +127,7 @@ try {
|
||||
if ($LASTEXITCODE) { throw 'Updater progress UI tests failed' }
|
||||
}
|
||||
if ($Far) {
|
||||
& $compiler @common tests/far_snippets.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_pairing.o build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/sync.o build/ocr.o build/richtext.o build/update.o build/screenshot.o build/screenshot_launcher.o build/screenshot_activation.o build/screenshot_notifications.o build/app.o build/configuration.o build/ui.o build/screenshot-res.o @libs -mwindows -municode -static -s -o build/KSwitcherFarSnippetTests.exe
|
||||
& $compiler @common tests/far_snippets.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/sync.o build/ocr.o build/richtext.o build/update.o build/screenshot.o build/screenshot_launcher.o build/screenshot_activation.o build/screenshot_notifications.o build/app.o build/configuration.o build/ui.o build/screenshot-res.o @libs -mwindows -municode -static -s -o build/KSwitcherFarSnippetTests.exe
|
||||
if ($LASTEXITCODE) { throw 'Far snippet test build failed' }
|
||||
$fixture = Start-Process -FilePath (Join-Path $outDir 'KSwitcherFarSnippetTests.exe') -WindowStyle Hidden -Wait -PassThru
|
||||
Get-Content -LiteralPath (Join-Path $outDir 'far-snippets.log')
|
||||
@@ -135,28 +139,28 @@ try {
|
||||
Copy-Item $executablePath,(Join-Path (Split-Path $executablePath) 'KSwitcher.Screenshot.exe') -Destination $historyFixture
|
||||
Set-Content (Join-Path $historyFixture '.local-dev') 'Isolated history integration test'
|
||||
$historyTest = Join-Path $historyFixture 'KSwitcherHistoryHostTests.exe'
|
||||
& $compiler @common tests/history_host.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_pairing.o build/core.o build/platform.o build/storage.o build/ocr.o @libs -lpsapi -static -s -o $historyTest
|
||||
& $compiler @common tests/history_host.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/storage.o build/ocr.o @libs -lpsapi -static -s -o $historyTest
|
||||
if ($LASTEXITCODE) { throw 'History host test build failed' }
|
||||
& $historyTest
|
||||
if ($LASTEXITCODE) { throw 'History host tests failed' }
|
||||
$snapshotTest = Join-Path (Split-Path $executablePath) 'KSwitcherClipboardSnapshotTests.exe'
|
||||
& $compiler @common tests/clipboard_snapshot.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_pairing.o build/core.o build/platform.o build/storage.o build/ocr.o @libs -lpsapi -static -s -o $snapshotTest
|
||||
& $compiler @common tests/clipboard_snapshot.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/storage.o build/ocr.o @libs -lpsapi -static -s -o $snapshotTest
|
||||
if ($LASTEXITCODE) { throw 'Clipboard snapshot test build failed' }
|
||||
& $snapshotTest
|
||||
if ($LASTEXITCODE) { throw 'Clipboard snapshot tests failed' }
|
||||
$captureTest = Join-Path (Split-Path $executablePath) 'KSwitcherImageCaptureTests.exe'
|
||||
& $compiler @common tests/image_capture.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_pairing.o build/core.o build/platform.o build/storage.o build/ocr.o @libs -static -s -o $captureTest
|
||||
& $compiler @common tests/image_capture.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/storage.o build/ocr.o @libs -static -s -o $captureTest
|
||||
if ($LASTEXITCODE) { throw 'Image capture test build failed' }
|
||||
& $captureTest
|
||||
if ($LASTEXITCODE) { throw 'Image capture tests failed' }
|
||||
& $compiler @common tests/integration.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_pairing.o build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/sync.o build/ocr.o build/richtext.o build/update.o build/screenshot.o build/screenshot_launcher.o build/screenshot_activation.o build/screenshot_notifications.o build/app.o build/configuration.o build/ui.o build/screenshot-res.o @libs -static -s -o build/KSwitcherIntegration.exe
|
||||
& $compiler @common tests/integration.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/sync.o build/ocr.o build/richtext.o build/update.o build/screenshot.o build/screenshot_launcher.o build/screenshot_activation.o build/screenshot_notifications.o build/app.o build/configuration.o build/ui.o build/screenshot-res.o @libs -static -s -o build/KSwitcherIntegration.exe
|
||||
if ($LASTEXITCODE) { throw 'Integration test build failed' }
|
||||
& .\build\KSwitcherIntegration.exe
|
||||
if ($LASTEXITCODE) { throw 'Integration tests failed' }
|
||||
}
|
||||
if ($Ocr) {
|
||||
$ocrTest = Join-Path (Split-Path $executablePath) 'KSwitcherOcrTests.exe'
|
||||
& $compiler @common tests/ocr.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_pairing.o build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/sync.o build/ocr.o build/richtext.o build/update.o build/screenshot.o build/screenshot_launcher.o build/screenshot_activation.o build/screenshot_notifications.o build/app.o build/configuration.o build/ui.o build/screenshot-res.o @libs -static -s -o $ocrTest
|
||||
& $compiler @common tests/ocr.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/sync.o build/ocr.o build/richtext.o build/update.o build/screenshot.o build/screenshot_launcher.o build/screenshot_activation.o build/screenshot_notifications.o build/app.o build/configuration.o build/ui.o build/screenshot-res.o @libs -static -s -o $ocrTest
|
||||
if ($LASTEXITCODE) { throw 'OCR test build failed' }
|
||||
& $ocrTest
|
||||
if ($LASTEXITCODE) { throw 'OCR tests failed' }
|
||||
|
||||
+22
-2
@@ -1,5 +1,6 @@
|
||||
#include "configuration.h"
|
||||
#include "settings_merge.h"
|
||||
#include "../vendor/json.hpp"
|
||||
#include <cstring>
|
||||
namespace ks
|
||||
{
|
||||
@@ -33,7 +34,8 @@ void launchConfiguration(App &app, bool snippets, bool logs)
|
||||
}
|
||||
LRESULT configurationRequest(App &app, HWND sender, const COPYDATASTRUCT &message)
|
||||
{
|
||||
if (!configurationPeer(sender, L"KSwitcher.Config.exe") || message.cbData > ConfigLimit ||
|
||||
const bool filesPeers = message.dwData == FilesLanPeers;
|
||||
if (!configurationPeer(sender, filesPeers ? L"KSwitcher.Files.exe" : L"KSwitcher.Config.exe") || message.cbData > ConfigLimit ||
|
||||
(message.cbData && !message.lpData)) return FALSE;
|
||||
Bytes reply;
|
||||
bool ok = false;
|
||||
@@ -41,6 +43,23 @@ LRESULT configurationRequest(App &app, HWND sender, const COPYDATASTRUCT &messag
|
||||
{
|
||||
switch (message.dwData)
|
||||
{
|
||||
case FilesLanPeers:
|
||||
{
|
||||
if(message.cbData) return FALSE;
|
||||
const std::string request=R"({"action":"filesPeers"})";
|
||||
const auto snapshot=nlohmann::json::parse(app.lanCommand(Bytes(request.begin(),request.end())));
|
||||
auto paired=nlohmann::json::array();
|
||||
for(const auto &peer:snapshot.at("peers"))
|
||||
{
|
||||
if(!peer.value("paired",false)) continue;
|
||||
const auto name=wide(peer.at("name").get<std::string>());
|
||||
if(name.empty() || name.find_first_of(std::wstring_view(L"\0\r\n\t",4))!=name.npos) continue;
|
||||
paired.push_back({{"id",peer.at("id")},{"name",peer.at("name")},{"address",peer.at("address")},{"filesBrowse",peer.value("filesBrowse",false)}});
|
||||
}
|
||||
const auto encoded=paired.dump();
|
||||
reply.assign(encoded.begin(),encoded.end());
|
||||
break;
|
||||
}
|
||||
case ConfigLan:
|
||||
{
|
||||
if (!message.cbData || message.cbData > 4096) throw std::runtime_error("Invalid LAN request.");
|
||||
@@ -121,7 +140,8 @@ LRESULT configurationRequest(App &app, HWND sender, const COPYDATASTRUCT &messag
|
||||
std::string text = error.what();
|
||||
reply.assign(text.begin(), text.end());
|
||||
}
|
||||
COPYDATASTRUCT response{ConfigReply, (DWORD)reply.size(), reply.data()};
|
||||
if(filesPeers && !ok) return FALSE;
|
||||
COPYDATASTRUCT response{filesPeers?FilesLanPeersReply:ConfigReply, (DWORD)reply.size(), reply.data()};
|
||||
DWORD_PTR received{};
|
||||
if (!SendMessageTimeoutW(sender, WM_COPYDATA, (WPARAM)app.window, (LPARAM)&response,
|
||||
SMTO_ABORTIFHUNG, 2000, &received)) return FALSE;
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include "app.h"
|
||||
#include "files_lan_ipc.h"
|
||||
namespace ks
|
||||
{
|
||||
inline constexpr UINT ConfigSettingsDownloaded = WM_APP + 24;
|
||||
@@ -8,7 +9,7 @@ inline constexpr ULONG_PTR ConfigRead = 0x4b530100, ConfigSave = ConfigRead + 1,
|
||||
ConfigCapture = ConfigRead + 4, ConfigLogs = ConfigRead + 5,
|
||||
ConfigReply = ConfigRead + 6, ConfigClearLog = ConfigRead + 7,
|
||||
ConfigSync = ConfigRead + 8, ConfigSyncConflicts = ConfigRead + 9,
|
||||
ConfigSyncResolve = ConfigRead + 10, ConfigLan = ConfigRead + 11, ConfigSaveEdited = ConfigRead + 12, ConfigEnd = ConfigRead + 13;
|
||||
ConfigSyncResolve = ConfigRead + 10, ConfigLan = ConfigRead + 11, ConfigSaveEdited = ConfigRead + 12, ConfigEnd = FilesLanPeers + 1;
|
||||
inline constexpr size_t ConfigLimit = 8 * 1024 * 1024;
|
||||
bool configurationPeer(HWND, const wchar_t *filename);
|
||||
void launchConfiguration(App &, bool snippets = false, bool logs = false);
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
#include <windows.h>
|
||||
|
||||
inline constexpr ULONG_PTR FilesLanPeers = 0x4b53010d;
|
||||
inline constexpr ULONG_PTR FilesLanPeersReply = 0x4b53010e;
|
||||
inline constexpr DWORD FilesLanPeersLimit = 64 * 1024;
|
||||
+863
-389
File diff suppressed because it is too large.
Load diff
+18
-5
@@ -2,6 +2,7 @@
|
||||
#include <ws2tcpip.h>
|
||||
#include <iphlpapi.h>
|
||||
#include "lan.h"
|
||||
#include "lan_files.h"
|
||||
#include "lan_pairing.h"
|
||||
#include "lan_mesh.h"
|
||||
#include "lan_clipboard.h"
|
||||
@@ -48,7 +49,7 @@ std::vector<ULONG> broadcasts()
|
||||
}
|
||||
struct LanService::Impl
|
||||
{
|
||||
struct Peer { std::string name,epoch; sockaddr_in address{}; uint64_t seen{},verified{}; };
|
||||
struct Peer { std::string name,epoch; sockaddr_in address{}; uint64_t seen{},verified{}; bool files{}; };
|
||||
std::filesystem::path path;
|
||||
std::string id,epoch=lan::randomHex(16),name=hostName(),status="Disabled.";
|
||||
std::map<std::string,Peer> peers;
|
||||
@@ -61,7 +62,7 @@ struct LanService::Impl
|
||||
std::optional<LogDownload> logDownload;
|
||||
bool shareLogs{};
|
||||
bool enabled{};
|
||||
std::atomic<bool> stopping{false};
|
||||
std::atomic<bool> stopping{false}, refreshFiles{false};
|
||||
std::mutex mutex;
|
||||
std::thread worker;
|
||||
bool shareInput{};
|
||||
@@ -225,6 +226,7 @@ struct LanService::Impl
|
||||
if(peer.address.sin_addr.s_addr!=address.sin_addr.s_addr) peer.verified=0;
|
||||
clipboard->peerSession(other,j.value("clipboardSession",""));
|
||||
transfers->peerSession(other,j.value("fileSession",""));
|
||||
peer.files=j.value("filesBrowse",false);
|
||||
peer.name=display; peer.epoch=session; peer.address=address; peer.seen=now;
|
||||
if(trusted.contains(other))
|
||||
{
|
||||
@@ -289,10 +291,17 @@ struct LanService::Impl
|
||||
if(bind(socket,(sockaddr*)&bound,sizeof(bound))) throw std::runtime_error("UDP port 45883 is in use. Only one LAN-enabled KSwitcher may run per computer.");
|
||||
uint64_t nextHello=0,nextOffer=0;
|
||||
lan::InputHub hub(id);
|
||||
lan::FilesListener files(id,[&](const std::string &peer)
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
return !stopping && trusted.contains(peer)?trusted.at(peer).at("key").get<std::string>():std::string{};
|
||||
});
|
||||
hub.datagrams([&](const auto &target,const auto &packet){
|
||||
std::lock_guard lock(mutex);
|
||||
if(shareInput&&trusted.contains(target)&&peers.contains(target))send(socket,peers.at(target).address,packet);
|
||||
});
|
||||
const bool filesListening=files.port()!=0;
|
||||
if(!filesListening) diagnosticEvent(L"LAN Files could not listen on TCP 45885.");
|
||||
while(!stopping)
|
||||
{
|
||||
auto now=GetTickCount64();bool clipboardActive=false;
|
||||
@@ -308,11 +317,12 @@ struct LanService::Impl
|
||||
clipboardActive=clipboard->active()||transfers->active();
|
||||
if(shareInput)for(auto &[peer,entry]:trusted.items())
|
||||
endpoints.push_back({peer,entry.at("key"),peers.contains(peer)?peers.at(peer).address.sin_addr.s_addr:0});
|
||||
if(now>=nextHello)
|
||||
if(refreshFiles.exchange(false) || now>=nextHello)
|
||||
{
|
||||
epoch=lan::randomHex(16); // Fresh challenge prevents replayed presence from staying online.
|
||||
auto hello=lan::envelope("hello",id); hello["name"]=name; hello["epoch"]=epoch;hello["clipboardSession"]=clipboard->session();
|
||||
hello["fileSession"]=transfers->session();
|
||||
hello["filesBrowse"]=filesListening;
|
||||
for(auto ip:broadcasts()) { auto address=bound; address.sin_addr.s_addr=ip; send(socket,address,hello); }
|
||||
nextHello=now+3000;
|
||||
}
|
||||
@@ -331,6 +341,7 @@ struct LanService::Impl
|
||||
{std::lock_guard lock(inputMutex);out.swap(inputOut);}
|
||||
if(abortInput.exchange(false)){hub.disconnect();out.clear();}
|
||||
hub.poll(endpoints,out,in,now);
|
||||
files.poll();
|
||||
auto readyPeers=hub.ready();inputConnected=!readyPeers.empty();
|
||||
{std::lock_guard lock(inputMutex);inputReady=std::move(readyPeers);
|
||||
for(auto &frame:in){
|
||||
@@ -385,6 +396,7 @@ struct LanService::Impl
|
||||
}
|
||||
std::lock_guard lock(mutex);
|
||||
auto now=GetTickCount64();
|
||||
if(action=="filesPeers") refreshFiles=true;
|
||||
if(action=="shareLogs")
|
||||
{
|
||||
bool previous=shareLogs;
|
||||
@@ -480,7 +492,7 @@ struct LanService::Impl
|
||||
auto exe=utf8(executable());std::string quoted;
|
||||
for(char c:exe){quoted+=c;if(c=='\'')quoted+='\'';}
|
||||
auto script="$ksPath = '"+quoted+"'\r\n"
|
||||
"New-NetFirewallRule -DisplayName 'KSwitcher LAN TCP' -Direction Inbound -Action Allow -Program $ksPath -Protocol TCP -LocalPort 45884 -RemoteAddress LocalSubnet -Profile Any\r\n"
|
||||
"New-NetFirewallRule -DisplayName 'KSwitcher LAN TCP' -Direction Inbound -Action Allow -Program $ksPath -Protocol TCP -LocalPort 45884,45885 -RemoteAddress LocalSubnet -Profile Any\r\n"
|
||||
"New-NetFirewallRule -DisplayName 'KSwitcher LAN UDP' -Direction Inbound -Action Allow -Program $ksPath -Protocol UDP -LocalPort 45883 -RemoteAddress LocalSubnet -Profile Any";
|
||||
return encoded({{"script",script}});
|
||||
}
|
||||
@@ -524,10 +536,11 @@ struct LanService::Impl
|
||||
? (lan::PairLifetime-(now-pending->started)+999)/1000 : 0;
|
||||
for(const auto &[peer,p]:peers)
|
||||
{
|
||||
if(action=="filesPeers" && !trusted.contains(peer)) continue;
|
||||
char ip[INET_ADDRSTRLEN]{}; InetNtopA(AF_INET,(void*)&p.address.sin_addr,ip,sizeof(ip));
|
||||
result["peers"].push_back({{"id",peer},{"name",p.name},{"address",ip},{"paired",trusted.contains(peer)},
|
||||
{"verified",p.verified && now-p.verified<lan::PeerLifetime},{"online",true},
|
||||
{"fileTransfer",transfers->hasSession(peer)}});
|
||||
{"fileTransfer",transfers->hasSession(peer)},{"filesBrowse",p.files}});
|
||||
}
|
||||
for(const auto &[peer,p]:trusted.items()) if(!peers.contains(peer))
|
||||
result["peers"].push_back({{"id",peer},{"name",p.at("name")},{"address",""},{"paired",true},{"verified",false},{"online",false}});
|
||||
|
||||
+4
-4
@@ -50,14 +50,14 @@ struct InputCipher::Impl
|
||||
};
|
||||
InputCipher::InputCipher(const std::string &key):impl(std::make_unique<Impl>(key)){}
|
||||
InputCipher::~InputCipher()=default;
|
||||
Bytes InputCipher::encryptPayload(const Bytes &plain)
|
||||
Bytes InputCipher::encryptPayload(const Bytes &plain,size_t limit)
|
||||
{
|
||||
if(plain.empty()||plain.size()>1024)throw std::runtime_error("Invalid file packet size.");
|
||||
if(plain.empty()||plain.size()>limit)throw std::runtime_error("Invalid file packet size.");
|
||||
Bytes tag(16);auto result=impl->crypt(plain,tag,true);result.insert(result.end(),tag.begin(),tag.end());return result;
|
||||
}
|
||||
Bytes InputCipher::decryptPayload(const Bytes &packet)
|
||||
Bytes InputCipher::decryptPayload(const Bytes &packet,size_t limit)
|
||||
{
|
||||
if(packet.size()<17||packet.size()>1040)throw std::runtime_error("Invalid file packet size.");
|
||||
if(packet.size()<17||packet.size()-16>limit)throw std::runtime_error("Invalid file packet size.");
|
||||
Bytes tag(packet.end()-16,packet.end());return impl->crypt(Bytes(packet.begin(),packet.end()-16),tag,false);
|
||||
}
|
||||
Bytes InputCipher::encrypt(const InputFrame &f)
|
||||
|
||||
+2
-2
@@ -39,8 +39,8 @@ class InputCipher
|
||||
~InputCipher();
|
||||
Bytes encrypt(const InputFrame &);
|
||||
InputFrame decrypt(const Bytes &);
|
||||
Bytes encryptPayload(const Bytes &);
|
||||
Bytes decryptPayload(const Bytes &);
|
||||
Bytes encryptPayload(const Bytes &,size_t limit=1024);
|
||||
Bytes decryptPayload(const Bytes &,size_t limit=1024);
|
||||
};
|
||||
std::string inputSessionKey(const std::string &secret,const std::string &from,const std::string &to,
|
||||
const std::string &fromNonce,const std::string &toNonce,int fromEdge=-1);
|
||||
|
||||
@@ -0,0 +1,825 @@
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <shellapi.h>
|
||||
#include <shlobj.h>
|
||||
#include <atomic>
|
||||
#include <bcrypt.h>
|
||||
#include <thread>
|
||||
#include "lan_files.h"
|
||||
#include "lan_channel.h"
|
||||
|
||||
namespace ks::lan
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
Json fileRequest(std::string_view action, const fs::path &path)
|
||||
{
|
||||
return {{"action",action},{"path",utf8(path.native())}};
|
||||
}
|
||||
|
||||
fs::path filePath(const Json &value)
|
||||
{
|
||||
return wide(value.get<std::string>());
|
||||
}
|
||||
|
||||
void checkFileReply(const Json &reply)
|
||||
{
|
||||
const auto error=reply.at("error").get<DWORD>();
|
||||
if(error) throw std::system_error(static_cast<int>(error),std::system_category());
|
||||
}
|
||||
|
||||
bool recycleFile(HWND owner,const fs::path &path)
|
||||
{
|
||||
const auto initialized=CoInitializeEx(nullptr,COINIT_APARTMENTTHREADED);
|
||||
if(FAILED(initialized)) return false;
|
||||
IFileOperation *operation{};
|
||||
IShellItem *item{};
|
||||
auto result=CoCreateInstance(__uuidof(FileOperation),nullptr,CLSCTX_INPROC_SERVER,IID_PPV_ARGS(&operation));
|
||||
if(SUCCEEDED(result))
|
||||
result=operation->SetOperationFlags(FOFX_RECYCLEONDELETE|FOF_SILENT|FOF_NOCONFIRMATION|
|
||||
FOF_NOERRORUI|FOFX_EARLYFAILURE);
|
||||
if(SUCCEEDED(result) && owner) result=operation->SetOwnerWindow(owner);
|
||||
if(SUCCEEDED(result)) result=SHCreateItemFromParsingName(path.c_str(),nullptr,IID_PPV_ARGS(&item));
|
||||
if(SUCCEEDED(result)) result=operation->DeleteItem(item,nullptr);
|
||||
if(SUCCEEDED(result)) result=operation->PerformOperations();
|
||||
BOOL aborted=TRUE;
|
||||
if(SUCCEEDED(result)) result=operation->GetAnyOperationsAborted(&aborted);
|
||||
if(item) item->Release();
|
||||
if(operation) operation->Release();
|
||||
CoUninitialize();
|
||||
return SUCCEEDED(result) && !aborted;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr size_t FrameLimit=256*1024;
|
||||
constexpr DWORD SocketTimeout=5000;
|
||||
|
||||
struct WinSocket
|
||||
{
|
||||
SOCKET value=INVALID_SOCKET;
|
||||
~WinSocket(){if(value!=INVALID_SOCKET) closesocket(value);}
|
||||
};
|
||||
|
||||
void socketError()
|
||||
{
|
||||
throw std::system_error(WSAGetLastError(),std::system_category());
|
||||
}
|
||||
|
||||
struct Network
|
||||
{
|
||||
Network()
|
||||
{
|
||||
WSADATA data{};
|
||||
if(const auto error=WSAStartup(MAKEWORD(2,2),&data))
|
||||
throw std::system_error(error,std::system_category());
|
||||
}
|
||||
~Network(){WSACleanup();}
|
||||
};
|
||||
|
||||
class Wire
|
||||
{
|
||||
SOCKET socket;
|
||||
std::unique_ptr<InputCipher> tx,rx;
|
||||
void bytes(void *buffer,size_t size,bool sending)
|
||||
{
|
||||
auto *next=static_cast<char*>(buffer);
|
||||
const auto deadline=GetTickCount64()+SocketTimeout;
|
||||
while(size)
|
||||
{
|
||||
if(GetTickCount64()>=deadline)
|
||||
throw std::system_error(WSAETIMEDOUT,std::system_category());
|
||||
const auto n=sending?send(socket,next,static_cast<int>(size),0):
|
||||
recv(socket,next,static_cast<int>(size),0);
|
||||
if(n==SOCKET_ERROR) socketError();
|
||||
if(!n) throw std::system_error(WSAECONNRESET,std::system_category());
|
||||
size-=n;
|
||||
next+=n;
|
||||
}
|
||||
}
|
||||
public:
|
||||
explicit Wire(SOCKET socket):socket(socket)
|
||||
{
|
||||
if(setsockopt(socket,SOL_SOCKET,SO_RCVTIMEO,reinterpret_cast<const char*>(&SocketTimeout),sizeof(SocketTimeout)) ||
|
||||
setsockopt(socket,SOL_SOCKET,SO_SNDTIMEO,reinterpret_cast<const char*>(&SocketTimeout),sizeof(SocketTimeout)))
|
||||
socketError();
|
||||
const BOOL yes=TRUE;
|
||||
setsockopt(socket,IPPROTO_TCP,TCP_NODELAY,reinterpret_cast<const char*>(&yes),sizeof(yes));
|
||||
}
|
||||
void write(const Json &message)
|
||||
{
|
||||
auto data=Json::to_cbor(message);
|
||||
if(tx) data=tx->encryptPayload(data,FrameLimit-16);
|
||||
if(data.size()>FrameLimit) throw std::runtime_error("LAN file frame too large.");
|
||||
uint32_t length=htonl(static_cast<uint32_t>(data.size()));
|
||||
bytes(&length,sizeof(length),true);
|
||||
bytes(data.data(),data.size(),true);
|
||||
}
|
||||
Json read(bool idle=false)
|
||||
{
|
||||
if(idle)
|
||||
{
|
||||
fd_set ready{};
|
||||
FD_SET(socket,&ready);
|
||||
if(select(0,&ready,nullptr,nullptr,nullptr)==SOCKET_ERROR) socketError();
|
||||
}
|
||||
uint32_t length{};
|
||||
bytes(&length,sizeof(length),false);
|
||||
length=ntohl(length);
|
||||
if(!length || length>(rx?FrameLimit:1024))
|
||||
throw std::runtime_error("Invalid LAN file frame.");
|
||||
Bytes data(length);
|
||||
bytes(data.data(),data.size(),false);
|
||||
if(rx) data=rx->decryptPayload(data,FrameLimit-16);
|
||||
return Json::from_cbor(data);
|
||||
}
|
||||
void authenticate(const std::string &secret,const std::string &client,const std::string &server,
|
||||
const std::string &challenge,const std::string &nonce,bool outbound)
|
||||
{
|
||||
if(!identifier(client)||!identifier(server)||client==server||!identifier(challenge)||!identifier(nonce))
|
||||
throw std::runtime_error("Invalid LAN file session.");
|
||||
const auto context="KSwitcher files v1|"+client+"|"+server+"|"+challenge+"|"+nonce;
|
||||
tx=std::make_unique<InputCipher>(mac(secret,context+(outbound?"|request":"|response")));
|
||||
rx=std::make_unique<InputCipher>(mac(secret,context+(outbound?"|response":"|request")));
|
||||
}
|
||||
};
|
||||
|
||||
void connectPeer(SOCKET socket,const FilesPeer &peer)
|
||||
{
|
||||
sockaddr_in address{};
|
||||
address.sin_family=AF_INET;
|
||||
address.sin_port=htons(peer.port);
|
||||
if(InetPtonA(AF_INET,peer.address.c_str(),&address.sin_addr)!=1)
|
||||
throw std::system_error(WSAEHOSTUNREACH,std::system_category());
|
||||
u_long mode=1;
|
||||
if(ioctlsocket(socket,FIONBIO,&mode)) socketError();
|
||||
if(connect(socket,reinterpret_cast<sockaddr*>(&address),sizeof(address)))
|
||||
{
|
||||
if(WSAGetLastError()!=WSAEWOULDBLOCK) socketError();
|
||||
fd_set writable{},failed{};
|
||||
FD_SET(socket,&writable);
|
||||
FD_SET(socket,&failed);
|
||||
timeval timeout{2,0};
|
||||
const auto result=select(0,nullptr,&writable,&failed,&timeout);
|
||||
if(result==SOCKET_ERROR) socketError();
|
||||
if(!result) throw std::system_error(WSAETIMEDOUT,std::system_category());
|
||||
int error{},size=sizeof(error);
|
||||
if(getsockopt(socket,SOL_SOCKET,SO_ERROR,reinterpret_cast<char*>(&error),&size)) socketError();
|
||||
if(error) throw std::system_error(error,std::system_category());
|
||||
}
|
||||
mode=0;
|
||||
if(ioctlsocket(socket,FIONBIO,&mode)) socketError();
|
||||
}
|
||||
|
||||
uint64_t fileSize(DWORD high,DWORD low){return (uint64_t(high)<<32)|low;}
|
||||
Json ok(){return {{"error",ERROR_SUCCESS}};}
|
||||
Json failure(DWORD error=GetLastError()){return {{"error",error?error:ERROR_GEN_FAILURE}};}
|
||||
|
||||
struct FileHandle
|
||||
{
|
||||
HANDLE value=INVALID_HANDLE_VALUE;
|
||||
void close(){if(value!=INVALID_HANDLE_VALUE) CloseHandle(std::exchange(value,INVALID_HANDLE_VALUE));}
|
||||
~FileHandle(){close();}
|
||||
};
|
||||
|
||||
fs::path temporaryFile(const fs::path &target)
|
||||
{
|
||||
return target.parent_path()/wide(".kswitcher-"+randomHex(16)+".tmp");
|
||||
}
|
||||
|
||||
std::string hashPrefix(HANDLE file,uint64_t size,const FileProgress &progress,uint64_t offset=0)
|
||||
{
|
||||
struct Hash
|
||||
{
|
||||
BCRYPT_ALG_HANDLE algorithm{};
|
||||
BCRYPT_HASH_HANDLE value{};
|
||||
~Hash(){if(value) BCryptDestroyHash(value);if(algorithm) BCryptCloseAlgorithmProvider(algorithm,0);}
|
||||
} hash;
|
||||
const auto checked=[](NTSTATUS status)
|
||||
{
|
||||
if(status<0) throw std::system_error(ERROR_GEN_FAILURE,std::system_category());
|
||||
};
|
||||
LARGE_INTEGER position{};
|
||||
position.QuadPart=offset;
|
||||
if(!SetFilePointerEx(file,position,nullptr,FILE_BEGIN))
|
||||
throw std::system_error(GetLastError(),std::system_category());
|
||||
checked(BCryptOpenAlgorithmProvider(&hash.algorithm,BCRYPT_SHA256_ALGORITHM,nullptr,0));
|
||||
checked(BCryptCreateHash(hash.algorithm,&hash.value,nullptr,0,nullptr,0,0));
|
||||
Bytes buffer(FileChunk),digest(32);
|
||||
const auto total=size;
|
||||
auto last=GetTickCount64();
|
||||
while(size)
|
||||
{
|
||||
const auto now=GetTickCount64();
|
||||
if(progress && now-last>=100)
|
||||
{
|
||||
if(!progress(total-size,total)) throw std::system_error(ERROR_CANCELLED,std::system_category());
|
||||
last=now;
|
||||
}
|
||||
const auto count=static_cast<DWORD>(std::min<uint64_t>(size,buffer.size()));
|
||||
DWORD read{};
|
||||
if(!ReadFile(file,buffer.data(),count,&read,nullptr) || read!=count)
|
||||
throw std::system_error(ERROR_READ_FAULT,std::system_category());
|
||||
checked(BCryptHashData(hash.value,buffer.data(),read,0));
|
||||
size-=read;
|
||||
}
|
||||
checked(BCryptFinishHash(hash.value,digest.data(),static_cast<ULONG>(digest.size()),0));
|
||||
std::string hex;
|
||||
for(const auto byte:digest){hex+="0123456789abcdef"[byte>>4];hex+="0123456789abcdef"[byte&15];}
|
||||
return hex;
|
||||
}
|
||||
|
||||
class FileOperations
|
||||
{
|
||||
bool remote;
|
||||
HANDLE search=INVALID_HANDLE_VALUE;
|
||||
WIN32_FIND_DATAW entry{};
|
||||
bool pending{};
|
||||
FileHandle input,output;
|
||||
fs::path temporary,target;
|
||||
bool resumable{};
|
||||
uint64_t appendBase{},expectedSize{};
|
||||
void closeSearch()
|
||||
{
|
||||
if(search!=INVALID_HANDLE_VALUE) FindClose(std::exchange(search,INVALID_HANDLE_VALUE));
|
||||
}
|
||||
void discard(bool removePartial=false)
|
||||
{
|
||||
output.close();
|
||||
if(resumable && !removePartial) {temporary.clear();resumable=false;return;}
|
||||
resumable=false;
|
||||
if(!temporary.empty() && !DeleteFileW(temporary.c_str()) && GetLastError()==ERROR_ACCESS_DENIED)
|
||||
{
|
||||
SetFileAttributesW(temporary.c_str(),FILE_ATTRIBUTE_NORMAL);
|
||||
DeleteFileW(temporary.c_str());
|
||||
}
|
||||
temporary.clear();
|
||||
}
|
||||
fs::path path(const Json &value) const
|
||||
{
|
||||
auto result=filePath(value);
|
||||
const auto &name=result.native();
|
||||
if(name.find(L'\0')!=name.npos) throw std::system_error(ERROR_INVALID_NAME,std::system_category());
|
||||
if(remote)
|
||||
{
|
||||
// Only drive paths from the remote Windows namespace. Reject NT devices and streams.
|
||||
if(name.size()<3 || !((name[0]>=L'A'&&name[0]<=L'Z')||(name[0]>=L'a'&&name[0]<=L'z')) ||
|
||||
name[1]!=L':' || (name[2]!=L'\\' && name[2]!=L'/') || name.find(L':',2)!=name.npos)
|
||||
throw std::system_error(ERROR_INVALID_NAME,std::system_category());
|
||||
result=result.lexically_normal();
|
||||
for(const auto &part:result.relative_path())
|
||||
{
|
||||
const auto &component=part.native();
|
||||
if(component.empty()) continue;
|
||||
const auto dosName=component.substr(0,component.find_first_of(L". "));
|
||||
if(part==L".." || component.find_first_of(L"<>|?*\"")!=component.npos ||
|
||||
component.back()==L' ' || component.back()==L'.' ||
|
||||
std::any_of(component.begin(),component.end(),[](wchar_t ch){return ch<32;}) ||
|
||||
_wcsicmp(dosName.c_str(),L"CON")==0 || _wcsicmp(dosName.c_str(),L"PRN")==0 ||
|
||||
_wcsicmp(dosName.c_str(),L"AUX")==0 || _wcsicmp(dosName.c_str(),L"NUL")==0 ||
|
||||
(dosName.size()==4 && (_wcsnicmp(dosName.c_str(),L"COM",3)==0 ||
|
||||
_wcsnicmp(dosName.c_str(),L"LPT",3)==0) && std::wstring_view(L"123456789¹²³").find(dosName.back())!=component.npos))
|
||||
throw std::system_error(ERROR_INVALID_NAME,std::system_category());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
Json listNext()
|
||||
{
|
||||
auto reply=ok();
|
||||
auto &entries=reply["entries"]=Json::array();
|
||||
while(search!=INVALID_HANDLE_VALUE && entries.size()<64)
|
||||
{
|
||||
if(!pending && !FindNextFileW(search,&entry))
|
||||
{
|
||||
const auto error=GetLastError();
|
||||
closeSearch();
|
||||
if(error!=ERROR_NO_MORE_FILES) return failure(error);
|
||||
break;
|
||||
}
|
||||
pending=false;
|
||||
const std::wstring name(entry.cFileName);
|
||||
if(name==L"." || name==L".." ||
|
||||
((entry.dwFileAttributes&FILE_ATTRIBUTE_HIDDEN) && name.starts_with(L".kswitcher-") &&
|
||||
name.ends_with(L".partial") && name.size()==83)) continue;
|
||||
entries.push_back({{"name",utf8(name)},{"attributes",entry.dwFileAttributes},
|
||||
{"size",fileSize(entry.nFileSizeHigh,entry.nFileSizeLow)},
|
||||
{"modified",fileSize(entry.ftLastWriteTime.dwHighDateTime,entry.ftLastWriteTime.dwLowDateTime)}});
|
||||
}
|
||||
reply["more"]=search!=INVALID_HANDLE_VALUE;
|
||||
return reply;
|
||||
}
|
||||
static DWORD CALLBACK copyProgress(LARGE_INTEGER total,LARGE_INTEGER transferred,LARGE_INTEGER,LARGE_INTEGER,
|
||||
DWORD,DWORD,HANDLE,HANDLE,LPVOID context)
|
||||
{
|
||||
const auto &callback=*static_cast<const FileProgress*>(context);
|
||||
try {return !callback || callback(transferred.QuadPart,total.QuadPart)?PROGRESS_CONTINUE:PROGRESS_CANCEL;}
|
||||
catch(...) {return PROGRESS_CANCEL;}
|
||||
}
|
||||
Json copy(const fs::path &source,const fs::path &destination,bool replace,bool append,
|
||||
const FileProgress &progress)
|
||||
{
|
||||
discard();
|
||||
temporary=temporaryFile(destination);
|
||||
if(!append)
|
||||
{
|
||||
if(!CopyFileExW(source.c_str(),temporary.c_str(),copyProgress,
|
||||
const_cast<FileProgress*>(&progress),nullptr,COPY_FILE_FAIL_IF_EXISTS))
|
||||
{
|
||||
const auto error=GetLastError();
|
||||
if(error==ERROR_FILE_EXISTS || error==ERROR_ALREADY_EXISTS) temporary.clear();
|
||||
return failure(error);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
FileHandle in{CreateFileW(source.c_str(),GENERIC_READ,FILE_SHARE_READ,nullptr,OPEN_EXISTING,0,nullptr)};
|
||||
if(in.value==INVALID_HANDLE_VALUE){temporary.clear();return failure();}
|
||||
if(!CopyFileExW(destination.c_str(),temporary.c_str(),nullptr,nullptr,nullptr,COPY_FILE_FAIL_IF_EXISTS))
|
||||
{
|
||||
const auto error=GetLastError();
|
||||
if(error==ERROR_FILE_EXISTS || error==ERROR_ALREADY_EXISTS) temporary.clear();
|
||||
return failure(error);
|
||||
}
|
||||
if(!SetFileAttributesW(temporary.c_str(),FILE_ATTRIBUTE_NORMAL)) return failure();
|
||||
FileHandle out{CreateFileW(temporary.c_str(),FILE_APPEND_DATA,0,nullptr,OPEN_EXISTING,0,nullptr)};
|
||||
if(out.value==INVALID_HANDLE_VALUE) return failure();
|
||||
LARGE_INTEGER size{};
|
||||
if(!GetFileSizeEx(in.value,&size)) return failure();
|
||||
Bytes buffer(FileChunk);
|
||||
uint64_t done=0;
|
||||
for(;;)
|
||||
{
|
||||
DWORD read{},written{};
|
||||
if(!ReadFile(in.value,buffer.data(),static_cast<DWORD>(buffer.size()),&read,nullptr)) return failure();
|
||||
if(!read) break;
|
||||
if(!WriteFile(out.value,buffer.data(),read,&written,nullptr)) return failure();
|
||||
if(written!=read) return failure(ERROR_WRITE_FAULT);
|
||||
done+=written;
|
||||
if(progress && !progress(done,size.QuadPart)) return failure(ERROR_CANCELLED);
|
||||
}
|
||||
}
|
||||
return commit(destination,replace);
|
||||
}
|
||||
static Json replaceFile(const fs::path &source,const fs::path &destination,bool replace)
|
||||
{
|
||||
const auto attributes=GetFileAttributesW(destination.c_str());
|
||||
const bool readOnly=replace && attributes!=INVALID_FILE_ATTRIBUTES &&
|
||||
(attributes&FILE_ATTRIBUTE_READONLY)!=0;
|
||||
if(replace && attributes!=INVALID_FILE_ATTRIBUTES &&
|
||||
(attributes&(FILE_ATTRIBUTE_DIRECTORY|FILE_ATTRIBUTE_REPARSE_POINT))) return failure(ERROR_ACCESS_DENIED);
|
||||
if(readOnly && !SetFileAttributesW(destination.c_str(),attributes&~FILE_ATTRIBUTE_READONLY))
|
||||
return failure();
|
||||
if(!MoveFileExW(source.c_str(),destination.c_str(),replace?MOVEFILE_REPLACE_EXISTING:0))
|
||||
{
|
||||
const auto error=GetLastError();
|
||||
if(readOnly) SetFileAttributesW(destination.c_str(),attributes);
|
||||
return failure(error);
|
||||
}
|
||||
return ok();
|
||||
}
|
||||
Json commit(const fs::path &destination,bool replace)
|
||||
{
|
||||
const auto reply=replaceFile(temporary,destination,replace);
|
||||
if(reply.at("error")==ERROR_SUCCESS) temporary.clear();
|
||||
return reply;
|
||||
}
|
||||
public:
|
||||
explicit FileOperations(bool remote):remote(remote){}
|
||||
~FileOperations(){closeSearch();discard();}
|
||||
Json call(const Json &request,const FileProgress &progress)
|
||||
{
|
||||
try {return perform(request,progress);}
|
||||
catch(const std::system_error &e)
|
||||
{
|
||||
if(e.code().value()==ERROR_CANCELLED) discard(true);
|
||||
return failure(static_cast<DWORD>(e.code().value()));
|
||||
}
|
||||
}
|
||||
Json perform(const Json &request,const FileProgress &progress)
|
||||
{
|
||||
const auto action=request.at("action").get<std::string>();
|
||||
if(action=="drives")
|
||||
{
|
||||
auto reply=ok();
|
||||
auto &drives=reply["drives"]=Json::array();
|
||||
const auto mask=GetLogicalDrives();
|
||||
if(!mask) return failure();
|
||||
for(int letter=0;letter<26;++letter)
|
||||
{
|
||||
if(!(mask&(1UL<<letter))) continue;
|
||||
const wchar_t root[]{static_cast<wchar_t>(L'A'+letter),L':',L'\\',0};
|
||||
const auto type=GetDriveTypeW(root);
|
||||
ULARGE_INTEGER total{},free{};
|
||||
if(type==DRIVE_FIXED || type==DRIVE_RAMDISK)
|
||||
GetDiskFreeSpaceExW(root,&free,&total,nullptr);
|
||||
drives.push_back({{"path",utf8(root)},{"type",type},{"total",total.QuadPart},{"free",free.QuadPart}});
|
||||
}
|
||||
return reply;
|
||||
}
|
||||
if(action=="listNext") return listNext();
|
||||
if(action=="read")
|
||||
{
|
||||
Bytes data(FileChunk);
|
||||
DWORD count{};
|
||||
if(!ReadFile(input.value,data.data(),static_cast<DWORD>(data.size()),&count,nullptr)) return failure();
|
||||
data.resize(count);
|
||||
return {{"error",0},{"data",Json::binary(std::move(data))}};
|
||||
}
|
||||
if(action=="closeRead"){input.close();return ok();}
|
||||
if(action=="abort"){discard(true);return ok();}
|
||||
if(action=="hashRead")
|
||||
{
|
||||
if(input.value==INVALID_HANDLE_VALUE) return failure(ERROR_INVALID_HANDLE);
|
||||
return {{"error",0},{"hash",hashPrefix(input.value,request.at("size"),progress)}};
|
||||
}
|
||||
if(action=="write")
|
||||
{
|
||||
const auto &data=request.at("data").get_binary();
|
||||
if(data.size()>FileChunk) return failure(ERROR_INVALID_DATA);
|
||||
LARGE_INTEGER zero{},start{};
|
||||
if(!SetFilePointerEx(output.value,zero,&start,FILE_CURRENT)) return failure();
|
||||
const auto end=uint64_t(start.QuadPart)+data.size();
|
||||
if(end<appendBase || end-appendBase>expectedSize) return failure(ERROR_INVALID_DATA);
|
||||
DWORD count{};
|
||||
if(WriteFile(output.value,data.data(),static_cast<DWORD>(data.size()),&count,nullptr) &&
|
||||
count==data.size() && FlushFileBuffers(output.value)) return ok();
|
||||
const auto error=GetLastError();
|
||||
if(!SetFilePointerEx(output.value,start,nullptr,FILE_BEGIN) || !SetEndOfFile(output.value))
|
||||
discard(true);
|
||||
return failure(error?error:ERROR_WRITE_FAULT);
|
||||
}
|
||||
if(action=="commit")
|
||||
{
|
||||
if(output.value==INVALID_HANDLE_VALUE) return failure(ERROR_INVALID_HANDLE);
|
||||
LARGE_INTEGER size{};
|
||||
if(!GetFileSizeEx(output.value,&size)) return failure();
|
||||
if(uint64_t(size.QuadPart)!=appendBase+expectedSize) return failure(ERROR_FILE_INVALID);
|
||||
const auto stamp=request.at("modified").get<uint64_t>();
|
||||
const FILETIME time{static_cast<DWORD>(stamp),static_cast<DWORD>(stamp>>32)};
|
||||
if(!SetFileTime(output.value,nullptr,nullptr,&time) || !FlushFileBuffers(output.value)) return failure();
|
||||
output.close();
|
||||
const auto reply=commit(target,request.at("replace").get<bool>());
|
||||
if(reply.at("error")==ERROR_SUCCESS) SetFileAttributesW(target.c_str(),FILE_ATTRIBUTE_ARCHIVE);
|
||||
return reply;
|
||||
}
|
||||
|
||||
const auto name=path(request.at("path"));
|
||||
if(action=="list")
|
||||
{
|
||||
closeSearch();
|
||||
search=FindFirstFileW((name/L"*").c_str(),&entry);
|
||||
pending=search!=INVALID_HANDLE_VALUE;
|
||||
if(!pending && GetLastError()!=ERROR_FILE_NOT_FOUND) return failure();
|
||||
return listNext();
|
||||
}
|
||||
if(action=="stat")
|
||||
{
|
||||
WIN32_FILE_ATTRIBUTE_DATA data{};
|
||||
if(!GetFileAttributesExW(name.c_str(),GetFileExInfoStandard,&data)) return failure();
|
||||
return {{"error",0},{"attributes",data.dwFileAttributes},{"size",fileSize(data.nFileSizeHigh,data.nFileSizeLow)},
|
||||
{"modified",fileSize(data.ftLastWriteTime.dwHighDateTime,data.ftLastWriteTime.dwLowDateTime)}};
|
||||
}
|
||||
if(action=="canonical")
|
||||
{
|
||||
std::error_code error;
|
||||
const auto canonical=fs::weakly_canonical(name,error);
|
||||
return error?failure(error.value()):Json{{"error",0},{"path",utf8(canonical.native())}};
|
||||
}
|
||||
if(action=="mkdir") return CreateDirectoryW(name.c_str(),nullptr)?ok():failure();
|
||||
if(action=="rmdir") return RemoveDirectoryW(name.c_str())?ok():failure();
|
||||
if(action=="recycle") return recycleFile(nullptr,name)?ok():failure(ERROR_ACCESS_DENIED);
|
||||
if(action=="remove")
|
||||
{
|
||||
if(request.contains("size"))
|
||||
{
|
||||
WIN32_FILE_ATTRIBUTE_DATA data{};
|
||||
if(!GetFileAttributesExW(name.c_str(),GetFileExInfoStandard,&data)) return failure();
|
||||
if((data.dwFileAttributes&(FILE_ATTRIBUTE_DIRECTORY|FILE_ATTRIBUTE_REPARSE_POINT)) ||
|
||||
fileSize(data.nFileSizeHigh,data.nFileSizeLow)!=request.at("size") ||
|
||||
fileSize(data.ftLastWriteTime.dwHighDateTime,data.ftLastWriteTime.dwLowDateTime)!=request.at("modified"))
|
||||
return failure(ERROR_FILE_INVALID);
|
||||
}
|
||||
const auto attributes=GetFileAttributesW(name.c_str());
|
||||
if(attributes==INVALID_FILE_ATTRIBUTES) return failure();
|
||||
if(attributes&FILE_ATTRIBUTE_DIRECTORY) return RemoveDirectoryW(name.c_str())?ok():failure();
|
||||
const bool clear=request.value("readOnly",false) && (attributes&FILE_ATTRIBUTE_READONLY) &&
|
||||
!(attributes&FILE_ATTRIBUTE_REPARSE_POINT);
|
||||
if(clear && !SetFileAttributesW(name.c_str(),attributes&~FILE_ATTRIBUTE_READONLY)) return failure();
|
||||
if(DeleteFileW(name.c_str())) return ok();
|
||||
const auto error=GetLastError();
|
||||
if(clear) SetFileAttributesW(name.c_str(),attributes);
|
||||
return failure(error);
|
||||
}
|
||||
if(action=="move")
|
||||
{
|
||||
const auto destination=path(request.at("target"));
|
||||
const bool replace=request.at("replace").get<bool>();
|
||||
if(!replace) return MoveFileExW(name.c_str(),destination.c_str(),0)?ok():failure();
|
||||
std::error_code error;
|
||||
if(fs::equivalent(name,destination,error)) return failure(ERROR_ALREADY_EXISTS);
|
||||
return replaceFile(name,destination,true);
|
||||
}
|
||||
if(action=="copy")
|
||||
{
|
||||
const auto destination=path(request.at("target"));
|
||||
if(remote && request.contains("size"))
|
||||
{
|
||||
// Both streams execute inside the serving process.
|
||||
FilesConnection source,target;
|
||||
streamFile(source,target,"same host",name,destination,request.at("size"),request.at("modified"),
|
||||
request.at("replace"),request.at("append"),progress);
|
||||
return ok();
|
||||
}
|
||||
return copy(name,destination,request.at("replace"),request.at("append"),progress);
|
||||
}
|
||||
if(action=="openRead")
|
||||
{
|
||||
input.close();
|
||||
input.value=CreateFileW(name.c_str(),GENERIC_READ,FILE_SHARE_READ,nullptr,OPEN_EXISTING,
|
||||
FILE_FLAG_OPEN_REPARSE_POINT|FILE_FLAG_SEQUENTIAL_SCAN,nullptr);
|
||||
if(input.value==INVALID_HANDLE_VALUE) return failure();
|
||||
BY_HANDLE_FILE_INFORMATION info{};
|
||||
if(!GetFileInformationByHandle(input.value,&info)) return failure();
|
||||
if(info.dwFileAttributes&(FILE_ATTRIBUTE_DIRECTORY|FILE_ATTRIBUTE_REPARSE_POINT))
|
||||
return failure(ERROR_ACCESS_DENIED);
|
||||
const auto size=fileSize(info.nFileSizeHigh,info.nFileSizeLow);
|
||||
const auto modified=fileSize(info.ftLastWriteTime.dwHighDateTime,info.ftLastWriteTime.dwLowDateTime);
|
||||
if(size!=request.at("size") || modified!=request.at("modified"))
|
||||
return failure(ERROR_FILE_INVALID);
|
||||
return ok();
|
||||
}
|
||||
if(action=="openWrite")
|
||||
{
|
||||
discard();
|
||||
target=name;
|
||||
const auto transfer=request.at("transfer").get<std::string>();
|
||||
if(transfer.size()!=64 || transfer.find_first_not_of("0123456789abcdef")!=transfer.npos)
|
||||
return failure(ERROR_INVALID_DATA);
|
||||
expectedSize=request.at("size").get<uint64_t>();
|
||||
const bool append=request.value("append",false);
|
||||
appendBase=0;
|
||||
auto staging=target.parent_path()/wide(".kswitcher-"+transfer+".partial");
|
||||
FileHandle original;
|
||||
if(append)
|
||||
{
|
||||
original.value=CreateFileW(target.c_str(),GENERIC_READ,FILE_SHARE_READ,nullptr,OPEN_EXISTING,0,nullptr);
|
||||
if(original.value==INVALID_HANDLE_VALUE) return failure();
|
||||
BY_HANDLE_FILE_INFORMATION info{};
|
||||
if(!GetFileInformationByHandle(original.value,&info)) return failure();
|
||||
appendBase=fileSize(info.nFileSizeHigh,info.nFileSizeLow);
|
||||
}
|
||||
output.value=CreateFileW(staging.c_str(),GENERIC_READ|GENERIC_WRITE,0,nullptr,OPEN_ALWAYS,
|
||||
FILE_ATTRIBUTE_HIDDEN|FILE_FLAG_OPEN_REPARSE_POINT,nullptr);
|
||||
if(output.value==INVALID_HANDLE_VALUE) return failure();
|
||||
const bool created=GetLastError()!=ERROR_ALREADY_EXISTS;
|
||||
BY_HANDLE_FILE_INFORMATION info{};
|
||||
if(!GetFileInformationByHandle(output.value,&info)) return failure();
|
||||
if(info.nNumberOfLinks!=1 || (info.dwFileAttributes&(FILE_ATTRIBUTE_DIRECTORY|FILE_ATTRIBUTE_REPARSE_POINT)))
|
||||
return failure(ERROR_ACCESS_DENIED);
|
||||
temporary=staging;
|
||||
resumable=true;
|
||||
if(created && appendBase)
|
||||
{
|
||||
Bytes buffer(FileChunk);
|
||||
for(uint64_t left=appendBase;left;)
|
||||
{
|
||||
const auto count=static_cast<DWORD>(std::min<uint64_t>(left,buffer.size()));
|
||||
DWORD read{},written{};
|
||||
if(!ReadFile(original.value,buffer.data(),count,&read,nullptr) || read!=count ||
|
||||
!WriteFile(output.value,buffer.data(),read,&written,nullptr) || written!=read)
|
||||
{
|
||||
discard(true);
|
||||
return failure(ERROR_WRITE_FAULT);
|
||||
}
|
||||
left-=written;
|
||||
}
|
||||
}
|
||||
const auto size=created?appendBase:fileSize(info.nFileSizeHigh,info.nFileSizeLow);
|
||||
const bool valid=size>=appendBase && size-appendBase<=expectedSize &&
|
||||
(!appendBase || hashPrefix(original.value,appendBase,progress)==hashPrefix(output.value,appendBase,progress));
|
||||
if(!valid)
|
||||
{
|
||||
discard(true);
|
||||
return {{"error",0},{"restart",true}};
|
||||
}
|
||||
const auto offset=size-appendBase;
|
||||
return {{"error",0},{"offset",offset},{"hash",hashPrefix(output.value,offset,progress,appendBase)}};
|
||||
}
|
||||
return failure(ERROR_INVALID_FUNCTION);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
struct FilesConnection::Impl
|
||||
{
|
||||
std::optional<Network> network;
|
||||
WinSocket socket;
|
||||
std::unique_ptr<Wire> wire;
|
||||
FileOperations local{false};
|
||||
Impl()=default;
|
||||
explicit Impl(const FilesPeer *peer)
|
||||
{
|
||||
if(!peer) return;
|
||||
const auto pairing=Json::parse(unprotect(readFile(dataDirectory()/L"lan-pairing.bin",256*1024)));
|
||||
if(!pairing.value("enabled",false) || !pairing.at("trusted").contains(peer->id))
|
||||
throw std::system_error(ERROR_ACCESS_DENIED,std::system_category());
|
||||
const auto id=pairing.at("id").get<std::string>();
|
||||
const auto key=pairing.at("trusted").at(peer->id).at("key").get<std::string>();
|
||||
connect(*peer,id,key);
|
||||
}
|
||||
void connect(const FilesPeer &peer,const std::string &id,const std::string &key)
|
||||
{
|
||||
network.emplace();
|
||||
socket.value=::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
|
||||
if(socket.value==INVALID_SOCKET) socketError();
|
||||
connectPeer(socket.value,peer);
|
||||
wire=std::make_unique<Wire>(socket.value);
|
||||
const auto nonce=randomHex(16);
|
||||
wire->write({{"version",1},{"client",id},{"server",peer.id},{"nonce",nonce}});
|
||||
const auto challenge=wire->read().at("challenge").get<std::string>();
|
||||
wire->authenticate(key,id,peer.id,challenge,nonce,true);
|
||||
wire->write({{"action","authenticate"}});
|
||||
checkFileReply(wire->read());
|
||||
}
|
||||
};
|
||||
|
||||
FilesConnection::FilesConnection(const FilesPeer *peer):impl(std::make_unique<Impl>(peer)){}
|
||||
FilesConnection::FilesConnection(const FilesPeer &peer,const std::string &local,const std::string &key):
|
||||
impl(std::make_unique<Impl>())
|
||||
{
|
||||
impl->connect(peer,local,key);
|
||||
}
|
||||
FilesConnection::~FilesConnection()=default;
|
||||
Json FilesConnection::call(const Json &request,const FileProgress &progress)
|
||||
{
|
||||
if(!impl->wire) return impl->local.call(request,progress);
|
||||
impl->wire->write(request);
|
||||
for(;;)
|
||||
{
|
||||
auto reply=impl->wire->read();
|
||||
if(!reply.contains("progress")) return reply;
|
||||
const auto &value=reply.at("progress");
|
||||
const bool proceed=!progress || progress(value[0].get<uint64_t>(),value[1].get<uint64_t>());
|
||||
impl->wire->write({{"continue",proceed}});
|
||||
}
|
||||
}
|
||||
|
||||
void streamFile(FilesConnection &source,FilesConnection &target,const std::string &sourceId,
|
||||
const std::filesystem::path &from,const std::filesystem::path &to,uint64_t size,uint64_t modified,
|
||||
bool replace,bool append,const FileProgress &progress)
|
||||
{
|
||||
auto openSource=fileRequest("openRead",from);
|
||||
openSource["size"]=size;
|
||||
openSource["modified"]=modified;
|
||||
checkFileReply(source.call(openSource));
|
||||
auto upload=fileRequest("openWrite",to);
|
||||
upload["append"]=append;
|
||||
upload["size"]=size;
|
||||
const auto identity=Json::array({sourceId,ks::utf8(from.native()),ks::utf8(to.native()),
|
||||
size,modified,append});
|
||||
upload["transfer"]=ks::lan::mac("KSwitcher partial v1",identity.dump());
|
||||
uint64_t done=0;
|
||||
const auto resumeProgress=[&](uint64_t,uint64_t){return !progress || progress(done,size);};
|
||||
try
|
||||
{
|
||||
for(int attempt=0;;++attempt)
|
||||
{
|
||||
auto reply=target.call(upload,resumeProgress);
|
||||
checkFileReply(reply);
|
||||
if(!reply.value("restart",false))
|
||||
{
|
||||
done=reply.at("offset");
|
||||
const auto sourceHash=source.call({{"action","hashRead"},{"size",done}},resumeProgress);
|
||||
checkFileReply(sourceHash);
|
||||
if(sourceHash.at("hash")==reply.at("hash")) break;
|
||||
checkFileReply(target.call({{"action","abort"}}));
|
||||
}
|
||||
if(attempt>0) throw std::system_error(ERROR_FILE_INVALID,std::system_category());
|
||||
}
|
||||
while(!progress || progress(done,size))
|
||||
{
|
||||
auto packet=source.call({{"action","read"}});
|
||||
checkFileReply(packet);
|
||||
auto &data=packet.at("data").get_binary();
|
||||
if(data.empty())
|
||||
{
|
||||
if(done!=size) throw std::system_error(ERROR_FILE_INVALID,std::system_category());
|
||||
checkFileReply(target.call({{"action","commit"},{"replace",replace},{"modified",modified}}));
|
||||
checkFileReply(source.call({{"action","closeRead"}}));
|
||||
return;
|
||||
}
|
||||
const auto chunkSize=data.size();
|
||||
checkFileReply(target.call({{"action","write"},{"data",std::move(packet.at("data"))}}));
|
||||
done+=chunkSize;
|
||||
}
|
||||
throw std::system_error(ERROR_CANCELLED,std::system_category());
|
||||
}
|
||||
catch(const std::system_error &error)
|
||||
{
|
||||
if(error.code().value()==ERROR_CANCELLED)
|
||||
checkFileReply(target.call({{"action","abort"}}));
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
struct FilesListener::Impl
|
||||
{
|
||||
struct Client
|
||||
{
|
||||
WinSocket socket;
|
||||
std::atomic<bool> done{};
|
||||
std::thread worker;
|
||||
~Client()
|
||||
{
|
||||
shutdown(socket.value,SD_BOTH);
|
||||
if(worker.joinable()) worker.join();
|
||||
}
|
||||
};
|
||||
std::string local;
|
||||
PairKey key;
|
||||
WinSocket listener;
|
||||
std::vector<std::unique_ptr<Client>> clients;
|
||||
uint16_t boundPort{};
|
||||
Impl(std::string id,PairKey lookup,uint16_t port):local(std::move(id)),key(std::move(lookup))
|
||||
{
|
||||
listener.value=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
|
||||
if(listener.value==INVALID_SOCKET) return;
|
||||
BOOL exclusive=TRUE;
|
||||
u_long mode=1;
|
||||
sockaddr_in address{};
|
||||
address.sin_family=AF_INET;
|
||||
address.sin_port=htons(port);
|
||||
if(setsockopt(listener.value,SOL_SOCKET,SO_EXCLUSIVEADDRUSE,reinterpret_cast<const char*>(&exclusive),sizeof(exclusive)) ||
|
||||
ioctlsocket(listener.value,FIONBIO,&mode) ||
|
||||
bind(listener.value,reinterpret_cast<sockaddr*>(&address),sizeof(address)) ||
|
||||
listen(listener.value,8))
|
||||
{
|
||||
closesocket(std::exchange(listener.value,INVALID_SOCKET));
|
||||
return;
|
||||
}
|
||||
int length=sizeof(address);
|
||||
if(!getsockname(listener.value,reinterpret_cast<sockaddr*>(&address),&length))
|
||||
boundPort=ntohs(address.sin_port);
|
||||
}
|
||||
void serve(SOCKET socket)
|
||||
{
|
||||
Wire wire(socket);
|
||||
const auto hello=wire.read();
|
||||
const auto peer=hello.at("client").get<std::string>();
|
||||
const auto nonce=hello.at("nonce").get<std::string>();
|
||||
if(hello.at("version")!=1 || hello.at("server")!=local || !identifier(peer) || !identifier(nonce)) return;
|
||||
const auto secret=key(peer);
|
||||
if(secret.empty()) return;
|
||||
const auto challenge=randomHex(16);
|
||||
wire.write({{"challenge",challenge}});
|
||||
wire.authenticate(secret,peer,local,challenge,nonce,false);
|
||||
if(wire.read().at("action")!="authenticate") return;
|
||||
wire.write(ok());
|
||||
FileOperations operations(true);
|
||||
for(;;)
|
||||
{
|
||||
const auto request=wire.read(true);
|
||||
if(!equal(key(peer),secret)) return;
|
||||
wire.write(operations.call(request,[&](uint64_t done,uint64_t total)
|
||||
{
|
||||
if(!equal(key(peer),secret))
|
||||
throw std::system_error(ERROR_ACCESS_DENIED,std::system_category());
|
||||
wire.write({{"progress",{done,total}}});
|
||||
const bool proceed=wire.read(true).value("continue",false);
|
||||
if(!equal(key(peer),secret)) throw std::system_error(ERROR_ACCESS_DENIED,std::system_category());
|
||||
return proceed;
|
||||
}));
|
||||
}
|
||||
}
|
||||
void poll()
|
||||
{
|
||||
std::erase_if(clients,[](const auto &client){return client->done.load();});
|
||||
if(listener.value==INVALID_SOCKET || clients.size()>=16) return;
|
||||
sockaddr_in address{};
|
||||
int size=sizeof(address);
|
||||
WinSocket accepted{accept(listener.value,reinterpret_cast<sockaddr*>(&address),&size)};
|
||||
const auto ip=ntohl(address.sin_addr.s_addr);
|
||||
if((ip>>24)!=10 && (ip>>24)!=127 && (ip>>20)!=0xac1 && (ip>>16)!=0xc0a8 && (ip>>16)!=0xa9fe)
|
||||
return;
|
||||
if(accepted.value==INVALID_SOCKET) return;
|
||||
u_long mode=0;
|
||||
if(ioctlsocket(accepted.value,FIONBIO,&mode)) return;
|
||||
auto client=std::make_unique<Client>();
|
||||
client->socket.value=std::exchange(accepted.value,INVALID_SOCKET);
|
||||
auto *connection=client.get();
|
||||
connection->worker=std::thread([this,connection]
|
||||
{
|
||||
try {serve(connection->socket.value);} catch(const std::exception &) {}
|
||||
connection->done=true;
|
||||
});
|
||||
clients.push_back(std::move(client));
|
||||
}
|
||||
};
|
||||
|
||||
FilesListener::FilesListener(std::string local,PairKey key,uint16_t port):
|
||||
impl(std::make_unique<Impl>(std::move(local),std::move(key),port)){}
|
||||
FilesListener::~FilesListener()=default;
|
||||
uint16_t FilesListener::port()const{return impl->boundPort;}
|
||||
void FilesListener::poll(){impl->poll();}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
#include "lan_pairing.h"
|
||||
#include <functional>
|
||||
|
||||
namespace ks::lan
|
||||
{
|
||||
inline constexpr uint16_t FilesPort=45885;
|
||||
struct FilesPeer
|
||||
{
|
||||
std::string id, address;
|
||||
std::wstring name;
|
||||
uint16_t port=FilesPort;
|
||||
bool filesBrowse{};
|
||||
};
|
||||
using FileProgress = std::function<bool(uint64_t, uint64_t)>;
|
||||
inline constexpr size_t FileChunk = 64 * 1024;
|
||||
|
||||
// One connection owns its enumeration, file handles and uncommitted upload.
|
||||
class FilesConnection
|
||||
{
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl;
|
||||
public:
|
||||
explicit FilesConnection(const FilesPeer *peer = nullptr);
|
||||
FilesConnection(const FilesPeer &peer,const std::string &local,const std::string &key);
|
||||
~FilesConnection();
|
||||
Json call(const Json &request, const FileProgress &progress = {});
|
||||
};
|
||||
|
||||
void streamFile(FilesConnection &source,FilesConnection &target,const std::string &sourceId,
|
||||
const std::filesystem::path &from,const std::filesystem::path &to,uint64_t size,uint64_t modified,
|
||||
bool replace,bool append,const FileProgress &progress);
|
||||
|
||||
class FilesListener
|
||||
{
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl;
|
||||
public:
|
||||
using PairKey = std::function<std::string(const std::string &)>;
|
||||
FilesListener(std::string local, PairKey key,uint16_t port=FilesPort);
|
||||
uint16_t port() const;
|
||||
~FilesListener();
|
||||
void poll();
|
||||
};
|
||||
|
||||
Json fileRequest(std::string_view action, const std::filesystem::path &path);
|
||||
std::filesystem::path filePath(const Json &path);
|
||||
void checkFileReply(const Json &reply);
|
||||
bool recycleFile(HWND owner, const std::filesystem::path &path);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
param([string]$Executable = "$PSScriptRoot/../build/files-dev/KSwitcher.Files.exe")
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Add-Type @'
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Runtime.InteropServices;
|
||||
public static class FilesDriveShortcutTest {
|
||||
[DllImport("user32.dll")] public static extern IntPtr GetDlgItem(IntPtr window, int id);
|
||||
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr window);
|
||||
[DllImport("user32.dll")] public static extern bool PostMessage(IntPtr window, uint msg, IntPtr w, IntPtr l);
|
||||
[DllImport("user32.dll", CharSet=CharSet.Unicode)]
|
||||
public static extern IntPtr SendMessage(IntPtr window, uint msg, IntPtr w, StringBuilder text);
|
||||
}
|
||||
'@
|
||||
function Wait-Until([scriptblock]$Condition) {
|
||||
for ($attempt = 0; $attempt -lt 60; $attempt++) {
|
||||
if (& $Condition) { return }
|
||||
Start-Sleep -Milliseconds 50
|
||||
}
|
||||
throw "Timed out waiting for: $Condition"
|
||||
}
|
||||
function Read-Text($Control, $Message = 13, $Parameter = 32768) {
|
||||
$buffer = [Text.StringBuilder]::new(32768)
|
||||
[void][FilesDriveShortcutTest]::SendMessage($Control, $Message, [IntPtr]$Parameter, $buffer)
|
||||
$buffer.ToString()
|
||||
}
|
||||
$process = Start-Process -FilePath (Resolve-Path $Executable) -PassThru
|
||||
try {
|
||||
Wait-Until { $process.Refresh(); $process.MainWindowHandle -ne [IntPtr]::Zero }
|
||||
$window = $process.MainWindowHandle
|
||||
$picker = [FilesDriveShortcutTest]::GetDlgItem($window, 121)
|
||||
$path = [FilesDriveShortcutTest]::GetDlgItem($window, 142)
|
||||
function Open-Picker {
|
||||
[void][FilesDriveShortcutTest]::PostMessage($window, 260, [IntPtr]112, [IntPtr]::Zero)
|
||||
Wait-Until { [FilesDriveShortcutTest]::IsWindowVisible($picker) }
|
||||
}
|
||||
function Press-Shortcut([char]$Key) {
|
||||
[void][FilesDriveShortcutTest]::PostMessage($picker, 258, [IntPtr][int]$Key, [IntPtr]::Zero)
|
||||
Wait-Until { ![FilesDriveShortcutTest]::IsWindowVisible($picker) }
|
||||
}
|
||||
Open-Picker
|
||||
$count = [FilesDriveShortcutTest]::SendMessage($picker, 395, [IntPtr]::Zero, $null).ToInt32()
|
||||
$rows = @(for ($i = 0; $i -lt $count; $i++) { Read-Text $picker 393 $i })
|
||||
$disk = $rows | Where-Object { $_ -match '^[A-Z]: fixed\t' } | Select-Object -First 1
|
||||
if (!$disk) { throw 'No fixed drive available for the shortcut test.' }
|
||||
Press-Shortcut ([char]::ToLowerInvariant($disk[0]))
|
||||
$root = $disk.Substring(0, 2) + '\'
|
||||
Wait-Until { (Read-Text $path) -eq $root }
|
||||
Write-Output 'Drive letter opened the drive immediately.'
|
||||
|
||||
Open-Picker
|
||||
$count = [FilesDriveShortcutTest]::SendMessage($picker, 395, [IntPtr]::Zero, $null).ToInt32()
|
||||
$rows = @(for ($i = 0; $i -lt $count; $i++) { Read-Text $picker 393 $i })
|
||||
for ($round = 1; $round -le 2; $round++) {
|
||||
Wait-Until {
|
||||
$count = [FilesDriveShortcutTest]::SendMessage($picker, 395, [IntPtr]::Zero, $null).ToInt32()
|
||||
$script:remoteRows = @(for ($i = 0; $i -lt $count; $i++) { Read-Text $picker 393 $i })
|
||||
[bool]($script:remoteRows | Where-Object { $_ -match '^ [A-Z]: ' })
|
||||
}
|
||||
if ((Read-Text $path) -ne $root) { throw 'Loading the remote drives changed the local path.' }
|
||||
[void][FilesDriveShortcutTest]::PostMessage($picker, 256, [IntPtr]27, [IntPtr]::Zero)
|
||||
Wait-Until { ![FilesDriveShortcutTest]::IsWindowVisible($picker) }
|
||||
if ($round -eq 1) { Open-Picker }
|
||||
}
|
||||
Write-Output 'Remote drives appeared automatically on both opens.'
|
||||
} finally {
|
||||
if (!$process.HasExited) { [void]$process.CloseMainWindow() }
|
||||
}
|
||||
@@ -97,6 +97,10 @@ int main(int argc,char **argv)
|
||||
LanService restored(root);auto state=request(restored,{{"action","status"}});
|
||||
check(state["peers"].size()==1 && state["peers"][0].at("paired").get<bool>() && !state["peers"][0].at("online").get<bool>(),"saved pair remains visible offline");
|
||||
check(state.dump().find(left.derived)==std::string::npos,"status never exposes stored pair keys");
|
||||
const auto filesPeers=request(restored,{{"action","filesPeers"}});
|
||||
check(filesPeers.at("peers").size()==1 && filesPeers.at("peers")[0].at("id")==b &&
|
||||
filesPeers.at("peers")[0].at("address")=="",
|
||||
"Files refresh returns the saved pair before discovery finds an address");
|
||||
rejects([&]{request(restored,{{"action","input"},{"enabled",true},{"peer",b},{"edge",1}});},"input cannot be enabled without discovery");
|
||||
rejects([&]{request(restored,{{"action","input"},{"enabled",false},{"edge",4}});},"invalid screen edge rejected");
|
||||
auto position=request(restored,{{"action","input"},{"enabled",false},{"peer",b},{"edge",2}});
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
#include <winsock2.h>
|
||||
#include <sddl.h>
|
||||
#include "lan_files.h"
|
||||
#include <bit>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
using namespace ks;
|
||||
using namespace ks::lan;
|
||||
namespace fs=std::filesystem;
|
||||
|
||||
namespace
|
||||
{
|
||||
void require(bool condition,const char *message)
|
||||
{
|
||||
if(!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
void save(const fs::path &path,const std::string &text)
|
||||
{
|
||||
std::ofstream file(path,std::ios::binary|std::ios::trunc);
|
||||
file.write(text.data(),text.size());
|
||||
require(bool(file),"write test fixture");
|
||||
}
|
||||
|
||||
std::string load(const fs::path &path)
|
||||
{
|
||||
std::ifstream file(path,std::ios::binary);
|
||||
return {std::istreambuf_iterator<char>(file),{}};
|
||||
}
|
||||
|
||||
void verifyDirection(const FilesPeer *sourcePeer,const FilesPeer *targetPeer,
|
||||
const std::string &id,const std::string &key,const fs::path &root)
|
||||
{
|
||||
auto connect=[&](const FilesPeer *peer)
|
||||
{
|
||||
return peer?std::make_unique<FilesConnection>(*peer,id,key):std::make_unique<FilesConnection>();
|
||||
};
|
||||
const auto sourcePath=root/L"source.bin";
|
||||
const auto targetPath=root/L"target.bin";
|
||||
std::string content(FileChunk*3+5,'a');
|
||||
for(size_t i=0;i<content.size();++i) content[i]=static_cast<char>((i/257)%251);
|
||||
save(sourcePath,content);
|
||||
save(targetPath,"original");
|
||||
auto source=connect(sourcePeer);
|
||||
const auto info=source->call(fileRequest("stat",sourcePath));
|
||||
checkFileReply(info);
|
||||
const auto size=info.at("size").get<uint64_t>(),modified=info.at("modified").get<uint64_t>();
|
||||
auto target=connect(targetPeer);
|
||||
try
|
||||
{
|
||||
streamFile(*source,*target,id,sourcePath,targetPath,size,modified,true,false,
|
||||
[](uint64_t done,uint64_t)
|
||||
{
|
||||
if(done==FileChunk) throw std::system_error(WSAECONNRESET,std::system_category());
|
||||
return true;
|
||||
});
|
||||
require(false,"interruption expected");
|
||||
}
|
||||
catch(const std::system_error &e){require(e.code().value()==WSAECONNRESET,"unexpected transfer failure");}
|
||||
target.reset();
|
||||
source.reset();
|
||||
require(load(targetPath)=="original","interruption overwrote target");
|
||||
require(load(sourcePath)==content,"interruption changed source");
|
||||
fs::path partial;
|
||||
for(const auto &entry:fs::directory_iterator(root))
|
||||
if(entry.path().extension()==L".partial") partial=entry.path();
|
||||
require(!partial.empty() && fs::file_size(partial)==FileChunk,"confirmed chunk was lost");
|
||||
|
||||
source=connect(sourcePeer);
|
||||
target=connect(targetPeer);
|
||||
uint64_t offset=UINT64_MAX;
|
||||
streamFile(*source,*target,id,sourcePath,targetPath,size,modified,true,false,
|
||||
[&](uint64_t done,uint64_t){offset=std::min(offset,done);return true;});
|
||||
require(offset==FileChunk,"resumed at the wrong offset");
|
||||
require(load(targetPath)==content,"resumed file differs");
|
||||
require(!fs::exists(partial),"committed partial remains");
|
||||
require(load(sourcePath)==content,"copy removed source");
|
||||
|
||||
save(targetPath,"original");
|
||||
try
|
||||
{
|
||||
streamFile(*source,*target,id,sourcePath,targetPath,size,modified,true,false,
|
||||
[](uint64_t done,uint64_t){return done<FileChunk;});
|
||||
require(false,"cancellation expected");
|
||||
}
|
||||
catch(const std::system_error &e){require(e.code().value()==ERROR_CANCELLED,"unexpected cancellation error");}
|
||||
require(load(targetPath)=="original" && !fs::exists(partial),"cancel touched original or retained partial");
|
||||
|
||||
save(partial,std::string(FileChunk,'z'));
|
||||
uint64_t minimum=UINT64_MAX;
|
||||
streamFile(*source,*target,id,sourcePath,targetPath,size,modified,true,false,
|
||||
[&](uint64_t done,uint64_t){minimum=std::min(minimum,done);return true;});
|
||||
require(minimum==0 && load(targetPath)==content,"corrupt partial was reused");
|
||||
|
||||
save(targetPath,"original");
|
||||
streamFile(*source,*target,id,sourcePath,targetPath,size,modified,true,true,{});
|
||||
require(load(targetPath)=="original"+content,"append did not preserve original prefix");
|
||||
checkFileReply(source->call(fileRequest("remove",sourcePath)));
|
||||
require(!fs::exists(sourcePath),"move source removal failed");
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
WSADATA network{};
|
||||
if(WSAStartup(MAKEWORD(2,2),&network)) return 1;
|
||||
const auto root=fs::temp_directory_path()/wide("kswitcher-lan-files-"+randomHex(16));
|
||||
int result=0;
|
||||
try
|
||||
{
|
||||
fs::create_directory(root);
|
||||
const auto client=randomHex(16),first=randomHex(16),second=randomHex(16),key=randomHex(32);
|
||||
auto lookup=[&](const std::string &id){return id==client?key:std::string{};};
|
||||
FilesListener a(first,lookup,0),b(second,lookup,0);
|
||||
require(a.port() && b.port() && a.port()!=b.port(),"test listeners unavailable");
|
||||
std::jthread polling([&](std::stop_token stop)
|
||||
{
|
||||
while(!stop.stop_requested()){a.poll();b.poll();Sleep(5);}
|
||||
});
|
||||
const FilesPeer one{first,"127.0.0.1",L"One",a.port()},two{second,"127.0.0.1",L"Two",b.port()};
|
||||
for(const auto &direction:{std::pair<const FilesPeer*,const FilesPeer*>{nullptr,&one},
|
||||
{&one,nullptr},{&one,&two}})
|
||||
verifyDirection(direction.first,direction.second,client,key,root);
|
||||
|
||||
try
|
||||
{
|
||||
FilesConnection unpaired(one,randomHex(16),key);
|
||||
require(false,"unpaired client authenticated");
|
||||
}
|
||||
catch(const std::system_error &){}
|
||||
FilesConnection host(one,client,key);
|
||||
const auto drives=host.call({{"action","drives"}});
|
||||
checkFileReply(drives);
|
||||
require(drives.at("drives").size()==static_cast<size_t>(std::popcount(GetLogicalDrives())),"drive mask differs");
|
||||
const auto denied=host.call(fileRequest("stat",root/L"NUL"));
|
||||
require(denied.at("error")==ERROR_INVALID_NAME,"reserved device accepted");
|
||||
// A deny ACE on file data applies to the resident's Windows token.
|
||||
const auto protectedFile=root/L"protected.bin";
|
||||
save(protectedFile,"restricted");
|
||||
const auto protectedInfo=host.call(fileRequest("stat",protectedFile));
|
||||
checkFileReply(protectedInfo);
|
||||
PSECURITY_DESCRIPTOR descriptor{};
|
||||
require(ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||||
L"D:(D;;0x1;;;WD)(A;;GA;;;WD)",SDDL_REVISION_1,&descriptor,nullptr)!=FALSE,
|
||||
"create test DACL");
|
||||
const auto applied=SetFileSecurityW(protectedFile.c_str(),DACL_SECURITY_INFORMATION,descriptor);
|
||||
LocalFree(descriptor);
|
||||
require(applied!=FALSE,"apply test DACL");
|
||||
auto protectedRead=fileRequest("openRead",protectedFile);
|
||||
protectedRead["size"]=protectedInfo.at("size");
|
||||
protectedRead["modified"]=protectedInfo.at("modified");
|
||||
require(host.call(protectedRead).at("error")==ERROR_ACCESS_DENIED,"Windows DACL did not deny remote read");
|
||||
checkFileReply(host.call(fileRequest("remove",protectedFile)));
|
||||
const auto tree=root/L"tree",file=tree/L"original.bin",copy=tree/L"copy.bin",renamed=tree/L"renamed.bin";
|
||||
checkFileReply(host.call(fileRequest("mkdir",tree)));
|
||||
save(file,"same host");
|
||||
auto operation=fileRequest("copy",file);
|
||||
operation["target"]=utf8(copy.native());
|
||||
operation["replace"]=false;
|
||||
operation["append"]=false;
|
||||
save(file,std::string(FileChunk*2,'s'));
|
||||
const auto largeInfo=host.call(fileRequest("stat",file));
|
||||
operation["size"]=largeInfo.at("size");
|
||||
operation["modified"]=largeInfo.at("modified");
|
||||
{
|
||||
FilesConnection interrupted(one,client,key);
|
||||
try
|
||||
{
|
||||
interrupted.call(operation,[](uint64_t done,uint64_t)
|
||||
{
|
||||
if(done==FileChunk) throw std::system_error(WSAECONNRESET,std::system_category());
|
||||
return true;
|
||||
});
|
||||
require(false,"same-host interruption expected");
|
||||
}
|
||||
catch(const std::system_error &e){require(e.code().value()==WSAECONNRESET,"same-host interruption error");}
|
||||
}
|
||||
require(!fs::exists(copy),"same-host interruption published a partial file");
|
||||
uint64_t firstOffset=UINT64_MAX;
|
||||
for(int i=0;i<100;++i)
|
||||
{
|
||||
const auto reply=host.call(operation,[&](uint64_t done,uint64_t)
|
||||
{
|
||||
firstOffset=std::min(firstOffset,done);
|
||||
return true;
|
||||
});
|
||||
if(reply.at("error")==ERROR_SHARING_VIOLATION){Sleep(10);continue;}
|
||||
checkFileReply(reply);
|
||||
break;
|
||||
}
|
||||
require(firstOffset==FileChunk && load(copy)==load(file),"same-host copy did not resume");
|
||||
operation=fileRequest("move",copy);
|
||||
operation["target"]=utf8(renamed.native());
|
||||
operation["replace"]=false;
|
||||
checkFileReply(host.call(operation));
|
||||
require(!fs::exists(copy) && load(renamed)==load(file),"same-host rename failed");
|
||||
checkFileReply(host.call(fileRequest("remove",renamed)));
|
||||
checkFileReply(host.call(fileRequest("remove",file)));
|
||||
checkFileReply(host.call(fileRequest("rmdir",tree)));
|
||||
require(!fs::exists(tree),"same-host directory remains");
|
||||
}
|
||||
catch(const std::exception &e){std::cerr<<e.what()<<'\n';result=1;}
|
||||
std::error_code ignored;
|
||||
fs::remove_all(root,ignored);
|
||||
WSACleanup();
|
||||
if(!result) std::cout<<"LAN file transfer regressions passed.\n";
|
||||
return result;
|
||||
}
|
||||
Reference in new issue
Block a user