2014-10-22 17:48:30 +00:00
|
|
|
|
using System;
|
2014-10-22 13:47:38 +00:00
|
|
|
|
using System.Collections.Generic;
|
2014-10-22 17:07:03 +00:00
|
|
|
|
using System.Diagnostics;
|
2014-10-22 13:47:38 +00:00
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Text;
|
2014-10-22 20:56:05 +00:00
|
|
|
|
using GarrysMod.AddonCreator.Hashing;
|
2014-10-22 14:29:51 +00:00
|
|
|
|
using Newtonsoft.Json;
|
2014-10-22 13:47:38 +00:00
|
|
|
|
|
2014-10-22 20:56:05 +00:00
|
|
|
|
namespace GarrysMod.AddonCreator.Addon
|
2014-10-22 13:47:38 +00:00
|
|
|
|
{
|
2014-10-22 20:56:05 +00:00
|
|
|
|
public class AddonFile
|
2014-10-22 13:47:38 +00:00
|
|
|
|
{
|
|
|
|
|
private const byte FormatVersion = 3;
|
|
|
|
|
private const uint AppID = 4000;
|
|
|
|
|
private const uint CompressionSignature = 0xbeefcace;
|
2014-10-22 17:48:30 +00:00
|
|
|
|
private static readonly byte[] FormatIdent = Encoding.ASCII.GetBytes("GMAD");
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2014-10-22 20:56:05 +00:00
|
|
|
|
/// Initializes a new instance of <see cref="AddonFile" />
|
2014-10-22 17:48:30 +00:00
|
|
|
|
/// </summary>
|
2014-10-22 20:56:05 +00:00
|
|
|
|
public AddonFile()
|
2014-10-22 17:48:30 +00:00
|
|
|
|
{
|
|
|
|
|
Files = new Dictionary<string, AddonFileInfo>();
|
|
|
|
|
RequiredContent = new List<string>();
|
|
|
|
|
Version = 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Returns the timestamp of when the addon was built. This data is retrieved from full imports and for new (unsaved)
|
|
|
|
|
/// addons this is 0.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public ulong BuildTimestamp { get; private set; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// The name of this addon.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public string Title { get; set; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// The author of this addon.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public string Author { get; set; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// A description of this addon.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public string Description { get; set; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// This addon's version.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public int Version { get; set; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// The files to include in the addon.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public Dictionary<string, AddonFileInfo> Files { get; private set; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Currently unused.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public ulong SteamID { get; set; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Content that needs to exist in order to run this addon.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public List<string> RequiredContent { get; private set; }
|
2014-10-22 13:47:38 +00:00
|
|
|
|
|
|
|
|
|
public static void CreateFromFolder()
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2014-10-22 17:48:30 +00:00
|
|
|
|
/// Imports a gmod addon into this instance.
|
2014-10-22 13:47:38 +00:00
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="path">Path to a gmod addon file.</param>
|
|
|
|
|
/// <param name="withMetadata">Import all metadata (title, description, creator, timestamp, etc.) as well?</param>
|
|
|
|
|
public void Import(string path, bool withMetadata = true)
|
|
|
|
|
{
|
2014-10-22 17:07:03 +00:00
|
|
|
|
var stream = File.OpenRead(path);
|
|
|
|
|
|
2014-10-22 13:47:38 +00:00
|
|
|
|
{
|
2014-10-23 00:49:01 +00:00
|
|
|
|
var sr = new BinaryReader(stream, Encoding.GetEncoding("iso-8859-1"));
|
2014-10-22 13:47:38 +00:00
|
|
|
|
|
|
|
|
|
// Check format header
|
|
|
|
|
if (!sr.ReadBytes(4).SequenceEqual(FormatIdent)
|
|
|
|
|
|| sr.ReadByte() != FormatVersion)
|
|
|
|
|
throw new FormatException();
|
|
|
|
|
|
|
|
|
|
// Check addon's CRC32 hash
|
|
|
|
|
{
|
2014-10-22 17:07:03 +00:00
|
|
|
|
Debug.WriteLine("Checking CRC32...");
|
2014-10-22 13:47:38 +00:00
|
|
|
|
var baseAddon = new byte[stream.Length - sizeof (int)];
|
|
|
|
|
var oldpos = stream.Position;
|
|
|
|
|
stream.Position = 0;
|
|
|
|
|
stream.Read(baseAddon, 0, baseAddon.Length);
|
|
|
|
|
var baseAddonHash = sr.ReadInt32();
|
|
|
|
|
if (ParallelCRC.Compute(baseAddon) != baseAddonHash)
|
|
|
|
|
{
|
|
|
|
|
throw new IOException("Data corrupted (calculated hash mismatching hash in addon file)");
|
|
|
|
|
}
|
2014-10-22 17:07:03 +00:00
|
|
|
|
stream.Position = oldpos;
|
2014-10-22 13:47:38 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Import metadata
|
|
|
|
|
var newSteamID = sr.ReadUInt64();
|
|
|
|
|
var newBuildTimestamp = sr.ReadUInt64();
|
|
|
|
|
var newRequiredContentLen = sr.ReadByte();
|
2014-10-22 17:07:03 +00:00
|
|
|
|
var newTitle = sr.ReadString(true);
|
|
|
|
|
var newDescription = sr.ReadString(true);
|
|
|
|
|
var newAuthor = sr.ReadString(true);
|
|
|
|
|
var newVersion = sr.ReadInt32();
|
|
|
|
|
|
|
|
|
|
Debug.WriteLine("## Metadata ##");
|
|
|
|
|
Debug.WriteLine("Steam ID: {0}", newSteamID);
|
|
|
|
|
Debug.WriteLine("Build time: {0}", newBuildTimestamp);
|
|
|
|
|
Debug.WriteLine("Required content count: {0}", newRequiredContentLen);
|
|
|
|
|
|
|
|
|
|
Debug.Assert(newSteamID == 0);
|
|
|
|
|
Debug.Assert(newRequiredContentLen == 0);
|
|
|
|
|
|
2014-10-22 13:47:38 +00:00
|
|
|
|
for (var b = 0; b < newRequiredContentLen; b++)
|
|
|
|
|
{
|
|
|
|
|
var value = sr.ReadString(true);
|
|
|
|
|
if (withMetadata && !RequiredContent.Contains(value))
|
|
|
|
|
RequiredContent.Add(value);
|
|
|
|
|
}
|
2014-10-22 17:07:03 +00:00
|
|
|
|
|
2014-10-22 13:47:38 +00:00
|
|
|
|
if (withMetadata)
|
|
|
|
|
{
|
|
|
|
|
SteamID = newSteamID;
|
|
|
|
|
BuildTimestamp = newBuildTimestamp;
|
2014-10-22 17:07:03 +00:00
|
|
|
|
Title = newTitle;
|
|
|
|
|
Description = newDescription;
|
|
|
|
|
Author = newAuthor;
|
|
|
|
|
Version = newVersion;
|
2014-10-22 13:47:38 +00:00
|
|
|
|
}
|
|
|
|
|
|
2014-10-22 17:07:03 +00:00
|
|
|
|
Debug.WriteLine("");
|
|
|
|
|
|
2014-10-22 13:47:38 +00:00
|
|
|
|
// file list
|
2014-10-22 17:07:03 +00:00
|
|
|
|
Debug.WriteLine("## File list ##");
|
2014-10-22 13:47:38 +00:00
|
|
|
|
var newFilesList = new Dictionary<string, Tuple<long, int>>();
|
2014-10-22 17:07:03 +00:00
|
|
|
|
var expectedFileId = 1;
|
2014-10-22 13:47:38 +00:00
|
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
var fileId = sr.ReadUInt32();
|
|
|
|
|
if (fileId == 0)
|
|
|
|
|
break; // end of list
|
|
|
|
|
|
|
|
|
|
// key, size, hash
|
|
|
|
|
var filePath = sr.ReadString(true);
|
|
|
|
|
var fileSize = sr.ReadInt64();
|
|
|
|
|
var fileHash = sr.ReadInt32();
|
|
|
|
|
|
2014-10-22 17:48:30 +00:00
|
|
|
|
Debug.WriteLine("\t#{2} : {0} ({1:0.0} kB)", filePath, fileSize/1024, fileId);
|
2014-10-22 17:07:03 +00:00
|
|
|
|
Debug.Assert(fileId == expectedFileId);
|
|
|
|
|
|
|
|
|
|
expectedFileId++;
|
|
|
|
|
|
2014-10-22 13:47:38 +00:00
|
|
|
|
// avoid duplicates
|
|
|
|
|
if (newFilesList.ContainsKey(filePath))
|
|
|
|
|
{
|
2014-10-22 17:48:30 +00:00
|
|
|
|
throw new IOException(
|
|
|
|
|
"Found duplicate file path in addon file. Contact the addon creator and tell him to build a new proper addon file.");
|
2014-10-22 13:47:38 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
newFilesList.Add(filePath, new Tuple<long, int>(fileSize, fileHash));
|
|
|
|
|
} while (true);
|
2014-10-22 17:07:03 +00:00
|
|
|
|
Debug.WriteLine("");
|
2014-10-22 13:47:38 +00:00
|
|
|
|
|
2014-10-22 17:07:03 +00:00
|
|
|
|
Debug.WriteLine("## File import ##");
|
2014-10-22 13:47:38 +00:00
|
|
|
|
foreach (var file in newFilesList)
|
|
|
|
|
{
|
|
|
|
|
var filePath = file.Key;
|
|
|
|
|
var fileSize = file.Value.Item1;
|
|
|
|
|
var fileHash = file.Value.Item2;
|
2014-10-22 17:07:03 +00:00
|
|
|
|
|
2014-10-22 17:48:30 +00:00
|
|
|
|
Debug.WriteLine("Extracting: {0} ({1:0.00} kB)", filePath, fileSize/1024);
|
2014-10-22 17:07:03 +00:00
|
|
|
|
|
2014-10-22 13:47:38 +00:00
|
|
|
|
var fileContent = new byte[fileSize];
|
2014-10-22 17:48:30 +00:00
|
|
|
|
|
2014-10-22 13:47:38 +00:00
|
|
|
|
// long-compatible file reading
|
|
|
|
|
for (long i = 0; i < fileSize; i += int.MaxValue)
|
|
|
|
|
{
|
2014-10-22 17:48:30 +00:00
|
|
|
|
var tempContent = sr.ReadBytes((int) Math.Min(int.MaxValue, fileSize));
|
2014-10-22 13:47:38 +00:00
|
|
|
|
tempContent.CopyTo(fileContent, i);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CRC check for this file
|
|
|
|
|
var fileCalcHash = ParallelCRC.Compute(fileContent);
|
|
|
|
|
if (fileCalcHash != fileHash)
|
|
|
|
|
{
|
|
|
|
|
throw new IOException("File " + filePath + " in addon file is corrupted (hash mismatch)");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Files.Add(filePath, new SegmentedAddonFileInfo(stream, sr.BaseStream.Position, fileSize, fileHash));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2014-10-22 17:48:30 +00:00
|
|
|
|
/// Exports this addon into a GMA file.
|
2014-10-22 13:47:38 +00:00
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="path">The output file path, should be pointing to a writable location ending with ".gma".</param>
|
|
|
|
|
public void Export(string path)
|
|
|
|
|
{
|
2014-10-22 17:34:42 +00:00
|
|
|
|
// Enforce .gma extension
|
|
|
|
|
if (!path.EndsWith(".gma", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
var pathSplit = path.Split('.');
|
|
|
|
|
pathSplit[pathSplit.Length - 1] = "gma";
|
|
|
|
|
path = string.Join(".", pathSplit);
|
|
|
|
|
}
|
2014-10-22 13:47:38 +00:00
|
|
|
|
|
|
|
|
|
// Checking for existing addon.json
|
|
|
|
|
if (!Files.ContainsKey("addon.json"))
|
|
|
|
|
{
|
|
|
|
|
throw new FileNotFoundException("Addon building requires a valid addon.json file.");
|
|
|
|
|
}
|
|
|
|
|
|
2014-10-22 14:29:51 +00:00
|
|
|
|
var files = Files;
|
|
|
|
|
|
|
|
|
|
// Check for errors and ignores in addon.json
|
2014-10-22 17:48:30 +00:00
|
|
|
|
var addonJson =
|
|
|
|
|
JsonConvert.DeserializeObject<AddonJson>(Encoding.UTF8.GetString(Files["addon.json"].GetContents()));
|
2014-10-22 14:29:51 +00:00
|
|
|
|
addonJson.CheckForErrors();
|
|
|
|
|
addonJson.RemoveIgnoredFiles(ref files);
|
|
|
|
|
|
2014-10-22 17:42:45 +00:00
|
|
|
|
// Extract data from addon.json
|
|
|
|
|
Title = addonJson.Title;
|
2014-10-22 17:51:18 +00:00
|
|
|
|
Author = addonJson.Author;
|
2014-10-22 17:42:45 +00:00
|
|
|
|
Description = string.IsNullOrEmpty(addonJson.Description) ? string.Empty : addonJson.Description;
|
|
|
|
|
Version = addonJson.Version;
|
2014-10-22 17:48:30 +00:00
|
|
|
|
|
2014-10-22 17:42:45 +00:00
|
|
|
|
// Create a stripped down version of addon.json for the output gma, it will replace the Description field
|
|
|
|
|
var newDescription = JsonConvert.SerializeObject(new AddonJson
|
|
|
|
|
{
|
|
|
|
|
Description = Description,
|
|
|
|
|
Tags = addonJson.Tags,
|
|
|
|
|
Type = addonJson.Type
|
|
|
|
|
});
|
2014-10-22 17:07:03 +00:00
|
|
|
|
|
2014-10-22 14:29:51 +00:00
|
|
|
|
// Sort files
|
|
|
|
|
var resultingFiles = new SortedDictionary<string, AddonFileInfo>(files);
|
2014-10-22 17:42:45 +00:00
|
|
|
|
resultingFiles.Remove("addon.json");
|
2014-10-22 14:29:51 +00:00
|
|
|
|
|
|
|
|
|
// General whitelist
|
|
|
|
|
var blacklistedFiles = AddonWhitelist
|
|
|
|
|
.FindBlacklistedFiles(resultingFiles.Select(i => i.Key))
|
|
|
|
|
.ToArray();
|
|
|
|
|
if (blacklistedFiles.Any())
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException("Found files which aren't whitelisted. Remove or ignore those files before you retry packing your addon:"
|
2014-10-22 17:48:30 +00:00
|
|
|
|
+ Environment.NewLine + Environment.NewLine
|
|
|
|
|
+ string.Join(Environment.NewLine, blacklistedFiles));
|
2014-10-22 14:29:51 +00:00
|
|
|
|
}
|
2014-10-22 13:47:38 +00:00
|
|
|
|
|
|
|
|
|
using (var stream = new MemoryStream())
|
|
|
|
|
{
|
|
|
|
|
// TODO: Standardized encoding - Garry should use standardized encoding, currently he uses Encoding.Default which is applocale-dependent...
|
2014-10-23 00:49:01 +00:00
|
|
|
|
var sw = new BinaryWriter(stream, Encoding.GetEncoding("iso-8859-1"));
|
2014-10-22 13:47:38 +00:00
|
|
|
|
|
|
|
|
|
// Format header
|
|
|
|
|
sw.Write(FormatIdent);
|
|
|
|
|
sw.Write(FormatVersion);
|
|
|
|
|
|
|
|
|
|
// Creator steam ID
|
|
|
|
|
sw.Write(SteamID);
|
|
|
|
|
|
|
|
|
|
// Build timestamp
|
2014-10-22 17:48:30 +00:00
|
|
|
|
sw.Write(BuildTimestamp = (ulong) (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds);
|
2014-10-22 13:47:38 +00:00
|
|
|
|
|
|
|
|
|
// Required content
|
|
|
|
|
if (RequiredContent.Count > byte.MaxValue)
|
|
|
|
|
{
|
2014-10-22 17:48:30 +00:00
|
|
|
|
throw new IndexOutOfRangeException("Required content count must not exceed " + byte.MaxValue +
|
|
|
|
|
" entries.");
|
2014-10-22 13:47:38 +00:00
|
|
|
|
}
|
2014-10-22 17:48:30 +00:00
|
|
|
|
sw.Write((byte) RequiredContent.Count);
|
2014-10-22 17:42:45 +00:00
|
|
|
|
foreach (var content in RequiredContent)
|
2014-10-22 13:47:38 +00:00
|
|
|
|
{
|
|
|
|
|
sw.Write(content, true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Metadata
|
|
|
|
|
sw.Write(Title, true);
|
2014-10-22 17:42:45 +00:00
|
|
|
|
sw.Write(newDescription, true); // "Description" field which is actually a trimmed down addon.json
|
2014-10-22 13:47:38 +00:00
|
|
|
|
sw.Write(Author, true);
|
|
|
|
|
sw.Write(Version);
|
|
|
|
|
|
|
|
|
|
// File list
|
2014-10-23 00:28:05 +00:00
|
|
|
|
#if SUPPORT_BIG
|
2014-10-22 13:47:38 +00:00
|
|
|
|
if (Files.Count > uint.MaxValue)
|
|
|
|
|
{
|
2014-10-22 17:48:30 +00:00
|
|
|
|
throw new IndexOutOfRangeException("Number of addon files must not exceed " + uint.MaxValue +
|
|
|
|
|
" elements.");
|
2014-10-22 13:47:38 +00:00
|
|
|
|
}
|
2014-10-23 00:28:05 +00:00
|
|
|
|
#endif
|
2014-10-22 13:47:38 +00:00
|
|
|
|
uint fileNum = 0;
|
2014-10-22 14:29:51 +00:00
|
|
|
|
foreach (var file in resultingFiles)
|
2014-10-22 13:47:38 +00:00
|
|
|
|
{
|
|
|
|
|
fileNum++;
|
|
|
|
|
sw.Write(fileNum);
|
|
|
|
|
sw.Write(file.Key.ToLower(), true); // Path
|
|
|
|
|
sw.Write(file.Value.Size);
|
|
|
|
|
sw.Write(file.Value.Crc32Hash);
|
|
|
|
|
}
|
2014-10-22 17:48:30 +00:00
|
|
|
|
sw.Write((uint) 0); // End of file list
|
2014-10-22 13:47:38 +00:00
|
|
|
|
|
|
|
|
|
// File contents
|
2014-10-22 14:29:51 +00:00
|
|
|
|
foreach (var file in resultingFiles)
|
2014-10-22 13:47:38 +00:00
|
|
|
|
{
|
|
|
|
|
if (file.Value.Size == 0)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
sw.Write(file.Value.GetContents());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Addon CRC
|
|
|
|
|
var addonHash = ParallelCRC.Compute(stream.ToArray());
|
|
|
|
|
sw.Write(addonHash);
|
|
|
|
|
|
|
|
|
|
using (var outfile = File.Create(path))
|
|
|
|
|
{
|
2014-10-22 14:01:44 +00:00
|
|
|
|
stream.Position = 0;
|
2014-10-22 13:47:38 +00:00
|
|
|
|
stream.CopyTo(outfile);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2014-10-22 17:48:30 +00:00
|
|
|
|
}
|