/* Copyright (c) Citrix Systems, Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms,
* with or without modification, are permitted provided
* that the following conditions are met:
*
* * Redistributions of source code must retain the above
* copyright notice, this list of conditions and the
* following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the
* following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using ICSharpCode.SharpZipLib.Tar;
namespace XenCenterLib.Archive
{
public static class TarSanitization
{
///
/// See https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
///
private static readonly string[] ReservedNames = { "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" };
private static readonly char[] ForbiddenChars = { '<', '>', ':', '"', '/', '\\', '|', '?', '*' };
///
/// SharpZipLib doesn't account for illegal file names or characters in
/// file names (e.g. ':' in Windows), so first we stream the tar to a
/// new tar, sanitizing any of the contained bad file names as we go.
/// We also need to record the modification times of all the files, so
/// that we can restore them into the final zip.
///
public static void SanitizeTarForWindows(string inputTar, string outputTar, Action cancellingDelegate)
{
using (var fsIn = File.OpenRead(inputTar))
using (var inputStream = new TarInputStream(fsIn))
using (var fsOut = File.OpenWrite(outputTar))
using (var outputStream = new TarOutputStream(fsOut))
{
TarEntry entry;
byte[] buf = new byte[8 * 1024];
var usedNames = new Dictionary();
while ((entry = inputStream.GetNextEntry()) != null)
{
cancellingDelegate?.Invoke();
entry.Name = SanitizeTarName(entry.Name, usedNames);
outputStream.PutNextEntry(entry);
long bytesToCopy = entry.Size;
while (bytesToCopy > 0)
{
int bytesRead = inputStream.Read(buf, 0, Math.Min(bytesToCopy > int.MaxValue ? int.MaxValue : (int)bytesToCopy, buf.Length));
outputStream.Write(buf, 0, bytesRead);
bytesToCopy -= bytesRead;
}
outputStream.CloseEntry();
}
}
}
public static string SanitizeTarPathMember(string member)
{
// Strip any leading/trailing whitespace, or Windows will do it for us, and we might generate non-unique names
member = member.Trim();
// Don't allow empty filename
if (member.Length == 0)
return "_";
foreach (string reserved in ReservedNames)
{
if (member.ToUpperInvariant() == reserved.ToUpperInvariant()
|| member.ToUpperInvariant().StartsWith(reserved.ToUpperInvariant() + "."))
{
member = "_" + member;
}
}
// Allow only 31 < c < 127, excluding any of the forbidden characters
var sb = new StringBuilder(member.Length);
foreach (char c in member)
{
if (c <= 31 || c >= 127 || ForbiddenChars.Contains(c))
sb.Append("_");
else
sb.Append(c);
}
member = sb.ToString();
// Windows also strips trailing '.' potentially generating non-unique names
if (member.EndsWith("."))
member = member.Substring(0, member.Length - 1) + "_";
return member;
}
///
/// Maps file/directory names that are illegal under Windows to 'sanitized' versions. The usedNames
/// parameter ensures this is done consistently within a directory tree.
///
/// The dictionary is used by SanitizeTarName() to ensure names are consistently sanitized. e.g.:
/// dir1: -> dir1_
/// dir1? -> dir1_ (1)
/// dir1_ -> dir1_ (2)
/// dir1:/file -> dir1_/file
/// dir1?/file -> dir1_ (1)/file
///
/// Pass the same dictionary to each invocation to get unique outputs within the same tree.
///
private static string SanitizeTarName(string path, Dictionary usedNames)
{
string sanitizedPath = "";
Stack bitsToEscape = new Stack();
// Trim any trailing slashes (usually indicates path is a directory)
path = path.TrimEnd('/');
// Take members off the end of the path until we have a name that already is
// a key in our dictionary, or until we have the empty string.
while (!usedNames.ContainsKey(path) && path.Length > 0)
{
string[] bits = path.Split('/');
string lastBit = bits[bits.Length - 1];
int lengthOfLastBit = lastBit.Length;
bitsToEscape.Push(lastBit);
path = path.Substring(0, path.Length - lengthOfLastBit);
path = path.TrimEnd('/');
}
if (usedNames.ContainsKey(path))
{
sanitizedPath = usedNames[path];
}
// Now for each member in the path, look up the escaping of that member if it exists; otherwise
// generate a new, unique escaping. Then append the escaped member to the end of the sanitized
// path and continue.
foreach (string member in bitsToEscape)
{
System.Diagnostics.Trace.Assert(member.Length > 0);
string sanitizedMember = SanitizeTarPathMember(member);
sanitizedPath = Path.Combine(sanitizedPath, sanitizedMember);
path = path + Path.DirectorySeparatorChar + member;
// Note: even if sanitizedMember == member, we must add it to the dictionary, since
// tar permits names that differ only in case, while Windows does not. We must e.g.:
// abc -> abc
// aBC -> aBC (1)
if (usedNames.ContainsKey(path))
{
// We have already generated an escaping for this path prefix: use it
sanitizedPath = usedNames[path];
continue;
}
// Generate the unique mapping
string pre = sanitizedPath;
int i = 1;
while (usedNames.Values.Any(v => v.ToUpperInvariant() == sanitizedPath.ToUpperInvariant()))
{
sanitizedPath = $"{pre} ({i})";
i++;
}
usedNames.Add(path, sanitizedPath);
}
return sanitizedPath;
}
}
}