From 372bf426f1e10c2ccab3446cc0043edc86bcad01 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Tue, 31 Mar 2020 17:23:20 +0300 Subject: [PATCH] [mono] Implement Environment.GetFolderPath on iOS (#34022) * Implement Environment.GetFolderPath on iOS * Address feedback * Move GetFolderPathCore to Environment.Unix.GetFolderPathCore.cs * Fix build issue * Address feedback * cache all special directories * Fix build issue * remove a whitespace * Fix UserProfile issue * undo changes in GetEnvironmentVariableCore * Update Environment.Unix.Mono.cs * Extract to InternalGetEnvironmentVariable * Fix build issue * Return emtpy string if underlying native function returns null * Add nullability --- .../OSX/System.Native/Interop.SearchPath.cs | 29 ++ .../Native/Unix/System.Native/CMakeLists.txt | 4 +- .../Unix/System.Native/pal_searchpath.h | 10 + .../Unix/System.Native/pal_searchpath.m | 14 + .../System.Private.CoreLib.Shared.projitems | 1 + .../Environment.Unix.GetFolderPathCore.cs | 250 ++++++++++++++++++ .../src/System/Environment.Unix.cs | 232 ---------------- .../System.Private.CoreLib.csproj | 6 + .../src/System/Environment.Unix.Mono.cs | 18 +- .../src/System/Environment.iOS.cs | 99 +++++++ 10 files changed, 422 insertions(+), 241 deletions(-) create mode 100644 src/libraries/Common/src/Interop/OSX/System.Native/Interop.SearchPath.cs create mode 100644 src/libraries/Native/Unix/System.Native/pal_searchpath.h create mode 100644 src/libraries/Native/Unix/System.Native/pal_searchpath.m create mode 100644 src/libraries/System.Private.CoreLib/src/System/Environment.Unix.GetFolderPathCore.cs create mode 100644 src/mono/netcore/System.Private.CoreLib/src/System/Environment.iOS.cs diff --git a/src/libraries/Common/src/Interop/OSX/System.Native/Interop.SearchPath.cs b/src/libraries/Common/src/Interop/OSX/System.Native/Interop.SearchPath.cs new file mode 100644 index 0000000000000..af3e6110c9bab --- /dev/null +++ b/src/libraries/Common/src/Interop/OSX/System.Native/Interop.SearchPath.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#nullable enable +using System; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Sys + { + [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_SearchPath")] + internal static extern string? SearchPath(NSSearchPathDirectory folderId); + + internal enum NSSearchPathDirectory + { + NSApplicationDirectory = 1, + NSLibraryDirectory = 5, + NSUserDirectory = 7, + NSDocumentDirectory = 9, + NSDesktopDirectory = 12, + NSCachesDirectory = 13, + NSMoviesDirectory = 17, + NSMusicDirectory = 18, + NSPicturesDirectory = 19 + } + } +} diff --git a/src/libraries/Native/Unix/System.Native/CMakeLists.txt b/src/libraries/Native/Unix/System.Native/CMakeLists.txt index f4bd26b5f3c3b..11d3456ec7c3f 100644 --- a/src/libraries/Native/Unix/System.Native/CMakeLists.txt +++ b/src/libraries/Native/Unix/System.Native/CMakeLists.txt @@ -23,7 +23,9 @@ set(NATIVE_SOURCES ) if (CLR_CMAKE_TARGET_IOS) - set(NATIVE_SOURCES ${NATIVE_SOURCES} pal_log.m) + set(NATIVE_SOURCES ${NATIVE_SOURCES} + pal_log.m + pal_searchpath.m) else () set(NATIVE_SOURCES ${NATIVE_SOURCES} pal_console.c) endif () diff --git a/src/libraries/Native/Unix/System.Native/pal_searchpath.h b/src/libraries/Native/Unix/System.Native/pal_searchpath.h new file mode 100644 index 0000000000000..21ee57d665cfa --- /dev/null +++ b/src/libraries/Native/Unix/System.Native/pal_searchpath.h @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#pragma once + +#include "pal_compiler.h" +#include "pal_types.h" + +PALEXPORT const char* SystemNative_SearchPath(int32_t folderId); diff --git a/src/libraries/Native/Unix/System.Native/pal_searchpath.m b/src/libraries/Native/Unix/System.Native/pal_searchpath.m new file mode 100644 index 0000000000000..a4f150da76452 --- /dev/null +++ b/src/libraries/Native/Unix/System.Native/pal_searchpath.m @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#include "pal_searchpath.h" +#import + +const char* SystemNative_SearchPath(int32_t folderId) +{ + NSSearchPathDirectory spd = (NSSearchPathDirectory) folderId; + NSURL* url = [[[NSFileManager defaultManager] URLsForDirectory:spd inDomains:NSUserDomainMask] lastObject]; + const char* path = [[url path] UTF8String]; + return path == NULL ? NULL : strdup (path); +} diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index b5a654a61e63b..689c61594bb14 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -1723,6 +1723,7 @@ + diff --git a/src/libraries/System.Private.CoreLib/src/System/Environment.Unix.GetFolderPathCore.cs b/src/libraries/System.Private.CoreLib/src/System/Environment.Unix.GetFolderPathCore.cs new file mode 100644 index 0000000000000..ad37bba3746e5 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Environment.Unix.GetFolderPathCore.cs @@ -0,0 +1,250 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; + +namespace System +{ + public static partial class Environment + { + private static Func? s_directoryCreateDirectory; + + private static string GetFolderPathCore(SpecialFolder folder, SpecialFolderOption option) + { + // Get the path for the SpecialFolder + string path = GetFolderPathCoreWithoutValidation(folder); + 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; + } + else + { + Debug.Assert(option == SpecialFolderOption.Create); + + Func createDirectory = LazyInitializer.EnsureInitialized(ref s_directoryCreateDirectory, () => + { + Type dirType = Type.GetType("System.IO.Directory, System.IO.FileSystem, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", throwOnError: true)!; + MethodInfo mi = dirType.GetTypeInfo().GetDeclaredMethod("CreateDirectory")!; + return (Func)mi.CreateDelegate(typeof(Func)); + }); + 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: + 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; + } + + 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 (string.IsNullOrEmpty(config) || config[0] != '/') + { + 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 (!string.IsNullOrEmpty(envPath) && envPath[0] == '/') + { + 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++; + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/Environment.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/Environment.Unix.cs index 993b23a89fc68..c29a56c14b53f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Environment.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Environment.Unix.cs @@ -13,8 +13,6 @@ namespace System { public static partial class Environment { - private static Func? s_directoryCreateDirectory; - public static bool UserInteractive => true; private static string CurrentDirectoryCore @@ -49,236 +47,6 @@ private static string ExpandEnvironmentVariablesCore(string name) return result.ToString(); } - private static string GetFolderPathCore(SpecialFolder folder, SpecialFolderOption option) - { - // Get the path for the SpecialFolder - string path = GetFolderPathCoreWithoutValidation(folder); - 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; - } - else - { - Debug.Assert(option == SpecialFolderOption.Create); - - Func createDirectory = LazyInitializer.EnsureInitialized(ref s_directoryCreateDirectory, () => - { - Type dirType = Type.GetType("System.IO.Directory, System.IO.FileSystem, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", throwOnError: true)!; - MethodInfo mi = dirType.GetTypeInfo().GetDeclaredMethod("CreateDirectory")!; - return (Func)mi.CreateDelegate(typeof(Func)); - }); - 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 || TARGET_IOS - 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 || TARGET_IOS - 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; - } - - 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 (string.IsNullOrEmpty(config) || config[0] != '/') - { - 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 (!string.IsNullOrEmpty(envPath) && envPath[0] == '/') - { - 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++; - } - public static string[] GetLogicalDrives() => Interop.Sys.GetAllMountPoints(); private static bool Is64BitOperatingSystemWhen32BitProcess => false; diff --git a/src/mono/netcore/System.Private.CoreLib/System.Private.CoreLib.csproj b/src/mono/netcore/System.Private.CoreLib/System.Private.CoreLib.csproj index da3dc7b7d0cc1..7c0684cfbe85b 100644 --- a/src/mono/netcore/System.Private.CoreLib/System.Private.CoreLib.csproj +++ b/src/mono/netcore/System.Private.CoreLib/System.Private.CoreLib.csproj @@ -291,6 +291,12 @@ + + + + Common\Interop\OSX\Interop.SearchPath.cs + + diff --git a/src/mono/netcore/System.Private.CoreLib/src/System/Environment.Unix.Mono.cs b/src/mono/netcore/System.Private.CoreLib/src/System/Environment.Unix.Mono.cs index e4ab168b1e6b4..b1ff2502d5a21 100644 --- a/src/mono/netcore/System.Private.CoreLib/src/System/Environment.Unix.Mono.cs +++ b/src/mono/netcore/System.Private.CoreLib/src/System/Environment.Unix.Mono.cs @@ -21,10 +21,7 @@ private static string GetEnvironmentVariableCore(string variable) if (s_environment == null) { - using (var h = RuntimeMarshal.MarshalString(variable)) - { - return internalGetEnvironmentVariable_native(h.Value); - } + return InternalGetEnvironmentVariable(variable); } variable = TrimStringOnFirstZero(variable); @@ -35,6 +32,14 @@ private static string GetEnvironmentVariableCore(string variable) } } + private static string InternalGetEnvironmentVariable(string name) + { + using (SafeStringMarshal handle = RuntimeMarshal.MarshalString(name)) + { + return internalGetEnvironmentVariable_native(handle.Value); + } + } + private static unsafe void SetEnvironmentVariableCore(string variable, string? value) { Debug.Assert(variable != null); @@ -97,10 +102,7 @@ private static Dictionary GetSystemEnvironmentVariables() { if (name != null) { - using (var h = RuntimeMarshal.MarshalString(name)) - { - results.Add(name, internalGetEnvironmentVariable_native(h.Value)); - } + results.Add(name, InternalGetEnvironmentVariable(name)); } } diff --git a/src/mono/netcore/System.Private.CoreLib/src/System/Environment.iOS.cs b/src/mono/netcore/System.Private.CoreLib/src/System/Environment.iOS.cs new file mode 100644 index 0000000000000..3b7b55d556534 --- /dev/null +++ b/src/mono/netcore/System.Private.CoreLib/src/System/Environment.iOS.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#nullable enable +using System.IO; +using System.Threading; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using NSSearchPathDirectory = Interop.Sys.NSSearchPathDirectory; + +namespace System +{ + public static partial class Environment + { + private static Dictionary? s_specialFolders; + + private static string GetFolderPathCore(SpecialFolder folder, SpecialFolderOption option) + { + if (s_specialFolders == null) + { + Interlocked.CompareExchange(ref s_specialFolders, new Dictionary(), null); + } + + string path; + lock (s_specialFolders) + { + if (!s_specialFolders.TryGetValue(folder, out path)) + { + path = GetSpecialFolder(folder) ?? string.Empty; + s_specialFolders[folder] = path; + } + } + return path; + } + + private static string? GetSpecialFolder(SpecialFolder folder) + { + switch (folder) + { + // TODO: fix for tvOS (https://github.com/dotnet/runtime/issues/34007) + // The "normal" NSDocumentDirectory is a read-only directory on tvOS + // and that breaks a lot of assumptions in the runtime and the BCL + + case SpecialFolder.Personal: + case SpecialFolder.LocalApplicationData: + return Interop.Sys.SearchPath(NSSearchPathDirectory.NSDocumentDirectory); + + case SpecialFolder.ApplicationData: + // note: at first glance that looked like a good place to return NSLibraryDirectory + // but it would break isolated storage for existing applications + return Path.Combine(Interop.Sys.SearchPath(NSSearchPathDirectory.NSDocumentDirectory), ".config"); + + case SpecialFolder.Resources: + return Interop.Sys.SearchPath(NSSearchPathDirectory.NSLibraryDirectory); // older (8.2 and previous) would return String.Empty + + case SpecialFolder.Desktop: + case SpecialFolder.DesktopDirectory: + return Interop.Sys.SearchPath(NSSearchPathDirectory.NSDesktopDirectory); + + case SpecialFolder.MyMusic: + return Interop.Sys.SearchPath(NSSearchPathDirectory.NSMusicDirectory); + + case SpecialFolder.MyPictures: + return Interop.Sys.SearchPath(NSSearchPathDirectory.NSPicturesDirectory); + + case SpecialFolder.Templates: + return Path.Combine(Interop.Sys.SearchPath(NSSearchPathDirectory.NSDocumentDirectory), "Templates"); + + case SpecialFolder.MyVideos: + return Interop.Sys.SearchPath(NSSearchPathDirectory.NSMoviesDirectory); + + case SpecialFolder.CommonTemplates: + return "/usr/share/templates"; + + case SpecialFolder.Fonts: + return Path.Combine(Interop.Sys.SearchPath(NSSearchPathDirectory.NSDocumentDirectory), ".fonts"); + + case SpecialFolder.Favorites: + return Path.Combine(Interop.Sys.SearchPath(NSSearchPathDirectory.NSLibraryDirectory), "Favorites"); + + case SpecialFolder.ProgramFiles: + return Interop.Sys.SearchPath(NSSearchPathDirectory.NSApplicationDirectory); + + case SpecialFolder.InternetCache: + return Interop.Sys.SearchPath(NSSearchPathDirectory.NSCachesDirectory); + + case SpecialFolder.UserProfile: + return InternalGetEnvironmentVariable("HOME"); + + case SpecialFolder.CommonApplicationData: + return "/usr/share"; + + default: + return string.Empty; + } + } + } +}