ziglang / zig

General-purpose programming language and toolchain for maintaining robust, optimal, and reusable software.
https://ziglang.org
MIT License
33.97k stars 2.48k forks source link

A Zig `SHGetKnownFolderPath` implementation in the standard library #18098

Open squeek502 opened 10 months ago

squeek502 commented 10 months ago

EDIT: This is currently being worked on here: https://github.com/squeek502/get-known-folder-path


Follow up from https://github.com/ziglang/zig/pull/18091

To avoid a dependency on shell32.dll, the SHGetKnownFolderPath call within std.fs.getAppDataDir was replaced with a LOCALAPPDATA environment variable lookup. This has the potential to regress getAppDataDir behavior for certain setups and it's not foolproof to rely on LOCALAPPDATA being set.

Instead, SHGetKnownFolderPath should be reimplemented in Zig without introducing a dependency on shell32. For this, wine's implementation would likely be a good reference.

Standalone version of the previous implementation using SHGetKnownFolderPath (this is ultimately the behavior that the Zig reimplementation is looking to match, but it shouldn't be FOLDERID_LocalAppData-specific):

const std = @import("std");
const os = std.os;

pub extern "shell32" fn SHGetKnownFolderPath(
    rfid: *const os.windows.KNOWNFOLDERID,
    dwFlags: os.windows.DWORD,
    hToken: ?os.windows.HANDLE,
    ppszPath: *[*:0]os.windows.WCHAR,
) callconv(os.windows.WINAPI) os.windows.HRESULT;

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer std.debug.assert(gpa.deinit() == .ok);
    const allocator = gpa.allocator();

    var dir_path_ptr: [*:0]u16 = undefined;
    switch (SHGetKnownFolderPath(
        &os.windows.FOLDERID_LocalAppData,
        os.windows.KF_FLAG_CREATE,
        null,
        &dir_path_ptr,
    )) {
        os.windows.S_OK => {
            const global_dir = std.unicode.utf16leToUtf8Alloc(allocator, std.mem.sliceTo(dir_path_ptr, 0)) catch |err| switch (err) {
                error.UnexpectedSecondSurrogateHalf => return error.AppDataDirUnavailable,
                error.ExpectedSecondSurrogateHalf => return error.AppDataDirUnavailable,
                error.DanglingSurrogateHalf => return error.AppDataDirUnavailable,
                error.OutOfMemory => return error.OutOfMemory,
            };
            defer allocator.free(global_dir);
            std.debug.print("{s}\n", .{global_dir});
        },
        os.windows.E_OUTOFMEMORY => return error.OutOfMemory,
        else => return error.AppDataDirUnavailable,
    }
}
matu3ba commented 10 months ago

I'll work on this to get a stab at some more windows tooling.

squeek502 commented 10 months ago

@matu3ba I've got an implementation mostly complete for retrieving a known path that I'll likely create a PR for soon.

Right now I'm thinking that the more complicated parts should not be supported (KF_FLAG_CREATE and KF_FLAG_INIT in particular, which would potentially need to do things like create "Library" folders, init desktop.ini files, respect various undocumented flags in FolderDescriptions registry entries, etc).

I'll go into more detail in the PR.

squeek502 commented 9 months ago

I've decided against making a PR for now to allow for (hopefully) developing a more conformant implementation. SHGetKnownFolderPath has a lot of moving parts, and Zig only really cares about getting the path to the Local App Data directory, so %LOCALAPPDATA% seems like a decent solution since it's set by the OS. That is, this issue doesn't seem urgent enough to warrant merging in a potentially half-baked SHGetKnownFolderPath reimplementation just to stop using %LOCALAPPDATA%.

I've put my mostly-working-but-probably-has-incorrect-corner-cases implementation here with a lot of explanation in the readme:

https://github.com/squeek502/get-known-folder-path

If anyone wants to help out or has ideas about how to go about this sort of thing, it'd be appreciated. It feels like it's nearly there, just a few pieces that need to be figured out.