Paradox Game Engine  v1.0.0 beta06
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Events Macros Pages
UPath.cs
Go to the documentation of this file.
1 // Copyright (c) 2014 Silicon Studio Corp. (http://siliconstudio.co.jp)
2 // This file is distributed under GPL v3. See LICENSE.md for details.
3 using System;
4 using System.Collections.Generic;
5 using System.IO;
6 using System.Linq;
7 using System.Text;
8 
9 namespace SiliconStudio.Core.IO
10 {
11  /// <summary>
12  /// Base class that describes a uniform path and provides method to manipulate them. Concrete class are <see cref="UFile"/> and <see cref="UDirectory"/>.
13  /// This class is immutable and its descendants are immutable. See remarks.
14  /// </summary>
15  /// <remarks>
16  /// <para>A uniform path contains only characters '/' to separate directories and doesn't contain any successive
17  /// '/' or './'. This class is used to represent a path, relative or absolute to a directory or filename.</para>
18  /// <para>This class can be used to represent uniforms paths both on windows or unix platforms</para>
19  /// TODO Provide more documentation on how to use this class
20  /// </remarks>
21  public abstract class UPath : IEquatable<UPath>, IComparable
22  {
23  private readonly static HashSet<char> InvalidFileNameChars = new HashSet<char>(System.IO.Path.GetInvalidFileNameChars());
24 
25  private readonly string fullPath; // is always non-null
26  private readonly int hashCode;
27 
28  private readonly StringSpan driveSpan;
29 
30  internal readonly StringSpan DirectorySpan;
31 
32  internal readonly StringSpan NameSpan;
33 
34  internal readonly StringSpan ExtensionSpan;
35 
36  /// <summary>
37  /// The directory separator char '/' used to separate directory in an url.
38  /// </summary>
39  public const char DirectorySeparatorChar = '/';
40 
41  /// <summary>
42  /// The directory separator char '\' used to separate directory in an url.
43  /// </summary>
44  public const char DirectorySeparatorCharAlt = '\\';
45 
46  /// <summary>
47  /// The directory separator string '/' used to separate directory in an url.
48  /// </summary>
49  public const string DirectorySeparatorString = "/";
50 
51  /// <summary>
52  /// The directory separator string '\' used to separate directory in an url.
53  /// </summary>
54  public const string DirectorySeparatorStringAlt = "\\";
55 
56  /// <summary>
57  /// Initializes a new instance of the <see cref="UPath" /> class from a file path.
58  /// </summary>
59  /// <param name="filePath">The full path to a file.</param>
60  /// <param name="isDirectory">if set to <c>true</c> the filePath is considered as a directory and not a filename.</param>
61  internal UPath(string filePath, bool isDirectory)
62  {
63  if (!isDirectory && filePath != null && (filePath.EndsWith("/") || filePath.EndsWith(@"\")))
64  {
65  throw new ArgumentException("A file path cannot end with with directory char '\\' or '/' ");
66  }
67 
68  fullPath = Decode(filePath, isDirectory, out driveSpan, out DirectorySpan, out NameSpan, out ExtensionSpan);
69  hashCode = ComputeStringHashCodeCaseInsensitive(fullPath);
70  }
71 
72  /// <summary>
73  /// Initializes a new instance of the <see cref="UPath" /> class.
74  /// </summary>
75  /// <param name="drive">The drive.</param>
76  /// <param name="directory">The directory.</param>
77  /// <param name="name">The name.</param>
78  /// <param name="extension">The extension.</param>
79  /// <exception cref="System.ArgumentException">Invalid DirectorySeparatorChar for name</exception>
80  internal UPath(string drive, string directory, string name, string extension)
81  {
82  var builder = new StringBuilder();
83  if (!string.IsNullOrWhiteSpace(drive))
84  {
85  builder.Append(drive);
86  }
87  if (!string.IsNullOrWhiteSpace(directory))
88  {
89  builder.Append(directory);
90  builder.Append(DirectorySeparatorChar);
91  }
92  if (!string.IsNullOrWhiteSpace(name))
93  {
94  builder.Append(name);
95  }
96  if (!string.IsNullOrWhiteSpace(extension))
97  {
98  builder.Append(extension);
99  }
100 
101  fullPath = Decode(builder.ToString(), string.IsNullOrWhiteSpace(name) && string.IsNullOrWhiteSpace(extension), out this.driveSpan, out this.DirectorySpan, out this.NameSpan, out this.ExtensionSpan);
102  hashCode = ComputeStringHashCodeCaseInsensitive(fullPath);
103  }
104 
105  protected UPath(string fullPath, StringSpan driveSpan, StringSpan directorySpan)
106  {
107  this.fullPath = fullPath;
108  this.hashCode = ComputeStringHashCodeCaseInsensitive(fullPath);
109  this.driveSpan = driveSpan;
110  this.DirectorySpan = directorySpan;
111  }
112 
113  /// <summary>
114  /// Gets the full path ((drive?)(directory?/)(name.ext?)). An empty path is an empty string.
115  /// </summary>
116  /// <value>The full path.</value>
117  public string FullPath
118  {
119  get
120  {
121  return fullPath;
122  }
123  }
124 
125  /// <summary>
126  /// Gets a value indicating whether this instance has a <see cref="Drive"/> != null.
127  /// </summary>
128  /// <value><c>true</c> if this instance has drive; otherwise, <c>false</c>.</value>
129  public bool HasDrive
130  {
131  get
132  {
133  return driveSpan.IsValid;
134  }
135  }
136 
137 
138  /// <summary>
139  /// Gets the drive (contains the ':' if any), can be null.
140  /// </summary>
141  /// <returns>The drive.</returns>
142  public string GetDrive()
143  {
144  return driveSpan.IsValid ? fullPath.Substring(driveSpan) : null;
145  }
146 
147  /// <summary>
148  /// Gets a value indicating whether this instance has a <see cref="GetDirectory()"/> != null;
149  /// </summary>
150  /// <value><c>true</c> if this instance has directory; otherwise, <c>false</c>.</value>
151  public bool HasDirectory
152  {
153  get
154  {
155  return DirectorySpan.IsValid;
156  }
157  }
158 
159  /// <summary>
160  /// Gets a value indicating whether this instance is a directory only path.
161  /// </summary>
162  /// <value><c>true</c> if this instance is directory only; otherwise, <c>false</c>.</value>
163  public bool IsDirectoryOnly
164  {
165  get
166  {
167  return FullPath == string.Empty || (HasDirectory && !IsFile);
168  }
169  }
170 
171  /// <summary>
172  /// Gets a value indicating whether this location is a relative location.
173  /// </summary>
174  /// <value><c>true</c> if this instance is relative; otherwise, <c>false</c>.</value>
175  public bool IsRelative
176  {
177  get
178  {
179  return !IsAbsolute;
180  }
181  }
182 
183  /// <summary>
184  /// Gets the directory. Can be null.
185  /// </summary>
186  /// <returns>The directory.</returns>
187  public string GetDirectory()
188  {
189  return DirectorySpan.IsValid ? fullPath.Substring(DirectorySpan) : null;
190  }
191 
192  /// <summary>
193  /// Gets the parent directory of this instance. For a file, this is the directory directly containing the file.
194  /// For a directory, this is the parent directory.
195  /// </summary>
196  /// <returns>The parent directory or <see cref="UDirectory.Empty"/> if no directory found.</returns>
198  {
199  if (IsFile)
200  {
201  if (DirectorySpan.IsValid)
202  {
203  return new UDirectory(fullPath.Substring(0, DirectorySpan.Next), driveSpan, DirectorySpan);
204  }
205  if (driveSpan.IsValid)
206  {
207  return new UDirectory(fullPath.Substring(driveSpan), driveSpan, new StringSpan());
208  }
209  }
210  else if (DirectorySpan.IsValid)
211  {
212  if (DirectorySpan.Length > 1)
213  {
214  var index = fullPath.IndexOfReverse(DirectorySeparatorChar);
215  if (index >= 0)
216  {
217  index = index == 0 ? index + 1 : index;
218  return new UDirectory(fullPath.Substring(0, index), driveSpan, new StringSpan(DirectorySpan.Start, index - DirectorySpan.Start));
219  }
220  }
221  if (driveSpan.IsValid)
222  {
223  return new UDirectory(fullPath.Substring(driveSpan), driveSpan, new StringSpan());
224  }
225  }
226 
227  return UDirectory.Empty;
228  }
229 
230  /// <summary>
231  /// Gets the full directory with <see cref="GetDrive()"/> + <see cref="GetDirectory()"/> or empty directory.
232  /// </summary>
233  /// <returns>System.String.</returns>
235  {
236  if (HasDirectory)
237  {
238  var subPath = fullPath.Substring(0, DirectorySpan.Start + DirectorySpan.Length);
239  return new UDirectory(subPath, driveSpan, DirectorySpan);
240  }
241 
242  if (HasDrive)
243  {
244  var subPath = fullPath.Substring(0, driveSpan.Length);
245  return new UDirectory(subPath, driveSpan, DirectorySpan);
246  }
247 
248  // TODO: Should we return null or an empty directory for this specific method?
249  return new UDirectory(string.Empty);
250  }
251 
252  /// <summary>
253  /// Gets a value indicating whether this instance is a location to a file. Can be null.
254  /// </summary>
255  /// <value><c>true</c> if this instance is file; otherwise, <c>false</c>.</value>
256  public bool IsFile
257  {
258  get
259  {
260  return NameSpan.IsValid || ExtensionSpan.IsValid;
261  }
262  }
263 
264  /// <summary>
265  /// Determines whether this instance is absolute.
266  /// </summary>
267  /// <returns><c>true</c> if this instance is absolute; otherwise, <c>false</c>.</returns>
268  public bool IsAbsolute
269  {
270  get
271  {
272  return HasDrive || (HasDirectory && fullPath[DirectorySpan.Start] == DirectorySeparatorChar);
273  }
274  }
275 
276  /// <summary>
277  /// Gets the type of the path (absolute or relative).
278  /// </summary>
279  /// <value>The type of the path.</value>
280  public UPathType PathType
281  {
282  get
283  {
284  return IsAbsolute ? UPathType.Absolute : UPathType.Relative;
285  }
286  }
287 
288  public bool Equals(UPath other)
289  {
290  if (ReferenceEquals(null, other)) return false;
291  if (ReferenceEquals(this, other)) return true;
292  return string.Equals(FullPath, other.FullPath, StringComparison.OrdinalIgnoreCase);
293  }
294 
295  public override bool Equals(object obj)
296  {
297  if (ReferenceEquals(null, obj)) return false;
298  if (ReferenceEquals(this, obj)) return true;
299  return obj is UPath && obj.GetType() == GetType() && Equals((UPath)obj);
300  }
301 
302  public override int GetHashCode()
303  {
304  return hashCode;
305  }
306 
307  private static int ComputeStringHashCodeCaseInsensitive(string text)
308  {
309  return text == null ? 0 : text.Aggregate(0, (current, t) => (current*397) ^ char.ToLowerInvariant(t));
310  }
311 
312  public int CompareTo(object obj)
313  {
314  var uPath = obj as UPath;
315  if (uPath != null)
316  {
317  if (FullPath != null && uPath.FullPath != null)
318  {
319  return String.Compare(FullPath, uPath.FullPath, StringComparison.OrdinalIgnoreCase);
320  }
321  }
322  return 0;
323  }
324 
325  public override string ToString()
326  {
327  return FullPath;
328  }
329 
330  /// <summary>
331  /// Converts this path to a Windows path (/ replaced by \)
332  /// </summary>
333  /// <returns>A string representation of this path in windows form.</returns>
334  public string ToWindowsPath()
335  {
336  return FullPath != null ? FullPath.Replace('/', '\\') : null;
337  }
338 
339  /// <summary>
340  /// Implements the ==.
341  /// </summary>
342  /// <param name="left">The left.</param>
343  /// <param name="right">The right.</param>
344  /// <returns>The result of the operator.</returns>
345  public static bool operator ==(UPath left, UPath right)
346  {
347  return Equals(left, right);
348  }
349 
350  /// <summary>
351  /// Implements the !=.
352  /// </summary>
353  /// <param name="left">The left.</param>
354  /// <param name="right">The right.</param>
355  /// <returns>The result of the operator.</returns>
356  public static bool operator !=(UPath left, UPath right)
357  {
358  return !Equals(left, right);
359  }
360 
361  /// <summary>
362  /// Combines the specified left uniform location and right location and return a new <see cref="UPath"/>
363  /// </summary>
364  /// <param name="leftPath">The left path.</param>
365  /// <param name="rightPath">The right path.</param>
366  /// <returns>The combination of both paths.</returns>
367  public static T Combine<T>(UDirectory leftPath, T rightPath) where T : UPath
368  {
369  if (leftPath == null) throw new ArgumentNullException("leftPath");
370  if (rightPath == null) throw new ArgumentNullException("rightPath");
371 
372  // If right path is absolute, return it directly
373  if (rightPath.IsAbsolute)
374  {
375  return rightPath;
376  }
377 
378  if (!leftPath.IsDirectoryOnly)
379  {
380  throw new ArgumentException("Expecting a directory", "leftPath");
381  }
382  var path = string.Format("{0}{1}{2}", leftPath.FullPath, string.IsNullOrEmpty(leftPath.FullPath) ? string.Empty : DirectorySeparatorString, rightPath.FullPath);
383  return rightPath is UFile ? (T)(object)new UFile(path) : (T)(object)new UDirectory(path);
384  }
385 
386  /// <summary>
387  /// Makes this instance relative to the specified anchor directory.
388  /// </summary>
389  /// <param name="anchorDirectory">The anchor directory.</param>
390  /// <returns>A relative path of this instance to the anchor directory.</returns>
391  public UPath MakeRelative(UDirectory anchorDirectory)
392  {
393  if (anchorDirectory == null) throw new ArgumentNullException("anchorDirectory");
394 
395  // If the toRelativize path is already relative, don't bother
396  if (IsRelative)
397  {
398  return this;
399  }
400 
401  // If anchor directory is not absolute directory, throw an error
402  if (!anchorDirectory.IsAbsolute || !anchorDirectory.IsDirectoryOnly)
403  {
404  throw new ArgumentException("Expecting an absolute directory", "anchorDirectory");
405  }
406 
407  if (anchorDirectory.HasDrive != HasDrive)
408  {
409  throw new InvalidOperationException("Path should have no drive information/or both drive information simultaneously");
410  }
411 
412  // Return a "." when the directory is the same
413  if (this is UDirectory && anchorDirectory == this)
414  {
415  return UDirectory.This;
416  }
417 
418  // Get the full path of the anchor directory
419  var anchorPath = anchorDirectory.FullPath;
420 
421  // Builds an absolute path for the toRelative path (directory-only)
422  var absoluteFile = Combine(anchorDirectory, this);
423  var absolutePath = absoluteFile.GetFullDirectory().FullPath;
424 
425  var relativePath = new StringBuilder();
426 
427  int index = anchorPath.Length;
428  bool foundCommonRoot = false;
429  for (; index >= 0; index--)
430  {
431  // Need to be a directory separator or end of string
432  if (!((index == anchorPath.Length || anchorPath[index] == DirectorySeparatorChar)))
433  continue;
434 
435  // Absolute path needs to also have a directory separator at the same location (or end of string)
436  if (index == absolutePath.Length || (index < absolutePath.Length && absolutePath[index] == DirectorySeparatorChar))
437  {
438  if (string.Compare(anchorPath, 0, absolutePath, 0, index, true) == 0)
439  {
440  foundCommonRoot = true;
441  break;
442  }
443  }
444 
445  relativePath.Append("..").Append(DirectorySeparatorChar);
446  }
447 
448  if (!foundCommonRoot)
449  {
450  return this;
451  }
452 
453  if (index < absolutePath.Length && absolutePath[index] == DirectorySeparatorChar)
454  {
455  index++;
456  }
457 
458  relativePath.Append(absolutePath.Substring(index));
459  if (absoluteFile is UFile)
460  {
461  // If not empty, add a separator
462  if (relativePath.Length > 0)
463  relativePath.Append(DirectorySeparatorChar);
464 
465  // Add filename
466  relativePath.Append(((UFile)absoluteFile).GetFileNameWithExtension());
467  }
468  var newPath = relativePath.ToString();
469  return IsDirectoryOnly ? (UPath)new UDirectory(newPath) : new UFile(newPath);
470  }
471 
472  /// <summary>
473  /// Performs an implicit conversion from <see cref="UPath"/> to <see cref="System.String"/>.
474  /// </summary>
475  /// <param name="url">The URL.</param>
476  /// <returns>The result of the conversion.</returns>
477  public static implicit operator string(UPath url)
478  {
479  return url == null ? null : url.FullPath;
480  }
481 
482  /// <summary>
483  /// Determines whether the specified path contains some directory characeters '\' or '/'
484  /// </summary>
485  /// <param name="path">The path.</param>
486  /// <returns><c>true</c> if the specified path contains some directory characeters '\' or '/'; otherwise, <c>false</c>.</returns>
487  public static bool HasDirectoryChars(string path)
488  {
489  return (path != null && (path.Contains(DirectorySeparatorChar) || path.Contains(DirectorySeparatorCharAlt)));
490  }
491 
492  /// <summary>
493  /// Determines whether the specified path is a valid <see cref="UPath"/>
494  /// </summary>
495  /// <param name="path">The path.</param>
496  /// <returns><c>true</c> if the specified path is valid; otherwise, <c>false</c>.</returns>
497  public static bool IsValid(string path)
498  {
499  string error;
500  Normalize(path, out error);
501  return error == null;
502  }
503 
504  /// <summary>
505  /// Normalize a path by replacing '\' by '/' and transforming relative '..' or current path '.' to an absolute path. See remarks.
506  /// </summary>
507  /// <param name="pathToNormalize">The path automatic normalize.</param>
508  /// <returns>A normalized path.</returns>
509  /// <exception cref="System.ArgumentException">If path is invalid</exception>
510  /// <remarks>Unlike <see cref="System.IO.Path" /> , this doesn't make a path absolute to the actual file system.</remarks>
511  public static string Normalize(string pathToNormalize)
512  {
513  string error;
514  var result = Normalize(pathToNormalize, out error);
515  if (error != null)
516  {
517  throw new ArgumentException(error, "pathToNormalize");
518  }
519  return result.ToString();
520  }
521 
522  /// <summary>
523  /// Normalize a path by replacing '\' by '/' and transforming relative '..' or current path '.' to an absolute path. See remarks.
524  /// </summary>
525  /// <param name="pathToNormalize">The path automatic normalize.</param>
526  /// <param name="error">The error or null if no errors.</param>
527  /// <returns>A normalized path or null if there is an error.</returns>
528  /// <remarks>Unlike <see cref="System.IO.Path" /> , this doesn't make a path absolute to the actual file system.</remarks>
529  public static StringBuilder Normalize(string pathToNormalize, out string error)
530  {
531  StringSpan drive;
532  StringSpan directoryOrFileName;
533  StringSpan fileName;
534  return Normalize(pathToNormalize, out drive, out directoryOrFileName, out fileName, out error);
535  }
536 
537  /// <summary>
538  /// Normalize a path by replacing '\' by '/' and transforming relative '..' or current path '.' to an absolute path. See remarks.
539  /// </summary>
540  /// <param name="pathToNormalize">The path automatic normalize.</param>
541  /// <param name="drive">The drive character region.</param>
542  /// <param name="directoryOrFileName">The directory.</param>
543  /// <param name="fileName">Name of the file.</param>
544  /// <param name="error">The error or null if no errors.</param>
545  /// <returns>A normalized path or null if there is an error.</returns>
546  /// <remarks>Unlike <see cref="System.IO.Path" /> , this doesn't make a path absolute to the actual file system.</remarks>
547  public unsafe static StringBuilder Normalize(string pathToNormalize, out StringSpan drive, out StringSpan directoryOrFileName, out StringSpan fileName, out string error)
548  {
549  drive = new StringSpan();
550  directoryOrFileName = new StringSpan();
551  fileName = new StringSpan();
552  error = null;
553  string path = pathToNormalize;
554  if (path == null)
555  {
556  return null;
557  }
558  int countDirectories = pathToNormalize.Count(pathItem => pathItem == DirectorySeparatorChar ||
559  pathItem == DirectorySeparatorCharAlt ||
560  pathItem == System.IO.Path.VolumeSeparatorChar);
561 
562  // Safeguard if count directories is going wild
563  if (countDirectories > 1024)
564  {
565  error = "Path contains too many directory '/' separator or ':'";
566  return null;
567  }
568 
569  // Optimize the code by using stack alloc in order to avoid allocation of a List<CharRegion>()
570  int currentPath = 0;
571  var paths = stackalloc StringSpan[countDirectories + 1];
572  var builder = new StringBuilder(pathToNormalize.Length);
573 
574  // Iterate on all chars on original path
575  foreach (var pathItem in pathToNormalize)
576  {
577  // Check if we have a directory separator
578  if (pathItem == DirectorySeparatorChar || pathItem == DirectorySeparatorCharAlt)
579  {
580  // Add only non consecutive '/'
581  if (builder.Length == 0 || builder[builder.Length - 1] != DirectorySeparatorChar)
582  {
583  // If '/' is the first char in the path, place the start at position 1 instead of 0
584  if (builder.Length == 0)
585  {
586  paths[0].Start = 1;
587  }
588 
589  // if '/' is closing a '..' or '.', then handle them here and go to next char
590  if (TrimParentAndSelfPath(builder, ref currentPath, paths, false))
591  {
592  continue;
593  }
594 
595  // If the root path is a drive, and we are trying to go its parent directory, return an error
596  if (IsInvalidBacktrackOnDrive(builder, currentPath, paths))
597  {
598  error = "Cannot go to parent directory '..' with a root drive";
599  return null;
600  }
601 
602  // Append the directory '/' separator
603  builder.Append(DirectorySeparatorChar);
604 
605  // Stack a new path entry
606  currentPath++;
607 
608  // Next entry start right after '/'
609  paths[currentPath].Start = builder.Length;
610  }
611  }
612  else if (pathItem == System.IO.Path.VolumeSeparatorChar)
613  {
614  // Check in case of volume separator ':'
615  if (IsDriveSpan(paths[0]))
616  {
617  error = "Path contains more than one drive ':' separator";
618  return null;
619  }
620 
621  if (currentPath > 0)
622  {
623  error = "Path cannot contain a drive ':' separator after a backslash";
624  return null;
625  }
626 
627  if (builder.Length == 0)
628  {
629  error = "Path cannot start with a drive ':' separator";
630  return null;
631  }
632 
633  // Append the volume ':' if no error
634  builder.Append(pathItem);
635 
636  // Update the path entry
637  paths[0].Length = -paths[0].Length; // Use of a negative length to identify a drive information
638 
639  // Next entry right after ':'
640  paths[1].Start = builder.Length;
641  currentPath = 1;
642  }
643  else if (!InvalidFileNameChars.Contains(pathItem))
644  {
645  // If no invalid character, we can add the current character
646  builder.Append(pathItem);
647  paths[currentPath].Length++;
648  }
649  else
650  {
651  // Else the character is invalid
652  error = "Invalid character [{0}] found in path [{1}]".ToFormat(pathItem, pathToNormalize);
653  return null;
654  }
655  }
656 
657  // Remove trailing '/'
658  RemoveTrailing(builder, DirectorySeparatorChar);
659 
660  // Remove trailing '..'
661  if (TrimParentAndSelfPath(builder, ref currentPath, paths, true))
662  {
663  // Remove trailing '/'
664  RemoveTrailing(builder, DirectorySeparatorChar);
665  }
666  else
667  {
668  // If the root path is a drive, and we are trying to go its parent directory, return an error
669  if (IsInvalidBacktrackOnDrive(builder, currentPath, paths))
670  {
671  error = "Cannot go to parent directory '..' with a root drive";
672  return null;
673  }
674  }
675 
676  // Go back to upper path if current is not vaid
677  if (currentPath > 0 && !paths[currentPath].IsValid)
678  {
679  currentPath--;
680  }
681 
682  // Copy the drive, directory, filename information to the output
683  int startDirectory = 0;
684  if (IsDriveSpan(paths[0]))
685  {
686  drive = paths[0];
687  // Make sure to revert to a conventional span (as we use the negative value to identify a drive)
688  drive.Length = -drive.Length + 1;
689  startDirectory = 1;
690  }
691 
692  // If there is any directory information, process it
693  if (startDirectory <= currentPath)
694  {
695  directoryOrFileName.Start = paths[startDirectory].Start == 1 ? 0 : paths[startDirectory].Start;
696  if (currentPath == startDirectory)
697  {
698  directoryOrFileName.Length = paths[startDirectory].Length == 0 && paths[startDirectory].Start == 1
699  ? 1
700  : paths[startDirectory].Length;
701  }
702  else
703  {
704  directoryOrFileName.Length = paths[currentPath - 1].Start + paths[currentPath - 1].Length - directoryOrFileName.Start;
705 
706  if (paths[currentPath].IsValid)
707  {
708  // In case last path is a parent '..' don't include it in fileName
709  if (IsParentPath(builder, paths[currentPath]))
710  {
711  directoryOrFileName.Length += paths[currentPath].Length + 1;
712  }
713  else
714  {
715  fileName.Start = paths[currentPath].Start;
716  fileName.Length = builder.Length - fileName.Start;
717  }
718  }
719  }
720  }
721 
722  return builder;
723  }
724 
725  private static void RemoveTrailing(StringBuilder builder, char charToRemove)
726  {
727  if (builder.Length > 1 && builder[builder.Length - 1] == charToRemove)
728  {
729  builder.Length = builder.Length - 1;
730  }
731  }
732 
733  private static bool IsParentPath(StringBuilder builder, StringSpan path)
734  {
735  return path.Length == 2 &&
736  builder[path.Start] == '.' &&
737  builder[path.Start + 1] == '.';
738  }
739 
740  private static bool IsRelativeCurrentPath(StringBuilder builder, StringSpan path)
741  {
742  return path.Length == 1 && builder[path.Start] == '.';
743  }
744 
745  private unsafe static bool IsInvalidBacktrackOnDrive(StringBuilder builder, int currentPath, StringSpan* paths)
746  {
747  // If the root path is a drive, and we are going to back slash, just don't
748  return currentPath > 0 && IsParentPath(builder, paths[currentPath]) && IsInvalidRelativeBacktrackOnDrive(currentPath, paths);
749  }
750 
751  private static bool IsDriveSpan(StringSpan stringSpan)
752  {
753  return stringSpan.Length < 0;
754  }
755 
756  private unsafe static bool IsInvalidRelativeBacktrackOnDrive(int currentPath, StringSpan* paths)
757  {
758  // If the root path is a drive, and we are going to back slash, just don't
759  return IsDriveSpan(paths[0]) && (currentPath == 1 || (currentPath == 2 && paths[1].Length == 0));
760  }
761 
762  /// <summary>
763  /// Trims the path by removing unecessary '..' and '.' path items.
764  /// </summary>
765  /// <param name="builder">The builder.</param>
766  /// <param name="currentPath">The current path.</param>
767  /// <param name="paths">The paths.</param>
768  /// <param name="isLastTrim">if set to <c>true</c> is last trim to occur.</param>
769  /// <returns><c>true</c> if trim has been done, <c>false</c> otherwise.</returns>
770  private unsafe static bool TrimParentAndSelfPath(StringBuilder builder, ref int currentPath, StringSpan* paths, bool isLastTrim)
771  {
772  var path = paths[currentPath];
773  if (currentPath > 0 && IsParentPath(builder, path))
774  {
775  // If the root path is a drive, and we are going to back slash, just don't
776  if (IsInvalidRelativeBacktrackOnDrive(currentPath, paths))
777  {
778  return false;
779  }
780 
781  // If previous path is already a relative path, then we probably can popup
782  var previousPath = paths[currentPath - 1];
783  if (IsParentPath(builder, previousPath))
784  {
785  return false;
786  }
787 
788  // We can popup the previous path
789  paths[currentPath] = new StringSpan();
790  currentPath--;
791  paths[currentPath].Length = 0;
792  builder.Length = paths[currentPath].Start;
793  return true;
794  }
795 
796  var isRelativeCurrentPath = IsRelativeCurrentPath(builder, path);
797  if (!(isLastTrim && currentPath == 0 && isRelativeCurrentPath) && isRelativeCurrentPath)
798  {
799  // We can popup the previous path
800  paths[currentPath].Length = 0;
801  builder.Length = paths[currentPath].Start;
802  return true;
803  }
804  return false;
805  }
806 
807  private static string Decode(string pathToNormalize, bool isPathDirectory, out StringSpan drive, out StringSpan directory, out StringSpan fileName, out StringSpan fileExtension)
808  {
809  drive = new StringSpan();
810  directory = new StringSpan();
811  fileName = new StringSpan();
812  fileExtension = new StringSpan();
813 
814  if (string.IsNullOrWhiteSpace(pathToNormalize))
815  {
816  return string.Empty;
817  }
818 
819  // Normalize path
820  // TODO handle network path/http/file path
821  string error;
822  var path = Normalize(pathToNormalize, out drive, out directory, out fileName, out error);
823  if (error != null)
824  {
825  throw new ArgumentException(error);
826  }
827 
828  if (isPathDirectory)
829  {
830  // If we are expecting a directory, merge the fileName with the directory
831  if (fileName.IsValid)
832  {
833  directory.Length = directory.Length + 1 + fileName.Length;
834  fileName = new StringSpan();
835  }
836  }
837  else
838  {
839  // In case this is only a directory name and we are expecting a filename, gets the directory name as a filename
840  if (directory.IsValid && !fileName.IsValid)
841  {
842  fileName = directory;
843  directory = new StringSpan();
844  }
845 
846  if (fileName.IsValid)
847  {
848  var extensionIndex = path.LastIndexOf('.', fileName.Start);
849  if (extensionIndex > 0)
850  {
851  fileName.Length = extensionIndex - fileName.Start;
852  fileExtension.Start = extensionIndex;
853  fileExtension.Length = path.Length - extensionIndex;
854  }
855  }
856  }
857 
858  return path.ToString();
859  }
860  }
861 }
_Use_decl_annotations_ bool IsValid(DXGI_FORMAT fmt)
Definition: DirectXTex.inl:25
static unsafe StringBuilder Normalize(string pathToNormalize, out StringSpan drive, out StringSpan directoryOrFileName, out StringSpan fileName, out string error)
Normalize a path by replacing '\' by '/' and transforming relative '..' or current path '...
Definition: UPath.cs:547
static StringBuilder Normalize(string pathToNormalize, out string error)
Normalize a path by replacing '\' by '/' and transforming relative '..' or current path '...
Definition: UPath.cs:529
static bool HasDirectoryChars(string path)
Determines whether the specified path contains some directory characeters '\' or '/' ...
Definition: UPath.cs:487
string ToWindowsPath()
Converts this path to a Windows path (/ replaced by )
Definition: UPath.cs:334
bool Equals(UPath other)
Definition: UPath.cs:288
UPath MakeRelative(UDirectory anchorDirectory)
Makes this instance relative to the specified anchor directory.
Definition: UPath.cs:391
static bool IsValid(string path)
Determines whether the specified path is a valid UPath
Definition: UPath.cs:497
Defines a normalized directory path. See UPath for details. This class cannot be inherited.
Definition: UDirectory.cs:13
UPathType
Describes if a UPath is relative or absolute.
Definition: UPathType.cs:8
bool HasDrive
Gets a value indicating whether this instance has a Drive != null.
Definition: UPath.cs:130
bool IsAbsolute
Determines whether this instance is absolute.
Definition: UPath.cs:269
override bool Equals(object obj)
Definition: UPath.cs:295
UDirectory GetFullDirectory()
Gets the full directory with GetDrive() + GetDirectory() or empty directory.
Definition: UPath.cs:234
Base class that describes a uniform path and provides method to manipulate them. Concrete class are U...
Definition: UPath.cs:21
UDirectory GetParent()
Gets the parent directory of this instance. For a file, this is the directory directly containing the...
Definition: UPath.cs:197
string GetDirectory()
Gets the directory. Can be null.
Definition: UPath.cs:187
UPath(string fullPath, StringSpan driveSpan, StringSpan directorySpan)
Definition: UPath.cs:105
bool IsDirectoryOnly
Gets a value indicating whether this instance is a directory only path.
Definition: UPath.cs:164
static string Normalize(string pathToNormalize)
Normalize a path by replacing '\' by '/' and transforming relative '..' or current path '...
Definition: UPath.cs:511
override string ToString()
Definition: UPath.cs:325
int CompareTo(object obj)
Definition: UPath.cs:312
override int GetHashCode()
Definition: UPath.cs:302
Defines a normalized file path. See UPath for details. This class cannot be inherited.
Definition: UFile.cs:13
A region of character in a string.
Definition: StringSpan.cs:12
string GetDrive()
Gets the drive (contains the ':' if any), can be null.
Definition: UPath.cs:142