Skip to content

Commit d40a4ac

Browse files
authored
Allow exporting to hidden files (#5795)
1 parent 5c46ba1 commit d40a4ac

5 files changed

Lines changed: 87 additions & 2 deletions

File tree

doc/ReleaseNotes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ The PowerShell module now automatically uses `GH_TOKEN` or `GITHUB_TOKEN` enviro
4949

5050
## Bug Fixes
5151

52+
* `winget export` now works when the destination path is a hidden file
5253
* 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"`.
5354
* `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 <url> /td sha256`, embedding a countersignature timestamp so that signed packages remain valid after the signing certificate expires.
5455
* File and directory paths passed to `signtool.exe` and `makeappx.exe` are now quoted, fixing failures when paths contain spaces.

src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "PackageCollection.h"
88
#include "DependenciesFlow.h"
99
#include "WorkflowBase.h"
10+
#include <winget/Filesystem.h>
1011
#include <winget/RepositorySearch.h>
1112
#include <winget/Runtime.h>
1213
#include <winget/PackageVersionSelection.h>
@@ -177,8 +178,21 @@ namespace AppInstaller::CLI::Workflow
177178
auto packages = PackagesJson::CreateJson(context.Get<Execution::Data::PackageCollection>());
178179

179180
std::filesystem::path outputFilePath{ context.Args.GetArg(Execution::Args::Type::OutputFile) };
180-
std::ofstream outputFileStream{ outputFilePath };
181-
outputFileStream << packages;
181+
182+
// GetFileAttributesW returns INVALID_FILE_ATTRIBUTES for nonexistent files, so no separate exists() check is needed.
183+
DWORD attrs = GetFileAttributesW(outputFilePath.c_str());
184+
bool isHidden = (attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_HIDDEN));
185+
186+
// Open the file directly without changing its attributes:
187+
// - For an existing hidden file, use TRUNCATE_EXISTING to clear its content while preserving its attributes.
188+
// - Otherwise, use CREATE_ALWAYS to create a new file or overwrite an existing one.
189+
DWORD creationDisposition = isHidden ? TRUNCATE_EXISTING : CREATE_ALWAYS;
190+
wil::unique_hfile fileHandle{ CreateFileW(outputFilePath.c_str(), GENERIC_WRITE, 0, nullptr, creationDisposition, FILE_ATTRIBUTE_NORMAL, nullptr) };
191+
THROW_LAST_ERROR_IF(!fileHandle);
192+
193+
Json::StreamWriterBuilder writerBuilder;
194+
std::string jsonContent = Json::writeString(writerBuilder, packages);
195+
Filesystem::WriteStringToFile(fileHandle.get(), jsonContent);
182196
}
183197

184198
void ReadImportFile(Execution::Context& context)

src/AppInstallerCLITests/Filesystem.cpp

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,56 @@ TEST_CASE("PathTree_VisitIf_Correct", "[filesystem][pathtree]")
224224
pathTree.VisitIf(L"C:", check_input, if_input);
225225
}
226226

227+
TEST_CASE("WriteStringToFile", "[filesystem]")
228+
{
229+
SECTION("Basic content")
230+
{
231+
TestCommon::TempDirectory tempDirectory{ "WriteStringToFile" };
232+
auto tempFile = tempDirectory.CreateTempFile("output", ".txt");
233+
wil::unique_hfile fileHandle{ CreateFileW(tempFile.GetPath().c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr) };
234+
REQUIRE(fileHandle);
235+
236+
std::string content = "Hello, WinGet!";
237+
REQUIRE_NOTHROW(WriteStringToFile(fileHandle.get(), content));
238+
fileHandle.reset();
239+
240+
std::ifstream readBack{ tempFile.GetPath() };
241+
std::string result{ std::istreambuf_iterator<char>(readBack), std::istreambuf_iterator<char>() };
242+
REQUIRE(result == content);
243+
}
244+
245+
SECTION("Empty content")
246+
{
247+
TestCommon::TempDirectory tempDirectory{ "WriteStringToFile" };
248+
auto tempFile = tempDirectory.CreateTempFile("empty", ".txt");
249+
wil::unique_hfile fileHandle{ CreateFileW(tempFile.GetPath().c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr) };
250+
REQUIRE(fileHandle);
251+
252+
REQUIRE_NOTHROW(WriteStringToFile(fileHandle.get(), ""));
253+
fileHandle.reset();
254+
255+
std::ifstream readBack{ tempFile.GetPath() };
256+
std::string result{ std::istreambuf_iterator<char>(readBack), std::istreambuf_iterator<char>() };
257+
REQUIRE(result.empty());
258+
}
259+
260+
SECTION("Large content")
261+
{
262+
TestCommon::TempDirectory tempDirectory{ "WriteStringToFile" };
263+
auto tempFile = tempDirectory.CreateTempFile("large", ".txt");
264+
wil::unique_hfile fileHandle{ CreateFileW(tempFile.GetPath().c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr) };
265+
REQUIRE(fileHandle);
266+
267+
std::string content(1 << 20, 'x'); // 1 MiB of 'x'
268+
REQUIRE_NOTHROW(WriteStringToFile(fileHandle.get(), content));
269+
fileHandle.reset();
270+
271+
std::ifstream readBack{ tempFile.GetPath() };
272+
std::string result{ std::istreambuf_iterator<char>(readBack), std::istreambuf_iterator<char>() };
273+
REQUIRE(result == content);
274+
}
275+
}
276+
227277
TEST_CASE("GetFileInfoFor", "[filesystem]")
228278
{
229279
TestCommon::TempDirectory tempDirectory{ "GetFileInfoFor" };

src/AppInstallerSharedLib/Filesystem.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,4 +565,20 @@ namespace AppInstaller::Filesystem
565565

566566
files.resize(i);
567567
}
568+
569+
void WriteStringToFile(HANDLE fileHandle, std::string_view content)
570+
{
571+
size_t totalBytesWritten = 0;
572+
while (totalBytesWritten < content.size())
573+
{
574+
DWORD bytesWritten = 0;
575+
THROW_LAST_ERROR_IF(!WriteFile(
576+
fileHandle,
577+
content.data() + totalBytesWritten,
578+
static_cast<DWORD>(content.size() - totalBytesWritten),
579+
&bytesWritten,
580+
nullptr));
581+
totalBytesWritten += bytesWritten;
582+
}
583+
}
568584
}

src/AppInstallerSharedLib/Public/winget/Filesystem.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include <filesystem>
55
#include <map>
66
#include <optional>
7+
#include <string_view>
78
#include <vector>
89
#include <shtypes.h>
910

@@ -148,4 +149,7 @@ namespace AppInstaller::Filesystem
148149

149150
// Modifies the given files to only include those that exceed the limits that are provided.
150151
void FilterToFilesExceedingLimits(std::vector<FileInfo>& files, const FileLimits& limits);
152+
153+
// Writes the given string to the file handle, handling partial writes.
154+
void WriteStringToFile(HANDLE fileHandle, std::string_view content);
151155
}

0 commit comments

Comments
 (0)