Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file modified BuildAll.cmd
100644 → 100755
Empty file.
Empty file modified BuildAll.ps1
100644 → 100755
Empty file.
49 changes: 37 additions & 12 deletions dev/AppLifecycle/ExtensionContract.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,27 +37,52 @@ namespace winrt::Microsoft::Windows::AppLifecycle::implementation

inline std::tuple<ExtendedActivationKind, winrt::Windows::Foundation::IInspectable> 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<int>(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 };
Expand Down
111 changes: 77 additions & 34 deletions dev/AppLifecycle/FileActivatedEventArgs.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation and Contributors.
// Copyright (c) Microsoft Corporation and Contributors.
// Licensed under the MIT License.
#pragma once

Expand Down Expand Up @@ -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<FileActivatedEventArgs>(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<FileActivatedEventArgs>(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<FileActivatedEventArgs>(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
Expand Down
41 changes: 1 addition & 40 deletions test/AppLifecycle/FunctionalTests.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation and Contributors.
// Copyright (c) Microsoft Corporation and Contributors.
// Licensed under the MIT License.

#include "pch.h"
Expand Down Expand Up @@ -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";
Expand All @@ -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;
Expand All @@ -65,7 +63,6 @@ namespace Test::AppLifecycle
try
{
DeleteContentFile(c_testDataFileName_Packaged);
DeleteContentFile(c_testDataFileName_Unicode);
DeleteContentFile(c_testDataFileName);
UninstallPackage(c_testPackageFullName);
}
Expand Down Expand Up @@ -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.
Expand Down