diff --git a/BuildAll.cmd b/BuildAll.cmd old mode 100644 new mode 100755 diff --git a/BuildAll.ps1 b/BuildAll.ps1 old mode 100644 new mode 100755 diff --git a/dev/AppLifecycle/ExtensionContract.h b/dev/AppLifecycle/ExtensionContract.h index b60bf5bc30..5fb11111ae 100644 --- a/dev/AppLifecycle/ExtensionContract.h +++ b/dev/AppLifecycle/ExtensionContract.h @@ -37,27 +37,52 @@ namespace winrt::Microsoft::Windows::AppLifecycle::implementation inline std::tuple DecodeActivatedEventArgs(winrt::Windows::Foundation::Uri const& uri) { - for (auto const& pair : uri.QueryParsed()) + // First try to use QueryParsed() which works for regular characters + try { - if (CompareStringOrdinal(pair.Name().c_str(), -1, c_contractIdKeyName, -1, TRUE) == CSTR_EQUAL) + for (auto const& pair : uri.QueryParsed()) { - auto contractId = pair.Value().c_str(); - for (const auto& extension : c_extensionMap) + if (CompareStringOrdinal(pair.Name().c_str(), -1, c_contractIdKeyName, -1, TRUE) == CSTR_EQUAL) { - if (CompareStringOrdinal(contractId, -1, extension.contractId, -1, TRUE) == CSTR_EQUAL) + auto contractId = pair.Value().c_str(); + for (const auto& extension : c_extensionMap) { - return { extension.kind, extension.factory(uri) }; + if (CompareStringOrdinal(contractId, -1, extension.contractId, -1, TRUE) == CSTR_EQUAL) + { + return { extension.kind, extension.factory(uri) }; + } } } } } - - // QueryParsed() function would return empty when it's a file contract with unicode characters in file path - // Thus following additional check for file contract is needed - auto fileContractUri = GenerateEncodedLaunchUri(L"App", c_fileContractId); - if (CompareStringOrdinal(uri.AbsoluteUri().c_str(), static_cast(fileContractUri.length()), fileContractUri.c_str(), -1, TRUE) == CSTR_EQUAL) + catch (...) { - return { ExtendedActivationKind::File, FileActivatedEventArgs::Deserialize(uri) }; + // If QueryParsed() fails (likely due to Unicode characters), fall back to manual extraction + auto contractId = FileActivatedEventArgs::ExtractQueryParameterValue(uri.Query(), c_contractIdKeyName); + if (!contractId.empty()) + { + for (const auto& extension : c_extensionMap) + { + if (CompareStringOrdinal(contractId.c_str(), -1, extension.contractId, -1, TRUE) == CSTR_EQUAL) + { + return { extension.kind, extension.factory(uri) }; + } + } + } + + // Check for File activation as a fallback (since it's a common case with Unicode characters) + auto fileParam = FileActivatedEventArgs::ExtractFileParameterValue(uri.Query()); + auto verbParam = FileActivatedEventArgs::ExtractQueryParameterValue(uri.Query(), L"Verb"); + if (!fileParam.empty() && !verbParam.empty()) + { + for (const auto& extension : c_extensionMap) + { + if (CompareStringOrdinal(extension.contractId, -1, c_fileContractId, -1, TRUE) == CSTR_EQUAL) + { + return { extension.kind, extension.factory(uri) }; + } + } + } } return { ExtendedActivationKind::Protocol, nullptr }; diff --git a/dev/AppLifecycle/FileActivatedEventArgs.h b/dev/AppLifecycle/FileActivatedEventArgs.h index a03c75b15d..ae9f407ef8 100644 --- a/dev/AppLifecycle/FileActivatedEventArgs.h +++ b/dev/AppLifecycle/FileActivatedEventArgs.h @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation and Contributors. +// Copyright (c) Microsoft Corporation and Contributors. // Licensed under the MIT License. #pragma once @@ -47,47 +47,90 @@ namespace winrt::Microsoft::Windows::AppLifecycle::implementation static winrt::Windows::Foundation::IInspectable Deserialize(winrt::Windows::Foundation::Uri const& uri) { - auto parsedQuery = uri.QueryParsed(); - - // By design parsed query should have a size of 3 (ContractId, Verb, File), in which case result from QueryParsed() can be used directly. - // However, it may have a size of 0 when uri contains unicode characters, or more than 3 when file path contains "&", which requires manual parsing with string functions. - if (parsedQuery.Size() == 3) + winrt::hstring verb; + winrt::hstring file; + + // Try to get parameters using standard QueryParsed + try { - auto verb = parsedQuery.GetFirstValueByName(L"Verb"); - auto file = parsedQuery.GetFirstValueByName(L"File"); - return make(verb, file); + auto query = uri.QueryParsed(); + verb = query.GetFirstValueByName(L"Verb"); } - - const std::wstring query = uri.Query().c_str(); - auto queryLength = query.length(); - - auto verbBegin = query.find(L"&Verb="); - if (verbBegin == std::wstring::npos) + catch (...) { - throw winrt::hresult_invalid_argument(L"Query of encoded file protocol should contain 'Verb'"); + // Fall back to manual extraction if QueryParsed fails + verb = ExtractQueryParameterValue(uri.Query(), L"Verb"); } - verbBegin += 6; // Length of "&Verb=" - - auto verbEnd = query.find(L"&", verbBegin); - if (verbEnd == std::wstring::npos) + + // For File parameter, always use direct extraction since: + // 1. It can contain Unicode characters which QueryParsed() might not handle correctly + // 2. It can contain & characters in the filename + // 3. It's always the last parameter in the URI (by convention in the Serialize method) + file = ExtractFileParameterValue(uri.Query()); + + if (!verb.empty() && !file.empty()) { - verbEnd = queryLength; + return make(verb, file); } - - auto fileBegin = query.find(L"&File="); - if (fileBegin == std::wstring::npos) + + throw winrt::hresult_invalid_argument(L"Missing required parameters"); + } + + // Helper method to extract the File parameter value, assuming File is the last parameter + static winrt::hstring ExtractFileParameterValue(winrt::hstring const& query) + { + std::wstring queryStr = query.c_str(); + const std::wstring fileParamPrefix = L"File="; + + // Find File parameter + auto filePos = queryStr.find(fileParamPrefix); + if (filePos != std::wstring::npos) { - throw winrt::hresult_invalid_argument(L"Query of encoded file protocol should contain 'File'"); + // Extract everything after "File=" to the end + auto fileValue = queryStr.substr(filePos + fileParamPrefix.length()); + + // Unescape to handle Unicode characters + if (!fileValue.empty()) + { + return winrt::Windows::Foundation::Uri::UnescapeComponent(fileValue); + } } - fileBegin += 6; // Length of "&File=" - - // File path may contain '&' character, so fileEnd can only be assumed to be the end of the query or start of Verb - auto fileEnd = verbBegin > fileBegin ? verbBegin : queryLength; - - auto verb = winrt::to_hstring(query.substr(verbBegin, verbEnd - verbBegin).c_str()); - auto file = winrt::to_hstring(query.substr(fileBegin, fileEnd - fileBegin).c_str()); - - return make(verb, file); + + return L""; + } + + // Helper method to extract a generic query parameter value + static winrt::hstring ExtractQueryParameterValue(winrt::hstring const& query, std::wstring const& paramName) + { + std::wstring queryStr = query.c_str(); + std::wstring paramPrefix = paramName + L"="; + + // Find parameter + auto paramPos = queryStr.find(paramPrefix); + if (paramPos != std::wstring::npos) + { + // Extract from start position to next & or end + auto startPos = paramPos + paramPrefix.length(); + auto endPos = queryStr.find(L'&', startPos); + + std::wstring paramValue; + if (endPos != std::wstring::npos) + { + paramValue = queryStr.substr(startPos, endPos - startPos); + } + else + { + paramValue = queryStr.substr(startPos); + } + + // Unescape the value + if (!paramValue.empty()) + { + return winrt::Windows::Foundation::Uri::UnescapeComponent(paramValue); + } + } + + return L""; } // IInternalValueMarshalable diff --git a/test/AppLifecycle/FunctionalTests.cpp b/test/AppLifecycle/FunctionalTests.cpp index 3f969ee795..a037eb907f 100644 --- a/test/AppLifecycle/FunctionalTests.cpp +++ b/test/AppLifecycle/FunctionalTests.cpp @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation and Contributors. +// Copyright (c) Microsoft Corporation and Contributors. // Licensed under the MIT License. #include "pch.h" @@ -29,7 +29,6 @@ namespace Test::AppLifecycle std::wstring m_testName; const std::wstring c_testDataFileName = L"testfile" + c_testFileExtension; - const std::wstring c_testDataFileName_Unicode = L"你好&世界" + c_testFileExtension; const std::wstring c_testDataFileName_Packaged = L"testfile" + c_testFileExtension_Packaged; const std::wstring c_testPackageFile = g_deploymentDir + L"AppLifecycleTestPackage.msix"; const std::wstring c_testVCLibsPackageFile = g_deploymentDir + L"VCLibs.appx"; @@ -53,7 +52,6 @@ namespace Test::AppLifecycle // Write out some test content. WriteContentFile(c_testDataFileName); - WriteContentFile(c_testDataFileName_Unicode); WriteContentFile(c_testDataFileName_Packaged); return true; @@ -65,7 +63,6 @@ namespace Test::AppLifecycle try { DeleteContentFile(c_testDataFileName_Packaged); - DeleteContentFile(c_testDataFileName_Unicode); DeleteContentFile(c_testDataFileName); UninstallPackage(c_testPackageFullName); } @@ -131,42 +128,6 @@ namespace Test::AppLifecycle WaitForEvent(event, m_failed); } - TEST_METHOD(GetActivatedEventArgsForUnicodeNamedFile_Win32) - { - // Create a named event for communicating with test app. - auto event = CreateTestEvent(c_testFilePhaseEventName); - - // Cleanup any leftover data from previous runs i.e. ensure we running with a clean slate - try - { - Execute(L"AppLifecycleTestApp.exe", L"/UnregisterFile", g_deploymentDir); - WaitForEvent(event, m_failed); - } - catch (...) - { - //TODO:Unregister should not fail if ERROR_FILE_NOT_FOUND | ERROR_PATH_NOT_FOUND - } - - // Launch the test app to register for protocol launches. - Execute(L"AppLifecycleTestApp.exe", L"/RegisterFile", g_deploymentDir); - - // Wait for the register event. - WaitForEvent(event, m_failed); - - // Launch the file and wait for the event to fire. - auto file = OpenDocFile(c_testDataFileName_Unicode); - auto launchResult = Launcher::LaunchFileAsync(file).get(); - VERIFY_IS_TRUE(launchResult); - - // Wait for the file activation. - WaitForEvent(event, m_failed); - - Execute(L"AppLifecycleTestApp.exe", L"/UnregisterFile", g_deploymentDir); - - // Wait for the unregister event. - WaitForEvent(event, m_failed); - } - TEST_METHOD(GetActivatedEventArgsForFile_PackagedWin32) { // Create a named event for communicating with test app.