Skip to content

Commit e89f76b

Browse files
authored
Merge pull request #1370 from sillsdev/robust-file-utilities
Fixed problems in GetSafeDirectories and GetDirectoryDistributedWithApplication
2 parents 3086d27 + e8eb899 commit e89f76b

File tree

4 files changed

+91
-47
lines changed

4 files changed

+91
-47
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
9494
- [SIL.Core.Desktop] Implemented GetDefaultProgramForFileType (as trenamed) in a way that works on Windows 11, Mono (probably) and MacOS (untested).
9595
- [SIL.Media] MediaInfo.HaveNecessaryComponents properly returns true if FFprobe is on the system path.
9696
- [SIL.Media] Made MediaInfo.FFprobeFolder look for and return the folder when first accessed, even if no prior call to the setter or other action had caused it t be found.
97+
- [SIL.Core] Made GetSafeDirectories not crash and simply not return any subdirectory the user does not have permission to access.
98+
- [SIL.Core] In GetDirectoryDistributedWithApplication, prevented a failure in accessing one of the specified subfolders from allowing it to try the others.
9799

98100
### Removed
99101

SIL.Core/IO/DirectoryHelper.cs

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
// Copyright (c) 2024 SIL Global
22
// This software is licensed under the MIT License (http://opensource.org/licenses/MIT)
33
using System;
4+
using System.Collections.Generic;
45
using System.IO;
56
using System.Linq;
7+
using System.Security;
68
using JetBrains.Annotations;
79
using SIL.PlatformUtilities;
10+
using SIL.Reporting;
811
using static System.Environment;
912
using static System.Environment.SpecialFolder;
1013
using static System.IO.Path;
@@ -25,7 +28,7 @@ public static void Copy(string sourcePath, string destinationPath, bool overwrit
2528
File.Copy(filepath, Combine(destinationPath, filename), overwrite);
2629
}
2730

28-
// Copy all the sub directories.
31+
// Copy all the subdirectories.
2932
foreach (var directoryPath in Directory.GetDirectories(sourcePath))
3033
{
3134
var directoryName = GetFileName(directoryPath);
@@ -106,21 +109,45 @@ public static bool IsEmpty(string path, bool onlyCheckForFiles = false)
106109

107110
/// <summary>
108111
/// Return subdirectories of <paramref name="path"/> that are not system or hidden.
112+
/// Subdirectories which the user does not have permission to access will also be skipped.
109113
/// There are some cases where our call to Directory.GetDirectories() throws.
110114
/// For example, when the access permissions on a folder are set so that it can't be read.
111115
/// Another possible example may be Windows Backup files, which apparently look like directories.
112116
/// </summary>
113117
/// <param name="path">Directory path to look in.</param>
114-
/// <returns>Zero or more directory names that are not system or hidden.</returns>
115-
/// <exception cref="UnauthorizedAccessException">E.g. when the user does not have
116-
/// read permission.</exception>
118+
/// <returns>Zero or more directory names that are not system or hidden</returns>
119+
/// <exception cref="UnauthorizedAccessException">The caller does not have the required
120+
/// permission to access the subdirectories of <paramref name="path"/>.</exception>
121+
/// <exception cref="ArgumentException">
122+
/// <paramref name="path" /> is a zero-length string, contains only white space, or contains one or more invalid characters. You can query for invalid characters by using the <see cref="M:System.IO.Path.GetInvalidPathChars" /> method.</exception>
123+
/// <exception cref="ArgumentNullException">
124+
/// <paramref name="path" /> is <see langword="null" />.</exception>
125+
/// <exception cref="PathTooLongException">The specified <paramref name="path" />, file name,
126+
/// or both exceed the system-defined maximum length.</exception>
127+
/// <exception cref="IOException"><paramref name="path" /> is a file name.</exception>
128+
/// <exception cref="DirectoryNotFoundException">The specified <paramref name="path" /> is
129+
/// invalid (for example, it is on an unmapped drive).</exception>
117130
public static string[] GetSafeDirectories(string path)
118131
{
119-
return (from directoryName in Directory.GetDirectories(path)
120-
let dirInfo = new DirectoryInfo(directoryName)
121-
where (dirInfo.Attributes & FileAttributes.System) != FileAttributes.System
122-
where (dirInfo.Attributes & FileAttributes.Hidden) != FileAttributes.Hidden
123-
select directoryName).ToArray();
132+
var list = new List<string>();
133+
foreach (var directoryName in Directory.GetDirectories(path))
134+
{
135+
try
136+
{
137+
var dirInfo = new DirectoryInfo(directoryName);
138+
if ((dirInfo.Attributes & FileAttributes.System) != FileAttributes.System)
139+
{
140+
if ((dirInfo.Attributes & FileAttributes.Hidden) != FileAttributes.Hidden)
141+
list.Add(directoryName);
142+
}
143+
}
144+
catch (SecurityException e)
145+
{
146+
Logger.WriteError(e);
147+
}
148+
}
149+
150+
return list.ToArray();
124151
}
125152

126153
#region Utilities that are specific to the Windows OS.

SIL.Core/IO/FileLocationUtilities.cs

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Text;
77
using SIL.PlatformUtilities;
88
using SIL.Reflection;
9+
using SIL.Reporting;
910

1011
namespace SIL.IO
1112
{
@@ -63,7 +64,7 @@ private static string LocateExecutableDistributedWithApplication(string[] partsO
6364
/// [applicationFolder]/[distFileFolderName]/[subPath1]/[subPathN] or
6465
/// [applicationFolder]/[distFileFolderName]/[platform]/[subPath1]/[subPathN] or
6566
/// [applicationFolder]/[subPath1]/[subPathN]. If the executable can't be found we
66-
/// search in the ProgramFiles folder ([programfiles]/[subPath1]/[subPathN]) on Windows,
67+
/// search in the ProgramFiles folder ([ProgramFiles]/[subPath1]/[subPathN]) on Windows,
6768
/// and in the folders included in the PATH environment variable on Linux.
6869
/// When the executable has a prefix of ".exe" we're running on Linux we also
6970
/// search for files without the prefix.
@@ -109,7 +110,7 @@ public static string LocateExecutable(params string[] partsOfTheSubPath)
109110
}
110111

111112
private static string[] DirectoriesHoldingFiles => new[] {string.Empty, "DistFiles",
112-
"common" /*for wesay*/, "src" /*for Bloom*/};
113+
"common" /*for WeSay*/, "src" /*for Bloom*/};
113114

114115
/// <summary>
115116
/// Find a file which, on a development machine, lives in [solution]/[distFileFolderName]/[subPath],
@@ -234,7 +235,7 @@ public static string GetDirectoryDistributedWithApplication(params string[] part
234235
///
235236
/// When subFoldersToSearch are not specified:
236237
/// - If fallBackToDeepSearch is true, then the entire program files folder (and all
237-
/// (sub folders) is searched.
238+
/// sub folders) is searched.
238239
/// - If fallBackToDeepSearch is false, then only the top-level program files
239240
/// folder is searched.
240241
///
@@ -278,11 +279,11 @@ private static string LocateInProgramFilesFolder(string exeName, SearchOption sr
278279
if (subFoldersToSearch.Length == 0)
279280
subFoldersToSearch = new[] { string.Empty };
280281

281-
foreach (var progFolder in Enumerable.Where<string>(GetPossibleProgramFilesFolders(), Directory.Exists))
282+
foreach (var progFolder in GetPossibleProgramFilesFolders().Where(Directory.Exists))
282283
{
283284
// calling Directory.GetFiles("C:\Program Files", exeName, SearchOption.AllDirectories) will fail
284285
// if even one of the children of the Program Files doesn't allow you to search it.
285-
// So instead we first gather up all the children directories, and then search those.
286+
// So instead we first gather all the child directories, and then search those.
286287
// Some will give us access denied, and that's fine, we skip them.
287288
// But we don't want to look in child directories on Linux because GetPossibleProgramFilesFolders()
288289
// gives us the individual elements of the PATH environment variable, and these specify exactly
@@ -291,11 +292,24 @@ private static string LocateInProgramFilesFolder(string exeName, SearchOption sr
291292
{
292293
if (Platform.IsWindows)
293294
{
294-
foreach (var subDir in DirectoryHelper.GetSafeDirectories(path))
295+
string[] subDirectories = null;
296+
try
295297
{
296-
var tgtPath = GetFiles(exeName, srcOption, subDir);
297-
if (!string.IsNullOrEmpty(tgtPath))
298-
return tgtPath;
298+
subDirectories = DirectoryHelper.GetSafeDirectories(path);
299+
}
300+
catch (Exception e)
301+
{
302+
Logger.WriteError(e);
303+
}
304+
305+
if (subDirectories != null)
306+
{
307+
foreach (var subDir in subDirectories)
308+
{
309+
var tgtPath = GetFiles(exeName, srcOption, subDir);
310+
if (!string.IsNullOrEmpty(tgtPath))
311+
return tgtPath;
312+
}
299313
}
300314
}
301315
else
@@ -336,14 +350,18 @@ private static IEnumerable<string> GetPossibleProgramFilesFolders()
336350
{
337351
if (!Platform.IsWindows)
338352
{
339-
foreach (var dir in Environment.GetEnvironmentVariable("PATH").Split(':'))
340-
yield return dir;
353+
var path = Environment.GetEnvironmentVariable("PATH");
354+
if (!string.IsNullOrEmpty(path))
355+
{
356+
foreach (var dir in path.Split(':'))
357+
yield return dir;
358+
}
341359
yield return "/opt"; // RAMP is installed in the /opt directory by default
342360
}
343361

344-
var pf = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
345-
yield return pf.Replace(" (x86)", string.Empty);
346-
yield return pf.Replace(" (x86)", string.Empty) + " (x86)";
362+
yield return Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
363+
if (Environment.Is64BitProcess)
364+
yield return Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
347365
}
348366
}
349367
}

SIL.Core/Reporting/Logger.cs

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,20 @@
44
using System.IO;
55
using System.Linq;
66
using System.Text;
7+
using JetBrains.Annotations;
78
using SIL.IO;
89

910
namespace SIL.Reporting
1011
{
11-
1212
public interface ILogger
1313
{
1414
/// <summary>
15-
/// This is something that should be listed in the source control checkin
15+
/// This is something that should be listed in the source control check-in
1616
/// </summary>
1717
void WriteConciseHistoricalEvent(string message, params object[] args);
1818
}
19+
20+
[PublicAPI]
1921
public class MultiLogger: ILogger
2022
{
2123
private readonly List<ILogger> _loggers= new List<ILogger>();
@@ -58,6 +60,8 @@ public void WriteConciseHistoricalEvent(string message, params object[] args)
5860
{
5961
_builder.Append(Logger.FormatMessage(message, args));
6062
}
63+
64+
[PublicAPI]
6165
public string GetLogText()
6266
{
6367
return _builder.ToString();
@@ -67,7 +71,7 @@ public string GetLogText()
6771
/// ----------------------------------------------------------------------------------------
6872
/// <summary>
6973
/// Logs stuff to a file created in
70-
/// c:\Documents and Settings\Username\Local Settings\Temp\Companyname\Productname\Log.txt
74+
/// c:\Documents and Settings\Username\Local Settings\Temp\Companyname\ProductName\Log.txt
7175
/// </summary>
7276
/// ----------------------------------------------------------------------------------------
7377
public class Logger: IDisposable, ILogger
@@ -206,7 +210,7 @@ private void RestartMinorEvents()
206210
public void CheckDisposed()
207211
{
208212
if (IsDisposed)
209-
throw new ObjectDisposedException(String.Format("'{0}' in use after being disposed.", GetType().Name));
213+
throw new ObjectDisposedException($"'{GetType().Name}' in use after being disposed.");
210214
}
211215

212216
/// <summary>
@@ -217,10 +221,7 @@ public void CheckDisposed()
217221
/// <summary>
218222
/// See if the object has been disposed.
219223
/// </summary>
220-
public bool IsDisposed
221-
{
222-
get { return m_isDisposed; }
223-
}
224+
public bool IsDisposed => m_isDisposed;
224225

225226
/// <summary>
226227
/// Finalizer, in case client doesn't dispose it.
@@ -243,7 +244,7 @@ public void Dispose()
243244
{
244245
Dispose(true);
245246
// This object will be cleaned up by the Dispose method.
246-
// Therefore, you should call GC.SupressFinalize to
247+
// Therefore, you should call GC.SuppressFinalize to
247248
// take this object off the finalization queue
248249
// and prevent finalization code for this object
249250
// from executing a second time.
@@ -258,7 +259,7 @@ public void Dispose()
258259
/// Both managed and unmanaged resources can be disposed.
259260
///
260261
/// 2. If disposing is false, the method has been called by the
261-
/// runtime from inside the finalizer and you should not reference (access)
262+
/// runtime from inside the finalizer, and you should not reference (access)
262263
/// other managed objects, as they already have been garbage collected.
263264
/// Only unmanaged resources can be disposed.
264265
/// </summary>
@@ -295,7 +296,7 @@ protected virtual void Dispose(bool disposing)
295296
#endregion IDisposable & Co. implementation
296297

297298
/// <summary>
298-
/// This is for version-control checkin descriptions. E.g. "Deleted foobar".
299+
/// This is for version-control check-in descriptions. E.g. "Deleted foobar".
299300
/// </summary>
300301
/// <param name="message"></param>
301302
/// <param name="args"></param>
@@ -354,10 +355,7 @@ private string GetLogTextAndStartOver()
354355
/// <summary>
355356
/// added this for a case of a catastrophic error so bad I couldn't get the means of finding out what just happened
356357
/// </summary>
357-
public static string MinorEventsLog
358-
{
359-
get { return Singleton.m_minorEvents.ToString(); }
360-
}
358+
public static string MinorEventsLog => Singleton.m_minorEvents.ToString();
361359

362360
/// <summary>
363361
/// the place on disk where we are storing the log
@@ -373,10 +371,7 @@ public static string LogPath
373371
return _actualLogPath;
374372
}
375373
}
376-
public static Logger Singleton
377-
{
378-
get { return _singleton; }
379-
}
374+
public static Logger Singleton => _singleton;
380375

381376
private static void SetActualLogPath(string filename)
382377
{
@@ -407,7 +402,7 @@ private void WriteEventCore(string message, params object[] args)
407402
{
408403
#endif
409404
CheckDisposed();
410-
if (m_out != null && m_out.BaseStream.CanWrite)
405+
if (m_out is { BaseStream: { CanWrite: true } })
411406
{
412407
m_out.Write(DateTime.Now.ToLongTimeString() + "\t");
413408
m_out.WriteLine(FormatMessage(message, args));
@@ -455,7 +450,7 @@ public static void WriteError(string msg, Exception e)
455450
}
456451

457452
/// <summary>
458-
/// only a limitted number of the most recent of these events will show up in the log
453+
/// only a limited number of the most recent of these events will show up in the log
459454
/// </summary>
460455
public static void WriteMinorEvent(string message, params object[] args)
461456
{
@@ -483,7 +478,7 @@ internal static string FormatMessage(string message, object[] args)
483478
}
484479
catch (Exception)
485480
{
486-
return string.Format("Error formatting message with {0} args: {1}", args.Length, message);
481+
return $"Error formatting message with {args.Length} args: {message}";
487482
}
488483
}
489484
return message;
@@ -520,16 +515,18 @@ private void WriteMinorEventCore(string message, params object[] args)
520515
}
521516
}
522517

518+
[PublicAPI]
523519
public static void ShowUserTheLogFile()
524520
{
525521
Singleton.m_out.Flush();
526522
Process.Start(LogPath);
527523
}
528524

529525
/// <summary>
530-
/// if you're working with unmanaged code and get a System.AccessViolationException, well you're toast, and anything
531-
/// that requires UI is gonna freeze up. So call this instead
526+
/// If you're working with unmanaged code and get a System.AccessViolationException, you're
527+
/// toast, and anything that requires UI is going to freeze up. So call this instead.
532528
/// </summary>
529+
[PublicAPI]
533530
public static void ShowUserATextFileRelatedToCatastrophicError(Exception reallyBadException)
534531
{
535532
//did this special because we don't have an event loop to drive the error reporting dialog if Application.Run() dies

0 commit comments

Comments
 (0)