Files
UnrealEngine/Engine/Source/Programs/IOS/iPhonePackager/FileOperations.cs
2025-05-18 13:04:45 +08:00

1049 lines
28 KiB
C#

/**
* Copyright Epic Games, Inc. All Rights Reserved.
*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using Ionic.Zip;
using Ionic.Zlib;
using System.Net;
using System.Net.NetworkInformation;
using System.Reflection;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Ipc;
using System.Runtime.Remoting.Channels.Tcp;
using System.Runtime.Remoting.Lifetime;
using System.Net.Sockets;
using DotNETUtilities;
namespace iPhonePackager
{
/**
* Stream proxy class which can read from a seekable stream (eg a file stream)
* and prevents the original stream (file) from being closed.
*/
public class ProxyReadStream : Stream
{
public long InternalPosition;
public Stream ParentStream;
public ProxyReadStream(Stream InParentStream)
{
ParentStream = InParentStream;
InternalPosition = 0;
if (!ParentStream.CanSeek || !ParentStream.CanRead)
{
throw new NotSupportedException();
}
}
public override bool CanRead
{
get { return true; }
}
public override bool CanSeek
{
get { return true; }
}
public override bool CanWrite
{
get { return false; }
}
public override long Length
{
get { return ParentStream.Length; }
}
public override long Position
{
get { return InternalPosition; }
set { InternalPosition = value; }
}
public override long Seek(long offset, SeekOrigin origin)
{
switch (origin)
{
case SeekOrigin.Begin:
InternalPosition = offset;
break;
case SeekOrigin.Current:
InternalPosition += offset;
break;
case SeekOrigin.End:
InternalPosition = Length + offset;
break;
}
return Position;
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void Flush()
{
ParentStream.Flush();
}
public override int Read(byte[] buffer, int offset, int count)
{
ParentStream.Position = InternalPosition;
int Result = ParentStream.Read(buffer, offset, count);
InternalPosition += Result;
return Result;
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
protected override void Dispose(bool disposing)
{
// Do not dispose parent
}
public override void Close()
{
// Do not close parent
}
}
public partial class FileOperations
{
private const int DefaultRetryCount = 5;
private static Socket RPCSocket;
/** Cranks up the remoting utility if needed */
static private bool ConditionalInitRemoting(string MacName)
{
if (RPCCommandHelper.PingRemoteHost(MacName))
{
try
{
RPCSocket = RPCCommandHelper.ConnectToUnrealRemoteTool(MacName);
}
catch (Exception)
{
Console.WriteLine("Failed to connect to UnrealRemoteTool running on {0}", MacName);
return false;
}
}
else
{
return false;
}
return true;
}
/// <summary>
/// Find a prefixable file (checks first with the prefix and then without it)
/// </summary>
/// <param name="BasePath">The path that the file should be in</param>
/// <param name="FileName">The filename to check for (with and without SigningPrefix prepended to it)</param>
/// <returns>The path to the most desirable file (may still not exist)</returns>
static public string FindPrefixedFile(string BasePath, string FileName)
{
return FindPrefixedFileOrDirectory(BasePath, FileName, false);
}
/// <summary>
/// Finds the first file in the directory with a given extension (if it exists). This method is to provide some
/// idiot-proofing (for example, .mobileprovision files, which will be expected to be saved from the website as
/// GameName.mobileprovision or Distro_GameName.mobileprovision, but falling back to using the first one we find
/// if the correctly named one is not found helps the user if they mess up during the process.
/// </summary>
static public string FindAnyFileWithExtension(string BasePath, string FileExt)
{
string[] FileList = Directory.GetFiles(BasePath);
foreach (string Filename in FileList)
{
if (Path.GetExtension(Filename).Equals(FileExt, StringComparison.InvariantCultureIgnoreCase))
{
return Filename;
}
}
return null;
}
/// <summary>
/// Find a prefixable file or directory (checks first with the prefix and then without it)
/// </summary>
/// <param name="BasePath">The path that the file should be in</param>
/// <param name="Name">The name to check for (with and without SigningPrefix prepended to it)</param>
/// <param name="bIsDirectory">Is the desired name a directory?</param>
///
/// <returns>The path to the most desirable file (may still not exist)</returns>
static public string FindPrefixedFileOrDirectory(string BasePath, string Name, bool bIsDirectory)
{
string PrefixedPath = Path.Combine(BasePath, Config.SigningPrefix + Name);
if ((bIsDirectory && Directory.Exists(PrefixedPath)) || (!bIsDirectory && File.Exists(PrefixedPath)))
{
return PrefixedPath;
}
else
{
return Path.Combine(BasePath, Name);
}
}
/**
* Safely delete a directory
*/
static public void DeleteDirectory( DirectoryInfo DirInfo )
{
if( DirInfo.Exists )
{
foreach( FileInfo Info in DirInfo.GetFiles() )
{
try
{
Info.IsReadOnly = false;
Info.Delete();
}
catch( Exception Ex )
{
Program.Error( "Could not delete file: " + Info.FullName );
Program.Error( " Exception: " + Ex.Message );
Program.ReturnCode = (int)ErrorCodes.Error_DeleteFile;
}
}
foreach( DirectoryInfo SubDirInfo in DirInfo.GetDirectories() )
{
DeleteDirectory( SubDirInfo );
}
try
{
DirInfo.Delete();
}
catch( Exception Ex )
{
Program.Error( "Could not delete folder: " + DirInfo.FullName );
Program.Error( " Exception: " + Ex.Message );
Program.ReturnCode = (int)ErrorCodes.Error_DeleteDirectory;
}
}
}
/**
* Safely delete a file
*/
static public void DeleteFile( string FileName )
{
FileInfo Info = new FileInfo( FileName );
if( Info.Exists )
{
try
{
Info.IsReadOnly = false;
Info.Delete();
}
catch( Exception Ex )
{
Program.Error( "Could not delete file: " + Info.FullName );
Program.Error( " Exception: " + Ex.Message );
Program.ReturnCode = (int)ErrorCodes.Error_DeleteFile;
}
}
}
/**
* Safely copy a folder structure
*/
static private void RecursiveFolderCopy( DirectoryInfo SourceFolderInfo, DirectoryInfo DestFolderInfo, bool bLowercase )
{
foreach( FileInfo SourceFileInfo in SourceFolderInfo.GetFiles() )
{
string DestFileName = Path.Combine(DestFolderInfo.FullName, bLowercase ? SourceFileInfo.Name.ToLower() : SourceFileInfo.Name);
CopyFile(SourceFileInfo, DestFileName, true, DefaultRetryCount);
}
foreach( DirectoryInfo SourceSubFolderInfo in SourceFolderInfo.GetDirectories() )
{
string DestFolderName = Path.Combine(DestFolderInfo.FullName, bLowercase ? SourceSubFolderInfo.Name.ToLower() : SourceSubFolderInfo.Name);
try
{
Directory.CreateDirectory( DestFolderName );
}
catch( Exception Ex )
{
Program.Error( "Could not create folder: " + DestFolderName );
Program.Error( " Exception: " + Ex.Message );
Program.ReturnCode = (int)ErrorCodes.Error_CreateDirectory;
}
RecursiveFolderCopy( SourceSubFolderInfo, new DirectoryInfo( DestFolderName ), bLowercase );
}
}
static public bool CopyFolder(string SourceFolder, string DestFolder, string DisplayDestFolder, bool bDeleteTarget, bool bLowercase)
{
if (DisplayDestFolder == null)
{
DisplayDestFolder = DestFolder;
}
DirectoryInfo SourceFolderInfo = new DirectoryInfo(SourceFolder);
if (!SourceFolderInfo.Exists)
{
Program.Error("Source folder does not exist");
return (false);
}
Program.Log(" ... '" + SourceFolder + "' -> '" + DisplayDestFolder + "'");
DirectoryInfo DestFolderInfo = new DirectoryInfo(DestFolder);
if (bDeleteTarget && DestFolderInfo.Exists)
{
DeleteDirectory(DestFolderInfo);
}
try
{
Directory.CreateDirectory(DestFolderInfo.FullName);
}
catch( Exception Ex )
{
Program.Error( "Could not create folder: " + DestFolderInfo.FullName );
Program.Error( " Exception: " + Ex.Message );
Program.ReturnCode = (int)ErrorCodes.Error_CreateDirectory;
}
RecursiveFolderCopy(SourceFolderInfo, DestFolderInfo, bLowercase);
return (true);
}
static public void RecursiveBatchUploadFolder(DirectoryInfo SourceFolderInfo, string DestFolder, List<string> FilesToUpload)
{
// add the files to the list of files to upload
foreach (FileInfo SourceFileInfo in SourceFolderInfo.GetFiles())
{
string DestFileName = DestFolder + "/" + SourceFileInfo.Name;
FilesToUpload.Add(SourceFileInfo.FullName + ";" + DestFileName);
}
// recurse into subdirectories
foreach (DirectoryInfo SourceSubFolderInfo in SourceFolderInfo.GetDirectories())
{
string DestFolderName = DestFolder + "/" + SourceSubFolderInfo.Name;
RecursiveBatchUploadFolder(SourceSubFolderInfo, DestFolderName, FilesToUpload);
}
}
static public bool BatchUploadFolder(string MacName, string SourceFolder, string DestFolder, bool bDeleteTarget)
{
if (Config.bUseRPCUtil)
{
if (!ConditionalInitRemoting(MacName))
{
return false;
}
if (bDeleteTarget)
{
// tell Mac to delete target if requested
RPCCommandHelper.RemoveDirectory(RPCSocket, DestFolder);
}
}
Program.Log(" ... '" + SourceFolder + "' -> '" + DestFolder + "'");
DirectoryInfo SourceFolderInfo = new DirectoryInfo(SourceFolder);
if (!SourceFolderInfo.Exists)
{
Program.Error("Source folder does not exist");
return false;
}
// gather the files to batch upload
List<string> FilesToUpload = new List<string>();
RecursiveBatchUploadFolder(SourceFolderInfo, DestFolder, FilesToUpload);
if (Config.bUseRPCUtil)
{
// send them off!
RPCCommandHelper.RPCBatchUpload(RPCSocket, FilesToUpload.ToArray());
}
else
{
SSHCommandHelper.BatchUpload(MacName, FilesToUpload.ToArray());
}
return true;
}
static public bool DownloadFile(string MacName, string SourceFilename, string DestFilename)
{
Hashtable Results = null;
if (Config.bUseRPCUtil)
{
if (!ConditionalInitRemoting(MacName))
{
return false;
}
Results = RPCCommandHelper.RPCDownload(RPCSocket, SourceFilename, DestFilename);
}
else
{
Results = SSHCommandHelper.DownloadFile(MacName, SourceFilename, DestFilename);
}
// success if exitcode is 0 or unspecified
if (Results["ExitCode"] != null)
{
return (Int64)Results["ExitCode"] == 0;
}
return true;
}
/**
* Copy a single file (deleting the existing file at the destination if it exists)
*/
static public void CopyRequiredFile(string SourcePath, string DestPath)
{
Program.Log(" ... '" + SourcePath + "' -> '" + DestPath + "'");
CopyFile(new FileInfo(SourcePath), DestPath, true, DefaultRetryCount);
}
/**
* Copy a single file (deleting the existing file at the destination if it exists)
*/
static public void CopyNonEssentialFile(string SourcePath, string DestPath)
{
CopyFile(new FileInfo(SourcePath), DestPath, false, DefaultRetryCount);
}
/**
* Copy a single file (deleting the existing file at the destination if it exists)
*/
static public bool CopyFile(FileInfo SourceFileInfo, string DestFileName, bool bCopyMustSucceed, int MaxRetryCount)
{
int CopyTryCount = 0;
bool bFileCopiedSuccessfully = false;
// if the file doesn't exist, but isn't needed, then just early out
if (!bCopyMustSucceed && !SourceFileInfo.Exists)
{
return false;
}
if (Config.bVerbose)
{
Program.Log(String.Format(" Copy: {0} -> {1}, last modified at {2}", SourceFileInfo.FullName, DestFileName, SourceFileInfo.LastWriteTime));
}
do
{
// If this isn't the first time through, sleep a little before trying again
if (CopyTryCount > 0)
{
Thread.Sleep(1000);
}
CopyTryCount++;
try
{
// Delete the destination file if it exists
FileInfo DestInfo = new FileInfo(DestFileName);
if (DestInfo.Exists)
{
DestInfo.IsReadOnly = false;
DestInfo.Delete();
}
Directory.CreateDirectory(Path.GetDirectoryName(DestFileName));
// Copy over the new file
SourceFileInfo.CopyTo(DestFileName);
// Make sure the destination file is writable
DestInfo = new FileInfo(DestFileName);
DestInfo.IsReadOnly = false;
DestInfo.LastWriteTime = SourceFileInfo.LastWriteTime;
// Sanity check
if (DestInfo.Length != SourceFileInfo.Length)
{
throw new Exception("Error: File copy failed (source and destination are different sizes)");
}
// Success!
bFileCopiedSuccessfully = true;
}
catch( Exception Ex )
{
Program.Warning("Failed to copy file to '" + DestFileName + "'");
Program.Warning(" Exception: " + Ex.Message + " (going to retry copy)");
}
}
while (!bFileCopiedSuccessfully && (CopyTryCount < MaxRetryCount));
if (!bFileCopiedSuccessfully)
{
string Msg = "Failed to copy file to " + DestFileName + " and there are no more retries!";
if (bCopyMustSucceed)
{
Program.Error(Msg);
Program.ReturnCode = (int)ErrorCodes.Error_CopyFile;
}
else
{
Program.Warning(Msg);
}
}
return bFileCopiedSuccessfully;
}
/**
* Copy a set of files
*/
static public bool CopyFiles( string SourceFolder, string DestFolder, string DisplayDestFolder, string FileSpec, string ExcludeExtension )
{
if( DisplayDestFolder == null )
{
DisplayDestFolder = DestFolder;
}
Program.Log( " ... '" + SourceFolder + "\\" + FileSpec + "' -> '" + DisplayDestFolder + "'" );
// Ensure the source folder exists - fail otherwise
DirectoryInfo SourceFolderInfo = new DirectoryInfo( SourceFolder );
if( !SourceFolderInfo.Exists )
{
Program.Warning( "Source folder does not exist: " + SourceFolderInfo );
return( false );
}
// Ensure the destination folder exists - create otherwise
DirectoryInfo DestFolderInfo = new DirectoryInfo( DestFolder );
if( !DestFolderInfo.Exists )
{
Directory.CreateDirectory( DestFolder );
}
foreach( FileInfo SourceInfo in SourceFolderInfo.GetFiles( FileSpec ) )
{
// Don't copy any file with the excluded extension
if( ExcludeExtension != null && SourceInfo.Extension == ExcludeExtension )
{
continue;
}
string DestPathName = Path.Combine( DestFolderInfo.FullName, SourceInfo.Name );
CopyFile(SourceInfo, DestPathName, false, DefaultRetryCount);
}
return( true );
}
/**
* Rename a file
*/
static public bool RenameFile( string Folder, string SourceName, string DestName )
{
Program.Log( " ... '" + Folder + "\\" + SourceName + "' -> '" + DestName + "'" );
// Ensure the source file exists - fail otherwise
string FullSourceName = Path.Combine( Folder, SourceName );
FileInfo SourceInfo = new FileInfo( FullSourceName );
if( !SourceInfo.Exists )
{
Program.Error( "File does not exist for renaming: " + FullSourceName );
return ( false );
}
// Delete the destination file if it exists
string FullDestName = Path.Combine( Folder, DestName );
FileInfo DestInfo = new FileInfo( FullDestName );
if( DestInfo.Exists && (FullSourceName != FullDestName))
{
DestInfo.IsReadOnly = false;
DestInfo.Delete();
}
// Rename
SourceInfo.MoveTo( DestInfo.FullName );
return ( true );
}
/// <summary>
/// This class exposes an interface that represents a limited subset of file system functionality, so the same
/// code can work with a Zip file or a raw .app directory
/// </summary>
public abstract class FileSystemAdapter
{
public abstract byte[] ReadAllBytes(string RelativeFilename);
public abstract Stream OpenRead(string RelativeFilename);
public abstract void CloseFile(Stream InStream);
public abstract void WriteAllBytes(string RelativeFilename, byte[] Data);
public abstract void WriteStream(string RelativeFilename, Stream Data);
public abstract void Close();
public abstract IEnumerable<string> GetAllPayloadFiles();
}
/// <summary>
/// Implementation of the FileSystemAdapter interface that handles the native file system
/// </summary>
public class RegularFileSystem : FileSystemAdapter
{
protected string MyAppDirectory;
protected string GetNativePath(string RelativeFilename)
{
return Path.Combine(MyAppDirectory, RelativeFilename.Replace("/", "\\"));
}
protected string GetZipStyleRelativePath(string NativeFilename)
{
if (!NativeFilename.StartsWith(MyAppDirectory))
{
throw new InvalidDataException("Path is not rooted in app directory");
}
string RelativePath = NativeFilename.Substring(MyAppDirectory.Length);
return RelativePath.Replace("\\", "/");
}
public RegularFileSystem(string AppDirectory)
{
MyAppDirectory = AppDirectory;
}
public override byte[] ReadAllBytes(string RelativeFilename)
{
return File.ReadAllBytes(GetNativePath(RelativeFilename));
}
public override Stream OpenRead(string RelativeFilename)
{
return File.OpenRead(GetNativePath(RelativeFilename));
}
public override void CloseFile(Stream InStream)
{
}
public override void WriteAllBytes(string RelativeFilename, byte[] Data)
{
File.WriteAllBytes(GetNativePath(RelativeFilename), Data);
}
public override void WriteStream(string RelativeFilename, Stream Data)
{
Stream FileStream = File.OpenWrite(GetNativePath(RelativeFilename));
Data.CopyTo(FileStream);
FileStream.Close();
}
public override void Close()
{
}
public override IEnumerable<string> GetAllPayloadFiles()
{
string[] SourceList = Directory.GetFiles(MyAppDirectory, "*.*", SearchOption.AllDirectories);
List<string> Result = new List<string>();
foreach (string Filename in SourceList)
{
Result.Add(GetZipStyleRelativePath(Filename));
}
return Result;
}
}
/// <summary>
/// Implementation of the FileSystemAdapter interface that handles Zip files
/// </summary>
public class ZipFileSystem : FileSystemAdapter
{
// Zip file
protected ZipFile Zip;
protected string InternalRootPath;
// Pending writes, in case someone tries to read a file they've previously modified without saving the Zip.
protected Dictionary<string, Stream> PendingWrites = new Dictionary<string, Stream>();
/// Name of executable file name, so we can set correct attributes when zipping
public String ExecutableFileName;
/// <summary>
/// Finds the root path in a given IPA (e.g., "Payload/UnrealGame.app/")
/// </summary>
public static string FindRootPathInIPA(ZipFile Zip)
{
// we want only the root Info.plist, not a plist inside an framework or extension
string Info = ".app/Info.plist";
var FileList = Zip.EntryFileNames;
foreach (string TestFilename in FileList)
{
if (TestFilename.EndsWith(Info))
{
// the - 5 removes the .app portion and leaves the slash
return TestFilename.Substring(0, TestFilename.Length - (Info.Length - 5));
}
}
return null;
}
public ZipFileSystem(string Filename)
{
Zip = ZipFile.Read(Filename);
InternalRootPath = FindRootPathInIPA(Zip);
}
public ZipFileSystem(ZipFile InZip)
{
this.Zip = InZip;
InternalRootPath = FindRootPathInIPA(InZip);
}
public ZipFileSystem(ZipFile InZip, string RootPath)
{
this.Zip = InZip;
InternalRootPath = RootPath;
}
protected string GetFileName(string RelativeFilename)
{
int SplitPoint = RelativeFilename.LastIndexOf("/") + 0;
return RelativeFilename.Substring(SplitPoint);
}
protected static void SplitPath(string RelativeFilename, out string ZipPath, out string ZipFilename)
{
int SplitPoint = RelativeFilename.LastIndexOf("/") + 0;
ZipPath = RelativeFilename.Substring(0, SplitPoint);
ZipFilename = RelativeFilename.Substring(SplitPoint);
}
public override byte[] ReadAllBytes(string RelativeFilename)
{
Stream Source = OpenRead(RelativeFilename);
byte[] Result = new byte[Source.Length];
Source.Read(Result, 0, (int)Source.Length);
Source.Close();
return Result;
}
public override Stream OpenRead(string RelativeFilename)
{
string FullPath = InternalRootPath + RelativeFilename;
Stream ReadStream;
Stream PreviousStream;
if (PendingWrites.TryGetValue(FullPath, out PreviousStream))
{
if (!PreviousStream.CanSeek)
{
// If data is not seekable, take a MemoryStream copy of the existing data
// This could fail if the stream data is greater than 2 GB.
MemoryStream MemStream = new MemoryStream();
PreviousStream.CopyTo(MemStream);
PreviousStream = MemStream;
PendingWrites[FullPath] = PreviousStream;
}
// Return a proxy of the stored stream that we can read from without closing the underlying stream
ReadStream = new ProxyReadStream(PreviousStream);
}
else
{
if (Zip.ContainsEntry(FullPath))
{
ZipEntry Entry = Zip[FullPath];
if (Entry.UncompressedSize >= (2L * 1024 * 1024 * 1024))
{
Entry.Extract(Path.GetTempPath());
ReadStream = File.OpenRead(Path.Combine(Path.GetTempPath(), Entry.FileName));
}
else
{
ReadStream = new MemoryStream((int)Entry.UncompressedSize);
Zip[FullPath].Extract(ReadStream);
}
}
else
{
string ErrorMessage = String.Format("Expected \"{0}\" in IPA but it was not found", FullPath);
Program.Error(ErrorMessage);
throw new Exception(ErrorMessage);
}
}
ReadStream.Position = 0;
return ReadStream;
}
public override void CloseFile(Stream InStream)
{
if ((InStream as System.IO.FileStream) != null)
{
InStream.Close();
File.Delete((InStream as System.IO.FileStream).Name);
}
}
public override void WriteAllBytes(string RelativeFilename, byte[] Data)
{
WriteStream(RelativeFilename, new MemoryStream(Data));
}
public override void WriteStream(string RelativeFilename, Stream Data)
{
string FullPath = InternalRootPath + RelativeFilename;
// Add it to the pending writes list
PendingWrites.Remove(FullPath);
PendingWrites.Add(FullPath, Data);
}
/// <summary>
/// Make sure every directory in a path has an explicit entry (the input must be a directory name only, with no filename)
protected void EnsureDirectoriesExistInStrictPath(string DirectoryName)
{
int SlashIndex = DirectoryName.LastIndexOf("/");
if (SlashIndex >= 0)
{
string SmallerPath = DirectoryName.Substring(0, SlashIndex);
EnsureDirectoriesExistInStrictPath(SmallerPath);
}
DirectoryName = DirectoryName + "/";
if (!Zip.ContainsEntry(DirectoryName))
{
Zip.AddDirectoryByName(DirectoryName);
}
}
/// <summary>
/// Make sure every directory in a fully qualified path has an explicit entry
/// For a file named foo/bar.baz, ionic will only create one entry for foo/bar.baz,
/// but the Mac or 7z would create a foo/ entry as well.
/// </summary>
protected void EnsureDirectoriesExist(string Filename)
{
int SlashIndex = Filename.LastIndexOf("/");
if (SlashIndex >= 0)
{
string DirectoryName = Filename.Substring(0, SlashIndex);
EnsureDirectoriesExistInStrictPath(DirectoryName);
}
}
void CommitWrite(string FullPath, Stream StreamData)
{
// Create the directory if needed
EnsureDirectoriesExist(FullPath);
// Add the data
Zip.UpdateEntry(FullPath, StreamData);
}
public override void Close()
{
// Flush out the pending writes
foreach (var KVP in PendingWrites)
{
if (KVP.Value.CanSeek)
{
// Move back to the start of the file, in case we've used a StreamReadProxy to read
KVP.Value.Seek(0, SeekOrigin.Begin);
}
CommitWrite(KVP.Key, KVP.Value);
}
PendingWrites.Clear();
// Update permissions to be UNIX-style
// Modify the file attributes of any added file to unix format
foreach (ZipEntry E in Zip.Entries)
{
const byte FileAttributePlatform_NTFS = 0x0A;
const byte FileAttributePlatform_UNIX = 0x03;
const byte FileAttributePlatform_FAT = 0x00;
const int UNIX_FILETYPE_NORMAL_FILE = 0x8000;
//const int UNIX_FILETYPE_SOCKET = 0xC000;
//const int UNIX_FILETYPE_SYMLINK = 0xA000;
//const int UNIX_FILETYPE_BLOCKSPECIAL = 0x6000;
const int UNIX_FILETYPE_DIRECTORY = 0x4000;
//const int UNIX_FILETYPE_CHARSPECIAL = 0x2000;
//const int UNIX_FILETYPE_FIFO = 0x1000;
const int UNIX_EXEC = 1;
const int UNIX_WRITE = 2;
const int UNIX_READ = 4;
int MyPermissions = UNIX_READ | UNIX_WRITE;
int OtherPermissions = UNIX_READ;
int PlatformEncodedBy = (E.VersionMadeBy >> 8) & 0xFF;
int LowerBits = 0;
// Try to preserve read-only if it was set
bool bIsDirectory = E.IsDirectory;
// Check to see if this
bool bIsExecutable = false;
if (!String.IsNullOrEmpty(ExecutableFileName) && Path.GetFileNameWithoutExtension(E.FileName).Equals(ExecutableFileName, StringComparison.InvariantCultureIgnoreCase))
{
bIsExecutable = true;
}
if (bIsExecutable)
{
// The executable will be encrypted in the final distribution IPA and will compress very poorly, so keeping it
// uncompressed gives a better indicator of IPA size for our distro builds
E.CompressionLevel = CompressionLevel.None;
}
if ((PlatformEncodedBy == FileAttributePlatform_NTFS) || (PlatformEncodedBy == FileAttributePlatform_FAT))
{
FileAttributes OldAttributes = E.Attributes;
//LowerBits = ((int)E.Attributes) & 0xFFFF;
if ((OldAttributes & FileAttributes.Directory) != 0)
{
bIsDirectory = true;
}
// Permissions
if ((OldAttributes & FileAttributes.ReadOnly) != 0)
{
MyPermissions &= ~UNIX_WRITE;
OtherPermissions &= ~UNIX_WRITE;
}
}
if (bIsDirectory || bIsExecutable)
{
MyPermissions |= UNIX_EXEC;
OtherPermissions |= UNIX_EXEC;
}
// Re-jigger the external file attributes to UNIX style if they're not already that way
if (PlatformEncodedBy != FileAttributePlatform_UNIX)
{
int NewAttributes = bIsDirectory ? UNIX_FILETYPE_DIRECTORY : UNIX_FILETYPE_NORMAL_FILE;
NewAttributes |= (MyPermissions << 6);
NewAttributes |= (OtherPermissions << 3);
NewAttributes |= (OtherPermissions << 0);
// Now modify the properties
E.AdjustExternalFileAttributes(FileAttributePlatform_UNIX, (NewAttributes << 16) | LowerBits);
}
}
Zip.Save();
}
public override IEnumerable<string> GetAllPayloadFiles()
{
List<string> Result = new List<string>();
// Add files in the zip
IEnumerable<string> SourceList = Zip.EntryFileNames;
foreach (string Filename in SourceList)
{
ZipEntry Entry = Zip[Filename];
if (!Entry.IsDirectory && Filename.StartsWith(InternalRootPath))
{
string RelativePath = Filename.Substring(InternalRootPath.Length);
if (RelativePath.Length > 0)
{
Result.Add(RelativePath);
}
}
}
// Add files that are pending that weren't originally in the zip
foreach (var KVP in PendingWrites)
{
string Filename = KVP.Key;
if ((Zip[Filename] == null) && Filename.StartsWith(InternalRootPath))
{
string RelativePath = Filename.Substring(InternalRootPath.Length);
Result.Add(RelativePath);
}
}
return Result;
}
public void SetCompression(Ionic.Zlib.CompressionLevel Level)
{
Zip.CompressionLevel = Level;
}
}
/// <summary>
/// Implementation of the FileSystemAdapter interface that handles read-only access to Zip files
/// </summary>
public class ReadOnlyZipFileSystem : ZipFileSystem
{
public ReadOnlyZipFileSystem(ZipFile InZip, string RootPath)
: base(InZip, RootPath)
{
}
public ReadOnlyZipFileSystem(ZipFile InZip)
: base(InZip)
{
}
public ReadOnlyZipFileSystem(string Filename) : base(Filename)
{
}
public override void Close()
{
Zip.Dispose();
Zip = null;
}
public override void WriteAllBytes(string RelativeFilename, byte[] Data)
{
throw new InvalidOperationException("Cannot write files in a ReadOnlyZipFile object");
}
}
}
}