From 5749550de3c286b436ee4b7282bca3b09785850b Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Fri, 10 Oct 2025 13:05:51 -0500 Subject: [PATCH 1/6] Allow exporting to hidden files --- .../Workflows/ImportExportFlow.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp index f6d8bf8ec1..3a6edc0e02 100644 --- a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -157,8 +157,25 @@ namespace AppInstaller::CLI::Workflow auto packages = PackagesJson::CreateJson(context.Get()); std::filesystem::path outputFilePath{ context.Args.GetArg(Execution::Args::Type::OutputFile) }; + + // Check if the file exists and is hidden + DWORD attrs = std::filesystem::exists(outputFilePath) ? GetFileAttributesW(outputFilePath.c_str()) : INVALID_FILE_ATTRIBUTES; + bool isHidden = (attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_HIDDEN)); + + if (isHidden) + { + // Remove hidden attribute so we can write to it + SetFileAttributesW(outputFilePath.c_str(), attrs & ~FILE_ATTRIBUTE_HIDDEN); + } + std::ofstream outputFileStream{ outputFilePath }; outputFileStream << packages; + + if (isHidden) + { + // Restore hidden attribute + SetFileAttributesW(outputFilePath.c_str(), attrs); + } } void ReadImportFile(Execution::Context& context) From af5e1f549e85e3df3389c1cf5825db449fa7557c Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Tue, 14 Oct 2025 20:32:35 -0500 Subject: [PATCH 2/6] Update release notes --- doc/ReleaseNotes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/ReleaseNotes.md b/doc/ReleaseNotes.md index cf22c48572..989d7ea875 100644 --- a/doc/ReleaseNotes.md +++ b/doc/ReleaseNotes.md @@ -39,3 +39,6 @@ The PowerShell module now automatically uses `GH_TOKEN` or `GITHUB_TOKEN` enviro ## Bug Fixes + +## Bug Fixes +* `winget export` now works when the destination path is a hidden file From 1a3704f4d851a5499abd3a6899c365df0a1410ba Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Thu, 26 Mar 2026 22:50:21 -0500 Subject: [PATCH 3/6] Write to file without changing attributes --- doc/ReleaseNotes.md | 3 -- .../Workflows/ImportExportFlow.cpp | 29 +++++++++---------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/doc/ReleaseNotes.md b/doc/ReleaseNotes.md index 989d7ea875..33792b1cf4 100644 --- a/doc/ReleaseNotes.md +++ b/doc/ReleaseNotes.md @@ -38,7 +38,4 @@ The PowerShell module now automatically uses `GH_TOKEN` or `GITHUB_TOKEN` enviro ## Bug Fixes - - -## Bug Fixes * `winget export` now works when the destination path is a hidden file diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp index 3a6edc0e02..c426dc218a 100644 --- a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -158,24 +158,21 @@ namespace AppInstaller::CLI::Workflow std::filesystem::path outputFilePath{ context.Args.GetArg(Execution::Args::Type::OutputFile) }; - // Check if the file exists and is hidden - DWORD attrs = std::filesystem::exists(outputFilePath) ? GetFileAttributesW(outputFilePath.c_str()) : INVALID_FILE_ATTRIBUTES; + // GetFileAttributesW returns INVALID_FILE_ATTRIBUTES for non-existent files, so no separate exists() check is needed. + DWORD attrs = GetFileAttributesW(outputFilePath.c_str()); bool isHidden = (attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_HIDDEN)); - if (isHidden) - { - // Remove hidden attribute so we can write to it - SetFileAttributesW(outputFilePath.c_str(), attrs & ~FILE_ATTRIBUTE_HIDDEN); - } - - std::ofstream outputFileStream{ outputFilePath }; - outputFileStream << packages; - - if (isHidden) - { - // Restore hidden attribute - SetFileAttributesW(outputFilePath.c_str(), attrs); - } + // 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); + DWORD bytesWritten = 0; + THROW_LAST_ERROR_IF(!WriteFile(fileHandle.get(), jsonContent.c_str(), static_cast(jsonContent.size()), &bytesWritten, nullptr)); } void ReadImportFile(Execution::Context& context) From c71b30207580d2591224fb4ba08ce2b6f64d4c62 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Thu, 26 Mar 2026 22:54:30 -0500 Subject: [PATCH 4/6] Spelling --- src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp index c426dc218a..14a23d21e5 100644 --- a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -158,7 +158,7 @@ namespace AppInstaller::CLI::Workflow std::filesystem::path outputFilePath{ context.Args.GetArg(Execution::Args::Type::OutputFile) }; - // GetFileAttributesW returns INVALID_FILE_ATTRIBUTES for non-existent files, so no separate exists() check is needed. + // 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)); From 41491c3f223c030bfc9de8d5c98428d57b5fb08b Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Tue, 7 Apr 2026 08:07:36 -0500 Subject: [PATCH 5/6] Implement write helper --- .../Workflows/ImportExportFlow.cpp | 4 ++-- src/AppInstallerSharedLib/Filesystem.cpp | 16 ++++++++++++++++ .../Public/winget/Filesystem.h | 4 ++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp index 14a23d21e5..b9f328f566 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 @@ -171,8 +172,7 @@ namespace AppInstaller::CLI::Workflow Json::StreamWriterBuilder writerBuilder; std::string jsonContent = Json::writeString(writerBuilder, packages); - DWORD bytesWritten = 0; - THROW_LAST_ERROR_IF(!WriteFile(fileHandle.get(), jsonContent.c_str(), static_cast(jsonContent.size()), &bytesWritten, nullptr)); + Filesystem::WriteStringToFile(fileHandle.get(), jsonContent); } void ReadImportFile(Execution::Context& context) 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); } From 42f5b8a372375a7183b0f073b2d2b0b40ae75973 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Tue, 7 Apr 2026 08:15:58 -0500 Subject: [PATCH 6/6] Add test cases --- src/AppInstallerCLITests/Filesystem.cpp | 50 +++++++++++++++++++++++++ 1 file changed, 50 insertions(+) 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" };