Skip to content

Commit

Permalink
[mono] Implement Environment.GetFolderPath on iOS (#34022)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
EgorBo authored Mar 31, 2020
1 parent be469ad commit 372bf42
Show file tree
Hide file tree
Showing 10 changed files with 422 additions and 241 deletions.
Original file line number Diff line number Diff line change
@@ -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
}
}
}
4 changes: 3 additions & 1 deletion src/libraries/Native/Unix/System.Native/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 ()
Expand Down
10 changes: 10 additions & 0 deletions src/libraries/Native/Unix/System.Native/pal_searchpath.h
Original file line number Diff line number Diff line change
@@ -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);
14 changes: 14 additions & 0 deletions src/libraries/Native/Unix/System.Native/pal_searchpath.m
Original file line number Diff line number Diff line change
@@ -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 <Foundation/Foundation.h>

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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -1723,6 +1723,7 @@
<Compile Include="$(MSBuildThisFileDirectory)System\Diagnostics\Tracing\RuntimeEventSourceHelper.Unix.cs" Condition="'$(FeaturePerfTracing)' == 'true'" />
<Compile Include="$(MSBuildThisFileDirectory)System\Environment.NoRegistry.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Environment.Unix.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Environment.Unix.GetFolderPathCore.cs" Condition="'$(TargetsiOS)' != 'true'" />
<Compile Include="$(MSBuildThisFileDirectory)System\Globalization\CalendarData.Unix.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Globalization\CompareInfo.Unix.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Globalization\CultureData.Unix.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, object>? 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<string, object> 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<string, object>)mi.CreateDelegate(typeof(Func<string, object>));
});
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++;
}
}
}
Loading

0 comments on commit 372bf42

Please sign in to comment.