dotnet 測試在 Linux 系統上的 Environment.GetFolderPath 行為

lindexi發表於2024-09-12

由於 Environment.GetFolderPath 可以傳入的引數裡面,有許多都是 Windows 系統特有的,在 Linux 上不存在的,也沒有對映對應的資料夾。本文將在 WSL Debian 和 UOS 系統上測試 Environment.GetFolderPath 行為

測試使用 Environment.SpecialFolder 的各個列舉獲取路徑的程式碼如下

            foreach (var name in Enum.GetNames<Environment.SpecialFolder>())
            {
                Console.WriteLine($"{name} = {Environment.GetFolderPath(Enum.Parse<Environment.SpecialFolder>(name))}");
            }

在 WSL Debian 的執行結果如下

Desktop =
Programs =
MyDocuments =
Personal =
Favorites =
Startup =
Recent =
SendTo =
StartMenu =
MyMusic =
MyVideos =
DesktopDirectory =
MyComputer =
NetworkShortcuts =
Fonts =
Templates =
CommonStartMenu =
CommonPrograms =
CommonStartup =
CommonDesktopDirectory =
ApplicationData = /home/user/.config
PrinterShortcuts =
LocalApplicationData = /home/user/.local/share
InternetCache =
Cookies =
History =
CommonApplicationData = /usr/share
Windows =
System =
ProgramFiles =
MyPictures =
UserProfile = /home/user
SystemX86 =
ProgramFilesX86 =
CommonProgramFiles =
CommonProgramFilesX86 =
CommonTemplates =
CommonDocuments =
CommonAdminTools =
AdminTools =
CommonMusic =
CommonPictures =
CommonVideos =
Resources =
LocalizedResources =
CommonOemLinks =
CDBurning =

在 UOS 系統的執行結果如下

Desktop = /home/lin/Desktop
Programs = 
MyDocuments = /home/lin/Documents
Personal = /home/lin/Documents
Favorites = 
Startup = 
Recent = 
SendTo = 
StartMenu = 
MyMusic = /home/lin/Music
MyVideos = /home/lin/Videos
DesktopDirectory = /home/lin/Desktop
MyComputer = 
NetworkShortcuts = 
Fonts = 
Templates = /home/lin/.Templates
CommonStartMenu = 
CommonPrograms = 
CommonStartup = 
CommonDesktopDirectory = 
ApplicationData = /home/lin/.config
PrinterShortcuts = 
LocalApplicationData = /home/lin/.local/share
InternetCache = 
Cookies = 
History = 
CommonApplicationData = /usr/share
Windows = 
System = 
ProgramFiles = 
MyPictures = /home/lin/Pictures
UserProfile = /home/lin
SystemX86 = 
ProgramFilesX86 = 
CommonProgramFiles = 
CommonProgramFilesX86 = 
CommonTemplates = 
CommonDocuments = 
CommonAdminTools = 
AdminTools = 
CommonMusic = 
CommonPictures = 
CommonVideos = 
Resources = 
LocalizedResources = 
CommonOemLinks = 
CDBurning = 

可以看到 UOS 上有更多的屬性是存在值的,存在一些行為差異

另外,根據 UOS 官方文件 的如下說明:

軟體包不允許直接向\(HOME目錄直接寫入檔案,後期系統將會使用沙箱技術重新定向\)HOME,任何依賴該特性的行為都可能失效。
應用使用如下環境變數指示的目錄寫入應用資料和配置:

    $XDG_DATA_HOME
    $XDG_CONFIG_HOME
    $XDG_CACHE_HOME

對於appid為org.deepin.browser的應用,其寫入目錄為:

    $XDG_DATA_HOME/org.deepin.browser
    $XDG_CONFIG_HOME/org.deepin.browser
    $XDG_CACHE_HOME/org.deepin.browser

我同時也測試了以上的 XDG_DATA_HOMEXDG_CONFIG_HOMEXDG_CACHE_HOME 環境變數的內容,在我的裝置上的輸出如下

XDG_DATA_HOME = /home/lin/.local/share
XDG_CONFIG_HOME = /home/lin/.config
XDG_CACHE_HOME = /home/lin/.cache

可以看到 XDG_DATA_HOMELocalApplicationData 是對應的值。而 XDG_CONFIG_HOMEApplicationData 是對應的值

本文以上程式碼放在githubgitee 歡迎訪問

可以透過如下方式獲取本文的原始碼,先建立一個空資料夾,接著使用命令列 cd 命令進入此空資料夾,在命令列裡面輸入以下程式碼,即可獲取到本文的程式碼

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 61a7e77b8b86e17ccf2b5d1a9d0460d09cc95036

以上使用的是 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源。請在命令列繼續輸入以下程式碼

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 61a7e77b8b86e17ccf2b5d1a9d0460d09cc95036

獲取程式碼之後,進入 NurbeakairweWaharbaner 資料夾

這裡的 XDG 是 X Desktop Group 的縮寫,更多 XDG 知識請參閱:

  • XDG基本目錄規範 DeepinWiki
  • Linux 基本目錄規範 ——XDG Winddoing's Notes

在 dotnet 的 runtime 底層的 Environment.GetFolderPath 實現如下

    public static partial class Environment
    {
        private static string GetFolderPathCore(SpecialFolder folder, SpecialFolderOption option)
        {
            // Get the path for the SpecialFolder
            string path = GetFolderPathCoreWithoutValidation(folder) ?? string.Empty;
            Debug.Assert(path != null);

            // If we didn't get one, or if we got one but we're not supposed to verify it,
            // or if we're supposed to verify it and it passes verification, return the path.
            if (path.Length == 0 ||
                option == SpecialFolderOption.DoNotVerify ||
                Interop.Sys.Access(path, Interop.Sys.AccessMode.R_OK) == 0)
            {
                return path;
            }

            // Failed verification.  If None, then we're supposed to return an empty string.
            // If Create, we're supposed to create it and then return the path.
            if (option == SpecialFolderOption.None)
            {
                return string.Empty;
            }

            Debug.Assert(option == SpecialFolderOption.Create);

            Directory.CreateDirectory(path);

            return path;
        }

        private static string? GetFolderPathCoreWithoutValidation(SpecialFolder folder)
        {
            // First handle any paths that involve only static paths, avoiding the overheads of getting user-local paths.
            // https://www.freedesktop.org/software/systemd/man/file-hierarchy.html
            switch (folder)
            {
                case SpecialFolder.CommonApplicationData: return "/usr/share";
                case SpecialFolder.CommonTemplates: return "/usr/share/templates";
#if TARGET_OSX
                case SpecialFolder.ProgramFiles: return "/Applications";
                case SpecialFolder.System: return "/System";
#endif
            }

            // All other paths are based on the XDG Base Directory Specification:
            // https://specifications.freedesktop.org/basedir-spec/latest/
            string? home = null;
            try
            {
                home = PersistedFiles.GetHomeDirectory();
            }
            catch (Exception exc)
            {
                Debug.Fail($"Unable to get home directory: {exc}");
            }

            // Fall back to '/' when we can't determine the home directory.
            // This location isn't writable by non-root users which provides some safeguard
            // that the application doesn't write data which is meant to be private.
            if (string.IsNullOrEmpty(home))
            {
                home = "/";
            }

            // TODO: Consider caching (or precomputing and caching) all subsequent results.
            // This would significantly improve performance for repeated access, at the expense
            // of not being responsive to changes in the underlying environment variables,
            // configuration files, etc.

            switch (folder)
            {
                case SpecialFolder.UserProfile:
                    return home;

                case SpecialFolder.Templates:
                    return ReadXdgDirectory(home, "XDG_TEMPLATES_DIR", "Templates");
                // TODO: Consider merging the OSX path with the rest of the Apple systems here:
                // https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Environment.iOS.cs
#if TARGET_OSX
                case SpecialFolder.Desktop:
                case SpecialFolder.DesktopDirectory:
                    return Interop.Sys.SearchPath(NSSearchPathDirectory.NSDesktopDirectory);
                case SpecialFolder.ApplicationData:
                case SpecialFolder.LocalApplicationData:
                    return Interop.Sys.SearchPath(NSSearchPathDirectory.NSApplicationSupportDirectory);
                case SpecialFolder.MyDocuments: // same value as Personal
                    return Interop.Sys.SearchPath(NSSearchPathDirectory.NSDocumentDirectory);
                case SpecialFolder.MyMusic:
                    return Interop.Sys.SearchPath(NSSearchPathDirectory.NSMusicDirectory);
                case SpecialFolder.MyVideos:
                    return Interop.Sys.SearchPath(NSSearchPathDirectory.NSMoviesDirectory);
                case SpecialFolder.MyPictures:
                    return Interop.Sys.SearchPath(NSSearchPathDirectory.NSPicturesDirectory);
                case SpecialFolder.Fonts:
                    return Path.Combine(home, "Library", "Fonts");
                case SpecialFolder.Favorites:
                    return Path.Combine(home, "Library", "Favorites");
                case SpecialFolder.InternetCache:
                    return Interop.Sys.SearchPath(NSSearchPathDirectory.NSCachesDirectory);
#else
                case SpecialFolder.Desktop:
                case SpecialFolder.DesktopDirectory:
                    return ReadXdgDirectory(home, "XDG_DESKTOP_DIR", "Desktop");
                case SpecialFolder.ApplicationData:
                    return GetXdgConfig(home);
                case SpecialFolder.LocalApplicationData:
                    // "$XDG_DATA_HOME defines the base directory relative to which user specific data files should be stored."
                    // "If $XDG_DATA_HOME is either not set or empty, a default equal to $HOME/.local/share should be used."
                    string? data = GetEnvironmentVariable("XDG_DATA_HOME");
                    if (data is null || !data.StartsWith('/'))
                    {
                        data = Path.Combine(home, ".local", "share");
                    }
                    return data;
                case SpecialFolder.MyDocuments: // same value as Personal
                    return ReadXdgDirectory(home, "XDG_DOCUMENTS_DIR", "Documents");
                case SpecialFolder.MyMusic:
                    return ReadXdgDirectory(home, "XDG_MUSIC_DIR", "Music");
                case SpecialFolder.MyVideos:
                    return ReadXdgDirectory(home, "XDG_VIDEOS_DIR", "Videos");
                case SpecialFolder.MyPictures:
                    return ReadXdgDirectory(home, "XDG_PICTURES_DIR", "Pictures");
                case SpecialFolder.Fonts:
                    return Path.Combine(home, ".fonts");
#endif
            }

            // No known path for the SpecialFolder
            return string.Empty;
        }

        private static string GetXdgConfig(string home)
        {
            // "$XDG_CONFIG_HOME defines the base directory relative to which user specific configuration files should be stored."
            // "If $XDG_CONFIG_HOME is either not set or empty, a default equal to $HOME/.config should be used."
            string? config = GetEnvironmentVariable("XDG_CONFIG_HOME");
            if (config is null || !config.StartsWith('/'))
            {
                config = Path.Combine(home, ".config");
            }
            return config;
        }

        private static string ReadXdgDirectory(string homeDir, string key, string fallback)
        {
            Debug.Assert(!string.IsNullOrEmpty(homeDir), $"Expected non-empty homeDir");
            Debug.Assert(!string.IsNullOrEmpty(key), $"Expected non-empty key");
            Debug.Assert(!string.IsNullOrEmpty(fallback), $"Expected non-empty fallback");

            string? envPath = GetEnvironmentVariable(key);
            if (envPath is not null && envPath.StartsWith('/'))
            {
                return envPath;
            }

            // Use the user-dirs.dirs file to look up the right config.
            // Note that the docs also highlight a list of directories in which to look for this file:
            // "$XDG_CONFIG_DIRS defines the preference-ordered set of base directories to search for configuration files in addition
            //  to the $XDG_CONFIG_HOME base directory. The directories in $XDG_CONFIG_DIRS should be separated with a colon ':'. If
            //  $XDG_CONFIG_DIRS is either not set or empty, a value equal to / etc / xdg should be used."
            // For simplicity, we don't currently do that.  We can add it if/when necessary.

            string userDirsPath = Path.Combine(GetXdgConfig(homeDir), "user-dirs.dirs");
            if (Interop.Sys.Access(userDirsPath, Interop.Sys.AccessMode.R_OK) == 0)
            {
                try
                {
                    using (var reader = new StreamReader(userDirsPath))
                    {
                        string? line;
                        while ((line = reader.ReadLine()) != null)
                        {
                            // Example lines:
                            // XDG_DESKTOP_DIR="$HOME/Desktop"
                            // XDG_PICTURES_DIR = "/absolute/path"

                            // Skip past whitespace at beginning of line
                            int pos = 0;
                            SkipWhitespace(line, ref pos);
                            if (pos >= line.Length) continue;

                            // Skip past requested key name
                            if (string.CompareOrdinal(line, pos, key, 0, key.Length) != 0) continue;
                            pos += key.Length;

                            // Skip past whitespace and past '='
                            SkipWhitespace(line, ref pos);
                            if (pos >= line.Length - 4 || line[pos] != '=') continue; // 4 for ="" and at least one char between quotes
                            pos++; // skip past '='

                            // Skip past whitespace and past first quote
                            SkipWhitespace(line, ref pos);
                            if (pos >= line.Length - 3 || line[pos] != '"') continue; // 3 for "" and at least one char between quotes
                            pos++; // skip past opening '"'

                            // Skip past relative prefix if one exists
                            bool relativeToHome = false;
                            const string RelativeToHomePrefix = "$HOME/";
                            if (string.CompareOrdinal(line, pos, RelativeToHomePrefix, 0, RelativeToHomePrefix.Length) == 0)
                            {
                                relativeToHome = true;
                                pos += RelativeToHomePrefix.Length;
                            }
                            else if (line[pos] != '/') // if not relative to home, must be absolute path
                            {
                                continue;
                            }

                            // Find end of path
                            int endPos = line.IndexOf('"', pos);
                            if (endPos <= pos) continue;

                            // Got we need.  Now extract it.
                            string path = line.Substring(pos, endPos - pos);
                            return relativeToHome ?
                                Path.Combine(homeDir, path) :
                                path;
                        }
                    }
                }
                catch (Exception exc)
                {
                    // assembly not found, file not found, errors reading file, etc. Just eat everything.
                    Debug.Fail($"Failed reading {userDirsPath}: {exc}");
                }
            }

            return Path.Combine(homeDir, fallback);
        }

        private static void SkipWhitespace(string line, ref int pos)
        {
            while (pos < line.Length && char.IsWhiteSpace(line[pos])) pos++;
        }
    }

注: 在 dotnet 6.0.26 和 dotnet 7 版本,獲取的 MyDocuments 的值將會和 UserProfile 相同,都是指向 $HOME 環境變數的路徑,如以下程式碼

                case SpecialFolder.UserProfile:
                case SpecialFolder.MyDocuments: // same value as Personal
                     return home;

更詳細的程式碼如下

        private static string GetFolderPathCoreWithoutValidation(SpecialFolder folder)
        {
            // First handle any paths that involve only static paths, avoiding the overheads of getting user-local paths.
            // https://www.freedesktop.org/software/systemd/man/file-hierarchy.html
            switch (folder)
            {
                case SpecialFolder.CommonApplicationData: return "/usr/share";
                case SpecialFolder.CommonTemplates: return "/usr/share/templates";
#if TARGET_OSX
                case SpecialFolder.ProgramFiles: return "/Applications";
                case SpecialFolder.System: return "/System";
#endif
            }

            // All other paths are based on the XDG Base Directory Specification:
            // https://specifications.freedesktop.org/basedir-spec/latest/
            string? home = null;
            try
            {
                home = PersistedFiles.GetHomeDirectory();
            }
            catch (Exception exc)
            {
                Debug.Fail($"Unable to get home directory: {exc}");
            }

            // Fall back to '/' when we can't determine the home directory.
            // This location isn't writable by non-root users which provides some safeguard
            // that the application doesn't write data which is meant to be private.
            if (string.IsNullOrEmpty(home))
            {
                home = "/";
            }

            // TODO: Consider caching (or precomputing and caching) all subsequent results.
            // This would significantly improve performance for repeated access, at the expense
            // of not being responsive to changes in the underlying environment variables,
            // configuration files, etc.

            switch (folder)
            {
                case SpecialFolder.UserProfile:
                case SpecialFolder.MyDocuments: // same value as Personal
                    return home;
                case SpecialFolder.ApplicationData:
                    return GetXdgConfig(home);
                case SpecialFolder.LocalApplicationData:
                    // "$XDG_DATA_HOME defines the base directory relative to which user specific data files should be stored."
                    // "If $XDG_DATA_HOME is either not set or empty, a default equal to $HOME/.local/share should be used."
                    string? data = GetEnvironmentVariable("XDG_DATA_HOME");
                    if (string.IsNullOrEmpty(data) || data[0] != '/')
                    {
                        data = Path.Combine(home, ".local", "share");
                    }
                    return data;

                case SpecialFolder.Desktop:
                case SpecialFolder.DesktopDirectory:
                    return ReadXdgDirectory(home, "XDG_DESKTOP_DIR", "Desktop");
                case SpecialFolder.Templates:
                    return ReadXdgDirectory(home, "XDG_TEMPLATES_DIR", "Templates");
                case SpecialFolder.MyVideos:
                    return ReadXdgDirectory(home, "XDG_VIDEOS_DIR", "Videos");

#if TARGET_OSX
                case SpecialFolder.MyMusic:
                    return Path.Combine(home, "Music");
                case SpecialFolder.MyPictures:
                    return Path.Combine(home, "Pictures");
                case SpecialFolder.Fonts:
                    return Path.Combine(home, "Library", "Fonts");
                case SpecialFolder.Favorites:
                    return Path.Combine(home, "Library", "Favorites");
                case SpecialFolder.InternetCache:
                    return Path.Combine(home, "Library", "Caches");
#else
                case SpecialFolder.MyMusic:
                    return ReadXdgDirectory(home, "XDG_MUSIC_DIR", "Music");
                case SpecialFolder.MyPictures:
                    return ReadXdgDirectory(home, "XDG_PICTURES_DIR", "Pictures");
                case SpecialFolder.Fonts:
                    return Path.Combine(home, ".fonts");
#endif
            }

            // No known path for the SpecialFolder
            return string.Empty;
        }

以上程式碼地址: https://github.com/dotnet/runtime/blob/v6.0.26/src/libraries/System.Private.CoreLib/src/System/Environment.GetFolderPathCore.Unix.cs

在 dotnet 8 的 Fix some incorrect SpecialFolder entries for Unix by Miepee · Pull Request #68610 · dotnet/runtime 的更改裡面,最佳化了各路徑的讀取方法,從而更改了 MyDocuments 的返回值路徑

詳細文件請看 .NET 8 中斷性變更:Unix 上的 GetFolderPath 行為 - .NET Microsoft Learn

以上不僅變更了在 Linux 上的行為也變更了在安卓 macOS 等的行為

相關文章