//-----------------------------------------------------------------------
//
// Copyright (c) Outercurve Foundation. All rights reserved.
//
//-----------------------------------------------------------------------
namespace DotNetOpenAuth.BuildTasks {
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Text;
internal class PathSegment {
private const float ParentChildResizeThreshold = 0.30f;
private readonly string originalName;
private string currentName;
private bool minimized;
private static readonly string[] ReservedFileNames = "CON PRN AUX CLOCK$ NUL COM0 COM1 COM2 COM3 COM4 COM5 COM6 COM7 COM8 COM9 LPT0 LPT1 LPT2 LPT3 LPT4 LPT5 LPT6 LPT7 LPT8 LPT9".Split(' ');
internal PathSegment() {
this.currentName = string.Empty;
this.originalName = string.Empty;
this.minimized = true;
this.Children = new Collection();
}
private PathSegment(string originalName, PathSegment parent)
: this() {
Contract.Requires(!string.IsNullOrEmpty(originalName));
Contract.Requires(parent != null);
this.currentName = this.originalName = originalName;
this.Parent = parent;
this.minimized = false;
}
internal PathSegment Parent { get; private set; }
internal string OriginalPath {
get {
if (this.Parent != null) {
return Path.Combine(this.Parent.OriginalPath, this.originalName);
} else {
return this.originalName;
}
}
}
internal string CurrentPath {
get {
if (this.Parent != null) {
return Path.Combine(this.Parent.CurrentPath, this.currentName);
} else {
return this.currentName;
}
}
}
internal string CurrentName {
get { return this.currentName; }
}
internal string OriginalName {
get { return this.originalName; }
}
private int SegmentCount {
get {
int parents = this.Parent != null ? this.Parent.SegmentCount : 0;
return parents + 1;
}
}
internal int FullLength {
get {
if (this.Parent != null) {
int parentLength = this.Parent.FullLength;
if (parentLength > 0) {
parentLength++; // allow for an in between slash
}
return parentLength + this.currentName.Length;
} else {
return this.currentName.Length;
}
}
}
internal bool NameChanged {
get { return !string.Equals(this.currentName, this.originalName, StringComparison.OrdinalIgnoreCase); }
}
internal bool IsLeaf {
get { return this.Children.Count == 0; }
}
internal IEnumerable Descendents {
get {
IEnumerable result = this.Children;
foreach (PathSegment child in this.Children) {
result = result.Concat(child.Descendents);
}
return result;
}
}
internal IEnumerable Ancestors {
get {
PathSegment parent = this.Parent;
while (parent != null) {
yield return parent;
parent = parent.Parent;
}
}
}
internal IEnumerable SelfAndDescendents {
get {
yield return this;
foreach (var child in this.Descendents) {
yield return child;
}
}
}
internal IEnumerable SelfAndAncestors {
get {
yield return this;
foreach (var parent in this.Ancestors) {
yield return parent;
}
}
}
internal IEnumerable LeafChildren {
get { return this.Children.Where(child => child.IsLeaf); }
}
internal IEnumerable LeafDescendents {
get { return this.Descendents.Where(child => child.IsLeaf); }
}
internal IEnumerable Siblings {
get { return this.Parent != null ? this.Parent.Children : Enumerable.Empty(); }
}
internal Collection Children { get; private set; }
public override string ToString() {
string path;
if (this.NameChanged) {
path = "{" + this.originalName + " => " + this.currentName + "}";
} else {
path = this.currentName;
}
if (path.Length > 0 && !this.IsLeaf) {
path += "\\";
}
if (this.Parent != null) {
path = Parent.ToString() + path;
}
return path;
}
internal PathSegment Add(string originalPath) {
Contract.Requires(!string.IsNullOrEmpty(originalPath));
Contract.Ensures(Contract.Result() != null);
string[] segments = originalPath.Split(Path.DirectorySeparatorChar);
return this.Add(segments, 0);
}
internal void Add(IEnumerable originalPaths) {
foreach (string path in originalPaths) {
this.Add(path);
}
}
internal int EnsureSelfAndChildrenNoLongerThan(int maxLength) {
Contract.Requires(maxLength > 0, "A path can only have a positive length.");
Contract.Requires(this.Parent == null || maxLength > this.Parent.FullLength + 1, "A child path cannot possibly be made shorter than its parent.");
Contract.Ensures(Contract.Result() <= maxLength);
const int uniqueBase = 16;
// Find the items that are too long, and always work on the longest one
var longPaths = this.SelfAndDescendents.Where(path => path.FullLength > maxLength).OrderByDescending(path => path.FullLength);
PathSegment longPath;
while ((longPath = longPaths.FirstOrDefault()) != null) {
// Keep working on this one until it's short enough.
do {
int tooLongBy = longPath.FullLength - maxLength;
var longSegments = longPath.SelfAndAncestors.Where(segment => !segment.minimized).OrderByDescending(segment => segment.CurrentName.Length);
PathSegment longestSegment = longSegments.FirstOrDefault();
if (longestSegment == null) {
throw new InvalidOperationException("Unable to shrink path length sufficiently.");
}
PathSegment secondLongestSegment = longSegments.Skip(1).FirstOrDefault();
int shortenByUpTo;
if (secondLongestSegment != null) {
shortenByUpTo = Math.Min(tooLongBy, Math.Max(1, longestSegment.CurrentName.Length - secondLongestSegment.CurrentName.Length));
} else {
shortenByUpTo = tooLongBy;
}
int minimumGuaranteedUniqueLength = Math.Max(1, RoundUp(Math.Log(longestSegment.Siblings.Count(), uniqueBase)));
int allowableSegmentLength = Math.Max(minimumGuaranteedUniqueLength, longestSegment.CurrentName.Length - shortenByUpTo);
if (allowableSegmentLength >= longestSegment.CurrentName.Length) {
// We can't make this segment any shorter.
longestSegment.minimized = true;
}
longestSegment.currentName = longestSegment.CreateUniqueShortFileName(longestSegment.CurrentName, allowableSegmentLength);
} while (longPath.FullLength > maxLength);
}
// Return the total length of self or longest child.
return this.SelfAndDescendents.Max(c => c.FullLength);
}
internal PathSegment FindByOriginalPath(string originalPath) {
Contract.Requires(!string.IsNullOrEmpty(originalPath));
string[] segments = originalPath.Split(Path.DirectorySeparatorChar);
return this.FindByOriginalPath(segments, 0);
}
private string GetUniqueShortName(string preferredPrefix, string preferredSuffix, int allowableLength) {
Contract.Requires(preferredPrefix != null);
Contract.Requires(preferredSuffix != null);
Contract.Requires(allowableLength > 0);
Contract.Ensures(!string.IsNullOrEmpty(Contract.Result()));
Contract.Ensures(Contract.Result().Length <= allowableLength);
string candidateName = string.Empty;
int i;
for (i = -1; candidateName.Length == 0 || ReservedFileNames.Contains(candidateName, StringComparer.OrdinalIgnoreCase) || this.Siblings.Any(child => string.Equals(child.CurrentName, candidateName, StringComparison.OrdinalIgnoreCase)); i++) {
string unique = i < 0 ? string.Empty : i.ToString("x");
if (allowableLength < unique.Length) {
throw new InvalidOperationException("Unable to shorten path sufficiently to fit constraints.");
}
candidateName = unique;
// Suffix gets higher priority than the prefix, but only if the entire suffix can be appended.
if (candidateName.Length + preferredSuffix.Length <= allowableLength) {
candidateName += preferredSuffix;
}
// Now prepend as much of the prefix as fits.
candidateName = preferredPrefix.Substring(0, Math.Min(allowableLength - candidateName.Length, preferredPrefix.Length)) + candidateName;
}
return candidateName;
}
private static int RoundUp(double value) {
int roundedValue = (int)value;
if (roundedValue < value) {
roundedValue++;
}
return roundedValue;
}
private string CreateUniqueShortFileName(string fileName, int targetLength) {
Contract.Requires(!string.IsNullOrEmpty(fileName));
Contract.Ensures(!string.IsNullOrEmpty(Contract.Result()));
Contract.Ensures(Contract.Result().Length <= targetLength);
// The filename may already full within the target length.
if (fileName.Length <= targetLength) {
return fileName;
}
string preferredPrefix = Path.GetFileNameWithoutExtension(fileName);
string preferredSuffix = Path.GetExtension(fileName);
string shortenedFileName = GetUniqueShortName(preferredPrefix, preferredSuffix, targetLength);
return shortenedFileName;
}
private void ShortenThis(int targetLength) {
this.currentName = CreateUniqueShortFileName(this.originalName, targetLength);
}
private PathSegment Add(string[] segments, int segmentIndex) {
Contract.Requires(segments != null);
Contract.Requires(segmentIndex < segments.Length);
Contract.Ensures(Contract.Result() != null);
var match = this.Children.SingleOrDefault(child => String.Equals(child.originalName, segments[segmentIndex]));
if (match == null) {
match = new PathSegment(segments[segmentIndex], this);
this.Children.Add(match);
}
return segments.Length == segmentIndex + 1 ? match : match.Add(segments, segmentIndex + 1);
}
private PathSegment FindByOriginalPath(string[] segments, int segmentIndex) {
Contract.Requires(segments != null);
Contract.Requires(segmentIndex < segments.Length);
if (string.Equals(this.originalName, segments[segmentIndex], StringComparison.OrdinalIgnoreCase)) {
if (segmentIndex == segments.Length - 1) {
return this;
}
foreach (var child in this.Children) {
var match = child.FindByOriginalPath(segments, segmentIndex + 1);
if (match != null) {
return match;
}
}
}
return null;
}
}
}