diff --git a/doc/ReleaseNotes.md b/doc/ReleaseNotes.md index 6b2029d84d..9557985291 100644 --- a/doc/ReleaseNotes.md +++ b/doc/ReleaseNotes.md @@ -49,6 +49,7 @@ The PowerShell module now automatically uses `GH_TOKEN` or `GITHUB_TOKEN` enviro ## Bug Fixes +* `winget export` now works when the destination path is a hidden file * Fixed the `useLatest` property in the DSC v3 `Microsoft.WinGet/Package` resource schema to emit a boolean default (`false`) instead of the incorrect string `"false"`. * `SignFile` in `WinGetSourceCreator` now supports an optional RFC 3161 timestamp server via the new `TimestampServer` property on the `Signature` model. When set, `signtool.exe` is called with `/tr /td sha256`, embedding a countersignature timestamp so that signed packages remain valid after the signing certificate expires. * File and directory paths passed to `signtool.exe` and `makeappx.exe` are now quoted, fixing failures when paths contain spaces. diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp index 979bf8119e..01956d3a82 100644 --- a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -7,6 +7,7 @@ #include "PackageCollection.h" #include "DependenciesFlow.h" #include "WorkflowBase.h" +#include #include #include #include @@ -177,8 +178,21 @@ namespace AppInstaller::CLI::Workflow auto packages = PackagesJson::CreateJson(context.Get()); std::filesystem::path outputFilePath{ context.Args.GetArg(Execution::Args::Type::OutputFile) }; - std::ofstream outputFileStream{ outputFilePath }; - outputFileStream << packages; + + // GetFileAttributesW returns INVALID_FILE_ATTRIBUTES for nonexistent files, so no separate exists() check is needed. + DWORD attrs = GetFileAttributesW(outputFilePath.c_str()); + bool isHidden = (attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_HIDDEN)); + + // Open the file directly without changing its attributes: + // - For an existing hidden file, use TRUNCATE_EXISTING to clear its content while preserving its attributes. + // - Otherwise, use CREATE_ALWAYS to create a new file or overwrite an existing one. + DWORD creationDisposition = isHidden ? TRUNCATE_EXISTING : CREATE_ALWAYS; + wil::unique_hfile fileHandle{ CreateFileW(outputFilePath.c_str(), GENERIC_WRITE, 0, nullptr, creationDisposition, FILE_ATTRIBUTE_NORMAL, nullptr) }; + THROW_LAST_ERROR_IF(!fileHandle); + + Json::StreamWriterBuilder writerBuilder; + std::string jsonContent = Json::writeString(writerBuilder, packages); + Filesystem::WriteStringToFile(fileHandle.get(), jsonContent); } void ReadImportFile(Execution::Context& context) diff --git a/src/AppInstallerCLITests/Filesystem.cpp b/src/AppInstallerCLITests/Filesystem.cpp index ae032b4ed5..f278abfd64 100644 --- a/src/AppInstallerCLITests/Filesystem.cpp +++ b/src/AppInstallerCLITests/Filesystem.cpp @@ -224,6 +224,56 @@ TEST_CASE("PathTree_VisitIf_Correct", "[filesystem][pathtree]") pathTree.VisitIf(L"C:", check_input, if_input); } +TEST_CASE("WriteStringToFile", "[filesystem]") +{ + SECTION("Basic content") + { + TestCommon::TempDirectory tempDirectory{ "WriteStringToFile" }; + auto tempFile = tempDirectory.CreateTempFile("output", ".txt"); + wil::unique_hfile fileHandle{ CreateFileW(tempFile.GetPath().c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr) }; + REQUIRE(fileHandle); + + std::string content = "Hello, WinGet!"; + REQUIRE_NOTHROW(WriteStringToFile(fileHandle.get(), content)); + fileHandle.reset(); + + std::ifstream readBack{ tempFile.GetPath() }; + std::string result{ std::istreambuf_iterator(readBack), std::istreambuf_iterator() }; + REQUIRE(result == content); + } + + SECTION("Empty content") + { + TestCommon::TempDirectory tempDirectory{ "WriteStringToFile" }; + auto tempFile = tempDirectory.CreateTempFile("empty", ".txt"); + wil::unique_hfile fileHandle{ CreateFileW(tempFile.GetPath().c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr) }; + REQUIRE(fileHandle); + + REQUIRE_NOTHROW(WriteStringToFile(fileHandle.get(), "")); + fileHandle.reset(); + + std::ifstream readBack{ tempFile.GetPath() }; + std::string result{ std::istreambuf_iterator(readBack), std::istreambuf_iterator() }; + REQUIRE(result.empty()); + } + + SECTION("Large content") + { + TestCommon::TempDirectory tempDirectory{ "WriteStringToFile" }; + auto tempFile = tempDirectory.CreateTempFile("large", ".txt"); + wil::unique_hfile fileHandle{ CreateFileW(tempFile.GetPath().c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr) }; + REQUIRE(fileHandle); + + std::string content(1 << 20, 'x'); // 1 MiB of 'x' + REQUIRE_NOTHROW(WriteStringToFile(fileHandle.get(), content)); + fileHandle.reset(); + + std::ifstream readBack{ tempFile.GetPath() }; + std::string result{ std::istreambuf_iterator(readBack), std::istreambuf_iterator() }; + REQUIRE(result == content); + } +} + TEST_CASE("GetFileInfoFor", "[filesystem]") { TestCommon::TempDirectory tempDirectory{ "GetFileInfoFor" }; diff --git a/src/AppInstallerSharedLib/Filesystem.cpp b/src/AppInstallerSharedLib/Filesystem.cpp index f29fb21b75..8d20374c80 100644 --- a/src/AppInstallerSharedLib/Filesystem.cpp +++ b/src/AppInstallerSharedLib/Filesystem.cpp @@ -565,4 +565,20 @@ namespace AppInstaller::Filesystem files.resize(i); } + + void WriteStringToFile(HANDLE fileHandle, std::string_view content) + { + size_t totalBytesWritten = 0; + while (totalBytesWritten < content.size()) + { + DWORD bytesWritten = 0; + THROW_LAST_ERROR_IF(!WriteFile( + fileHandle, + content.data() + totalBytesWritten, + static_cast(content.size() - totalBytesWritten), + &bytesWritten, + nullptr)); + totalBytesWritten += bytesWritten; + } + } } diff --git a/src/AppInstallerSharedLib/Public/winget/Filesystem.h b/src/AppInstallerSharedLib/Public/winget/Filesystem.h index ea1f5ce815..50f2a9b6b6 100644 --- a/src/AppInstallerSharedLib/Public/winget/Filesystem.h +++ b/src/AppInstallerSharedLib/Public/winget/Filesystem.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -148,4 +149,7 @@ namespace AppInstaller::Filesystem // Modifies the given files to only include those that exceed the limits that are provided. void FilterToFilesExceedingLimits(std::vector& files, const FileLimits& limits); + + // Writes the given string to the file handle, handling partial writes. + void WriteStringToFile(HANDLE fileHandle, std::string_view content); }