├── AFSLib ├── Entry.cs ├── StreamEntryInfo.cs ├── NullEntry.cs ├── HeaderMagicType.cs ├── NotificationType.cs ├── AttributesInfoType.cs ├── FileEntry.cs ├── StreamEntry.cs ├── AFSLib.csproj ├── Utils.cs ├── DataEntry.cs ├── SubStream.cs └── AFS.cs ├── AFSLib.Tests ├── AFSLib.Tests.csproj └── AFSLibTests.cs ├── LICENSE ├── AFSLib.sln ├── CHANGELOG.md ├── README.md └── .gitignore /AFSLib/Entry.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace AFSLib 4 | { 5 | /// 6 | /// Abstract class that represents an entry. All types of entries derive from Entry. 7 | /// 8 | public abstract class Entry 9 | { 10 | internal abstract Stream GetStream(); 11 | } 12 | } -------------------------------------------------------------------------------- /AFSLib/StreamEntryInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AFSLib 4 | { 5 | internal struct StreamEntryInfo 6 | { 7 | public uint Offset; 8 | public string Name; 9 | public uint Size; 10 | public DateTime LastWriteTime; 11 | public uint CustomData; 12 | 13 | public bool IsNull => Offset == 0; 14 | } 15 | } -------------------------------------------------------------------------------- /AFSLib/NullEntry.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace AFSLib 4 | { 5 | /// 6 | /// Class that represents an empty entry with no data. 7 | /// 8 | public class NullEntry : Entry 9 | { 10 | internal NullEntry() 11 | { 12 | 13 | } 14 | 15 | internal override Stream GetStream() 16 | { 17 | return null; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /AFSLib/HeaderMagicType.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace AFSLib 3 | { 4 | /// 5 | /// Enumeration containing each type of header magic that can be found in an AFS archive. 6 | /// 7 | public enum HeaderMagicType 8 | { 9 | /// 10 | /// Some AFS files contain a 4-byte header magic with 'AFS' followed by 0x00. 11 | /// 12 | AFS_00, 13 | 14 | /// 15 | /// Some AFS files contain a 4-byte header magic with 'AFS' followed by 0x20. 16 | /// 17 | AFS_20 18 | } 19 | } -------------------------------------------------------------------------------- /AFSLib.Tests/AFSLib.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /AFSLib/NotificationType.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace AFSLib 3 | { 4 | /// 5 | /// Enumeration containing all types of notifications. 6 | /// 7 | public enum NotificationType 8 | { 9 | /// 10 | /// Notification considered as Information. 11 | /// 12 | Info, 13 | 14 | /// 15 | /// Notification considered as Warning. 16 | /// 17 | Warning, 18 | 19 | /// 20 | /// Notification considered as Error. 21 | /// 22 | Error, 23 | 24 | /// 25 | /// Notification considered as Success. 26 | /// 27 | Success 28 | } 29 | } -------------------------------------------------------------------------------- /AFSLib/AttributesInfoType.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace AFSLib 3 | { 4 | /// 5 | /// Enumeration that contains all possible attribute info locations in an AFS archive. 6 | /// 7 | public enum AttributesInfoType 8 | { 9 | /// 10 | /// The AFS file doesn't contain an attributes block. 11 | /// 12 | NoAttributes, 13 | 14 | /// 15 | /// Info about the attributes block is located at the beginning of the attributes info block. 16 | /// 17 | InfoAtBeginning, 18 | 19 | /// 20 | /// Info about the attributes block is located at the end of the attributes info block. 21 | /// 22 | InfoAtEnd 23 | } 24 | } -------------------------------------------------------------------------------- /AFSLib/FileEntry.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace AFSLib 4 | { 5 | /// 6 | /// Class that represents an entry with data referenced from a file. 7 | /// 8 | public sealed class FileEntry : DataEntry 9 | { 10 | private readonly FileInfo fileInfo; 11 | 12 | internal FileEntry(string fileNamePath, string entryName) 13 | { 14 | fileInfo = new FileInfo(fileNamePath); 15 | 16 | Name = entryName; 17 | Size = (uint)fileInfo.Length; 18 | LastWriteTime = fileInfo.LastWriteTime; 19 | CustomData = (uint)fileInfo.Length; 20 | } 21 | 22 | internal override Stream GetStream() 23 | { 24 | return fileInfo.OpenRead(); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /AFSLib/StreamEntry.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace AFSLib 4 | { 5 | /// 6 | /// Class that represents an entry with data referenced from a stream. 7 | /// 8 | public sealed class StreamEntry : DataEntry 9 | { 10 | private readonly Stream baseStream; 11 | private readonly uint baseStreamDataOffset; 12 | 13 | internal StreamEntry(Stream baseStream, StreamEntryInfo info) 14 | { 15 | this.baseStream = baseStream; 16 | baseStreamDataOffset = info.Offset; 17 | 18 | Name = info.Name; 19 | Size = info.Size; 20 | LastWriteTime = info.LastWriteTime; 21 | CustomData = info.CustomData; 22 | } 23 | 24 | internal override Stream GetStream() 25 | { 26 | baseStream.Position = baseStreamDataOffset; 27 | return new SubStream(baseStream, 0, Size, true); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 MaikelChan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /AFSLib/AFSLib.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | true 6 | MaikelChan 7 | AFSLib is a library that can extract, create and manipulate AFS files. The AFS format is used in many games from companies like Sega. 8 | 2.0.3 9 | https://github.com/MaikelChan/AFSLib 10 | https://github.com/MaikelChan/AFSLib 11 | games, game, packer, unpacker, romhacking, rom, afs, afs-archive 12 | false 13 | MaikelChan.AFSLib 14 | MIT 15 | https://github.com/MaikelChan/AFSLib/blob/main/CHANGELOG.md 16 | 17 | 18 | 19 | none 20 | false 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /AFSLib/Utils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | 5 | namespace AFSLib 6 | { 7 | internal static class Utils 8 | { 9 | internal static uint Pad(uint value, uint alignment) 10 | { 11 | uint mod = value % alignment; 12 | if (mod != 0) return value + (alignment - mod); 13 | else return value; 14 | } 15 | 16 | internal static void FillStreamWithZeroes(Stream stream, uint length) 17 | { 18 | byte[] padding = new byte[length]; 19 | stream.Write(padding, 0, (int)length); 20 | } 21 | 22 | internal static void CopySliceTo(this Stream origin, Stream destination, int bytesCount) 23 | { 24 | byte[] buffer = new byte[65536]; 25 | int count; 26 | 27 | while ((count = origin.Read(buffer, 0, Math.Min(buffer.Length, bytesCount))) != 0) 28 | { 29 | destination.Write(buffer, 0, count); 30 | bytesCount -= count; 31 | } 32 | } 33 | 34 | internal static string GetStringFromBytes(byte[] bytes) 35 | { 36 | return Encoding.Default.GetString(bytes).Replace("\0", ""); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /AFSLib.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31624.102 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AFSLib", "AFSLib\AFSLib.csproj", "{12457AF2-EE52-45F8-AC07-85451250C630}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AFSLib.Tests", "AFSLib.Tests\AFSLib.Tests.csproj", "{F993C812-66F6-4854-96AF-5355DDAA4D77}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {12457AF2-EE52-45F8-AC07-85451250C630}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {12457AF2-EE52-45F8-AC07-85451250C630}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {12457AF2-EE52-45F8-AC07-85451250C630}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {12457AF2-EE52-45F8-AC07-85451250C630}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {F993C812-66F6-4854-96AF-5355DDAA4D77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {F993C812-66F6-4854-96AF-5355DDAA4D77}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {F993C812-66F6-4854-96AF-5355DDAA4D77}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {F993C812-66F6-4854-96AF-5355DDAA4D77}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {74903023-7F99-4707-9CF9-A1A08AAFE436} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [2.0.3] - 2023-09-11 8 | ### Added 9 | - Added `AllAttributesContainEntrySize` property easily check if all the attributes of an AFS archive contain the entry size or a customized data. 10 | ### Fixed 11 | - Fixed interpreting 0 byte files as Null entries, which broke games like Urban Reign. 12 | ### Deprecated 13 | - Deprecated the `UnknownAttribute` and related properties. It is now called `CustomData`. 14 | 15 | ## [2.0.2] - 2021-10-26 16 | ### Fixed 17 | - Fixed regression: now it's able to handle AFS files with invalid dates. 18 | 19 | ## [2.0.1] - 2021-09-06 20 | ### Fixed 21 | - Fixed missing XML documentation in NuGet package. 22 | 23 | ## [2.0.0] - 2021-09-06 24 | ### Added 25 | - Entry block alignment is now configurable. 26 | - AFS constructor now accepts a file path. 27 | - Added method to add all files in a directory and, optionally, its subdirectories. 28 | - Added a method to save directly to a file. 29 | - AddEntry* methods now return a reference to the added entry. 30 | - Implemented null entries. They are created with NullEntry, and fixed all issues related to not being able to remove them or add them. 31 | 32 | ### Changed 33 | - AFS class is now IDisposable. Dispose needs to be called to close the internal FileStream in case the AFS object is created from a file. 34 | - When adding an entry, null entry names are considered as string.Empty. 35 | - Improved XML documentation. 36 | - Renamed Name to SanitizedName and RawName to Name. This makes more sense because Name is the actual name of an entry, and SanitizedName is the autogenerated read-only name that contains no invalid characters. 37 | - UnknownAttribute and LastWriteTime properties are now modifiable. 38 | 39 | ### Deprecated 40 | - Deprecated AddEntry, ExtractEntry and ExtractAllEntries. Now they are called AddEntryFromFile, AddEntryFromStream, ExtractEntryToFile, ExtractAllEntriesToDirectory. 41 | - In an entry, Rename has been deprecated. Use the Name property instead. 42 | - In an entry, Unknown property has been deprecated. Use the UnknownAttribute property instead. 43 | 44 | ### Fixed 45 | - Make sure an entry name cannot be longer than 32 characters. 46 | 47 | ## [1.0.1] - 2021-09-01 48 | ### Added 49 | - Be able to add an entry from a Stream to an AFS file. 50 | ### Fixed 51 | - Fixed an exception message. 52 | 53 | ## [1.0.0] - 2021-09-01 54 | - Initial release. -------------------------------------------------------------------------------- /AFSLib/DataEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AFSLib 4 | { 5 | /// 6 | /// Abstract class that represents an entry with data. 7 | /// 8 | public abstract class DataEntry : Entry 9 | { 10 | /// 11 | /// The name of the entry, which can't be longer than 32 characters, including extension. 12 | /// It can contain special characters like ":", be an empty string, 13 | /// and it can be the same name as other entries. 14 | /// For that reason, it's not safe to use this name to extract a file into the operating system. 15 | /// 16 | public string Name 17 | { 18 | get => name; 19 | set 20 | { 21 | if (value.Length > AFS.MAX_ENTRY_NAME_LENGTH) 22 | { 23 | throw new ArgumentOutOfRangeException(nameof(value), $"Entry name can't be longer than {AFS.MAX_ENTRY_NAME_LENGTH} characters: \"{value}\"."); 24 | } 25 | 26 | if (value == null) 27 | { 28 | value = string.Empty; 29 | } 30 | 31 | name = value; 32 | NameChanged?.Invoke(); 33 | } 34 | } 35 | private string name; 36 | 37 | /// 38 | /// An autogenerated name of the entry. 39 | /// It will be unique and won't contain special characters like ":". 40 | /// This can be safely used to name extracted files into the operating system. 41 | /// 42 | public string SanitizedName { get; internal set; } 43 | 44 | /// 45 | /// Size of the entry data. 46 | /// 47 | public uint Size { get; protected set; } 48 | 49 | /// 50 | /// Date of the last time the entry was modified. 51 | /// 52 | public DateTime LastWriteTime { get; set; } 53 | 54 | /// 55 | /// In old AFS archives this usually contains the entry size, but in later versions it contains custom developer data. 56 | /// 57 | public uint CustomData { get; set; } 58 | 59 | /// 60 | /// Returns true if CustomData contains the entry size. 61 | /// 62 | public bool CustomDataContainsSize => CustomData == Size; 63 | 64 | internal event Action NameChanged; 65 | 66 | #region Deprecated 67 | 68 | /// 69 | /// Sometimes it's the entry size, sometimes not? 70 | /// 71 | [Obsolete("This property is deprecated since version 2.0.0, please use the UnknownAttribute property.")] 72 | public uint Unknown => UnknownAttribute; 73 | 74 | /// 75 | /// Renames an entry. 76 | /// 77 | /// The new name to assign to the entry. 78 | [Obsolete("This method is deprecated since version 2.0.0, please use the Name property.")] 79 | public void Rename(string name) 80 | { 81 | Name = name; 82 | } 83 | 84 | /// 85 | /// In many AFS archives this contains the entry size, but some of them contain some unknown values. 86 | /// 87 | [Obsolete("This property is deprecated since version 2.0.3, please use the CustomData property.")] 88 | public uint UnknownAttribute 89 | { 90 | get => CustomData; 91 | set => CustomData = value; 92 | } 93 | 94 | /// 95 | /// Usually, UnknownAttribute will contain the same value as Size, but sometimes it will contain a different unknown value. 96 | /// This returns true if it's the later. 97 | /// 98 | [Obsolete("This property is deprecated since version 2.0.3, please use CustomDataIsSize.")] 99 | public bool HasUnknownAttribute => !CustomDataContainsSize; 100 | 101 | #endregion 102 | } 103 | } -------------------------------------------------------------------------------- /AFSLib/SubStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace AFSLib 5 | { 6 | class SubStream : Stream 7 | { 8 | private Stream baseStream; 9 | private readonly long origin; 10 | private readonly long length; 11 | private readonly bool leaveBaseStreamOpen; 12 | 13 | public SubStream(Stream baseStream, long offset, long length, bool leaveBaseStreamOpen) 14 | { 15 | if (baseStream == null) throw new ArgumentNullException(nameof(baseStream)); 16 | if (!baseStream.CanRead) throw new ArgumentException("Base stream is not readable."); 17 | if (!baseStream.CanSeek) throw new ArgumentException("Base stream is non seekable."); 18 | if (offset < 0) throw new ArgumentOutOfRangeException(nameof(offset)); 19 | if (length < 0) throw new ArgumentOutOfRangeException(nameof(length)); 20 | 21 | origin = baseStream.Position + offset; 22 | if (baseStream.Length - origin < length) throw new ArgumentException("Offset or length arguments would try to read past the end of base stream."); 23 | 24 | this.baseStream = baseStream; 25 | this.length = length; 26 | this.leaveBaseStreamOpen = leaveBaseStreamOpen; 27 | 28 | Position = 0; 29 | } 30 | 31 | public override int Read(byte[] buffer, int offset, int count) 32 | { 33 | CheckDisposed(); 34 | 35 | long remaining = length - Position; 36 | if (remaining <= 0) return 0; 37 | if (remaining < count) count = (int)remaining; 38 | return baseStream.Read(buffer, offset, count); 39 | } 40 | 41 | private void CheckDisposed() 42 | { 43 | if (baseStream == null) throw new ObjectDisposedException(GetType().Name); 44 | } 45 | 46 | public override long Length 47 | { 48 | get { CheckDisposed(); return length; } 49 | } 50 | 51 | public override bool CanRead 52 | { 53 | get { CheckDisposed(); return true; } 54 | } 55 | 56 | public override bool CanWrite 57 | { 58 | get { CheckDisposed(); return false; } 59 | } 60 | 61 | public override bool CanSeek 62 | { 63 | get { CheckDisposed(); return true; } 64 | } 65 | 66 | public override long Position 67 | { 68 | get 69 | { 70 | CheckDisposed(); 71 | 72 | return baseStream.Position - origin; 73 | } 74 | set 75 | { 76 | CheckDisposed(); 77 | 78 | if (value < 0) throw new ArgumentOutOfRangeException(nameof(value)); 79 | if (value > length) throw new ArgumentOutOfRangeException(nameof(value)); 80 | baseStream.Position = origin + value; 81 | } 82 | } 83 | 84 | public override long Seek(long offset, SeekOrigin seekOrigin) 85 | { 86 | CheckDisposed(); 87 | 88 | if (offset > int.MaxValue) throw new ArgumentOutOfRangeException(nameof(offset)); 89 | 90 | switch (seekOrigin) 91 | { 92 | case SeekOrigin.Begin: 93 | Position = offset; 94 | break; 95 | case SeekOrigin.Current: 96 | Position += offset; 97 | break; 98 | case SeekOrigin.End: 99 | Position = length - offset; 100 | break; 101 | default: 102 | throw new ArgumentException("Invalid seekOrigin."); 103 | } 104 | 105 | return Position; 106 | } 107 | 108 | public override void SetLength(long value) 109 | { 110 | throw new NotSupportedException(); 111 | } 112 | 113 | public override void Flush() 114 | { 115 | CheckDisposed(); 116 | baseStream.Flush(); 117 | } 118 | 119 | protected override void Dispose(bool disposing) 120 | { 121 | base.Dispose(disposing); 122 | 123 | if (disposing) 124 | { 125 | if (baseStream != null) 126 | { 127 | if (!leaveBaseStreamOpen) 128 | { 129 | try { baseStream.Dispose(); } 130 | catch { } 131 | } 132 | 133 | baseStream = null; 134 | } 135 | } 136 | } 137 | 138 | public override void Write(byte[] buffer, int offset, int count) 139 | { 140 | throw new NotImplementedException(); 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /AFSLib.Tests/AFSLibTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using System.IO; 3 | using System.Security.Cryptography; 4 | 5 | namespace AFSLib.Tests 6 | { 7 | [TestClass] 8 | public class AFSLibTests 9 | { 10 | readonly string TestFilesDirectory = @"..\..\..\..\..\..\Test Files"; 11 | 12 | readonly string[] TestFiles = new string[] 13 | { 14 | //"0_sound.afs", // Pro Evolution Soccer 5 (Fails due to having random data between files. Rest is ok) 15 | //"ADX_USA.AFS", // Dragon Ball Z Budokai 3 (Fails due to having an uint indicating the size of the whole AFS file. No other AFS has this. Rest is ok) 16 | "AREA50.AFS", // Winback 2: Project Poseidon 17 | "btl_voice.afs", // Arc Rise Fantasia 18 | //"DVD000.AFS", // Urban Reign (PS2) (Fails due to adding missind dates) 19 | "movie.afs", // Soul Calibur 2 (PS2) 20 | "mry.afs" // Resident Evil: Code Veronica (GameCube) 21 | }; 22 | 23 | [TestMethod] 24 | public void Regeneration() 25 | { 26 | using (MD5 md5 = MD5.Create()) 27 | { 28 | for (int tf = 0; tf < TestFiles.Length; tf++) 29 | { 30 | string filePath1 = Path.Combine(TestFilesDirectory, TestFiles[tf]); 31 | string filePath2 = filePath1 + "_2"; 32 | string extractionDirectory = Path.ChangeExtension(filePath1, null); 33 | 34 | byte[] hash1; 35 | byte[] hash2; 36 | 37 | HeaderMagicType header; 38 | AttributesInfoType attributesInfo; 39 | uint entryBlockAlignment; 40 | string[] entriesNames; 41 | string[] entriesSanitizedNames; 42 | uint[] entriesCustomData; 43 | 44 | using (FileStream stream1 = File.OpenRead(filePath1)) 45 | { 46 | hash1 = md5.ComputeHash(stream1); 47 | } 48 | 49 | using (AFS afs1 = new AFS(filePath1)) 50 | { 51 | header = afs1.HeaderMagicType; 52 | attributesInfo = afs1.AttributesInfoType; 53 | entryBlockAlignment = afs1.EntryBlockAlignment; 54 | entriesNames = new string[afs1.Entries.Count]; 55 | entriesSanitizedNames = new string[afs1.Entries.Count]; 56 | entriesCustomData = new uint[afs1.Entries.Count]; 57 | 58 | for (int e = 0; e < entriesSanitizedNames.Length; e++) 59 | { 60 | if (afs1.Entries[e] is NullEntry) 61 | { 62 | entriesNames[e] = null; 63 | entriesSanitizedNames[e] = null; 64 | entriesCustomData[e] = 0; 65 | } 66 | else 67 | { 68 | DataEntry dataEntry = afs1.Entries[e] as DataEntry; 69 | 70 | entriesNames[e] = dataEntry.Name; 71 | entriesSanitizedNames[e] = dataEntry.SanitizedName; 72 | entriesCustomData[e] = dataEntry.CustomData; 73 | } 74 | } 75 | 76 | afs1.ExtractAllEntriesToDirectory(extractionDirectory); 77 | } 78 | 79 | using (AFS afs2 = new AFS()) 80 | { 81 | afs2.HeaderMagicType = header; 82 | afs2.AttributesInfoType = attributesInfo; 83 | afs2.EntryBlockAlignment = entryBlockAlignment; 84 | 85 | for (int e = 0; e < entriesSanitizedNames.Length; e++) 86 | { 87 | if (entriesSanitizedNames[e] == null && entriesNames[e] == null) 88 | { 89 | afs2.AddNullEntry(); 90 | } 91 | else 92 | { 93 | FileEntry fileEntry = afs2.AddEntryFromFile(Path.Combine(extractionDirectory, entriesSanitizedNames[e]), entriesNames[e]); 94 | fileEntry.CustomData = entriesCustomData[e]; 95 | } 96 | } 97 | 98 | afs2.SaveToFile(filePath2); 99 | } 100 | 101 | using (FileStream stream2 = File.OpenRead(filePath2)) 102 | { 103 | hash2 = md5.ComputeHash(stream2); 104 | } 105 | 106 | Assert.IsTrue(CompareHashes(hash1, hash2), $"File \"{TestFiles[tf]}\" was not regenerated correctly."); 107 | 108 | File.Delete(filePath2); 109 | Directory.Delete(extractionDirectory, true); 110 | } 111 | } 112 | } 113 | 114 | static bool CompareHashes(byte[] hash1, byte[] hash2) 115 | { 116 | if (hash1.Length != hash2.Length) return false; 117 | 118 | for (int h = 0; h < hash1.Length; h++) 119 | { 120 | if (hash1[h] != hash2[h]) return false; 121 | } 122 | 123 | return true; 124 | } 125 | } 126 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AFSLib 2 | 3 | ![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/MaikelChan/AFSLib?sort=semver&style=for-the-badge) 4 | ![Nuget](https://img.shields.io/nuget/v/MaikelChan.AFSLib?color=blueviolet&style=for-the-badge) 5 | ![GitHub commits since latest release (by SemVer)](https://img.shields.io/github/commits-since/MaikelChan/AFSLib/latest?color=orange&sort=semver&style=for-the-badge) 6 | ![GitHub](https://img.shields.io/github/license/MaikelChan/AFSLib?style=for-the-badge) 7 | 8 | AFSLib is a library that can extract, create and manipulate AFS files. The AFS format is used in many games from companies like Sega. Even though it's a simple format, there are lots of quirks and edge cases in many games that require implementing specific fixes or workarounds for the library to work with them. If you encounter any issue with a specific game, don't hesitate to report it. 9 | 10 | For a usage example, you can check [AFSPacker](https://github.com/MaikelChan/AFSPacker), a simple command line tool that uses AFSLib to extract and create AFS files. 11 | 12 | ## Usage examples 13 | 14 | In order to use this examples, make sure you are using AFSLib. 15 | 16 | ```cs 17 | using AFSLib; 18 | ``` 19 | 20 | ### Create an AFS archive from scratch with some files 21 | 22 | ```cs 23 | // Create a new AFS object. 24 | 25 | using (AFS afs = new AFS()) 26 | { 27 | // Add two files. The first parameter is the path to the file. 28 | // By default, an entry will be created with the same name as the file. 29 | // But you can also set a different entry name with the second parameter. 30 | 31 | afs.AddEntryFromFile(@"C:\Data\Files\MyTextFile.txt"); 32 | afs.AddEntryFromFile(@"C:\Data\Files\MyImageFile.png", "Title.png"); 33 | 34 | // Finally save the AFS archive to the specified file. 35 | 36 | afs.SaveToFile(@"C:\Data\MyArchive.afs"); 37 | } 38 | ``` 39 | 40 | ### Create an AFS archive out of all the files inside a directory 41 | 42 | ```cs 43 | // Create a new AFS object. 44 | 45 | using (AFS afs = new AFS()) 46 | { 47 | // Add all the files located in the directory C:\Data\Files. 48 | 49 | afs.AddEntriesFromDirectory(@"C:\Data\Files"); 50 | 51 | // Finally save the AFS archive to the specified file. 52 | 53 | afs.SaveToFile(@"C:\Data\MyArchive.afs"); 54 | } 55 | ``` 56 | 57 | ### Extract all the entries from an existing AFS archive 58 | 59 | ```cs 60 | // Create an AFS object out of an existing AFS archive. 61 | 62 | using (AFS afs = new AFS(@"C:\Data\MyArchive.afs")) 63 | { 64 | // Extract all the entries to the specified directory. 65 | 66 | afs.ExtractAllEntriesToDirectory(@"C:\Data\Files"); 67 | } 68 | ``` 69 | 70 | ### Change some entries properties and save them in another AFS archive 71 | 72 | ```cs 73 | // Create an AFS object out of an existing AFS archive. 74 | 75 | using (AFS afs = new AFS(@"C:\Data\MyArchive.afs")) 76 | { 77 | // To change some properties, first we need to be sure that we 78 | // are trying to change an entry with data, and not a null entry. 79 | // So cast to DataEntry, which is the parent class of all entries 80 | // that can contain data. 81 | 82 | DataEntry dataEntry = afs.Entries[0] as DataEntry; 83 | if (dataEntry != null) 84 | { 85 | // It seems that it's indeed an entry with data, 86 | // so let's change some stuff. 87 | 88 | dataEntry.Name = "NewFileName.txt"; 89 | dataEntry.LastWriteTime = DateTime.Now; 90 | } 91 | 92 | // Save again with a different name. 93 | 94 | afs.SaveToFile(@"C:\Data\MyNewArchive.afs"); 95 | } 96 | ``` 97 | 98 | ### Advanced AFS creation 99 | 100 | There are many variants of the AFS format found in many games. Some of those games may require to have a specific variant of AFS archive. There are several modifiable properties that allow to customize the archive so it matches what the game needs. To know what values are needed, read them from one of the AFS archives found in the game. 101 | 102 | ```cs 103 | // Some variables to store the properties 104 | 105 | HeaderMagicType header; 106 | AttributesInfoType attributesInfo; 107 | uint entryBlockAlignment; 108 | 109 | // Create an AFS object out of an existing AFS archive. 110 | 111 | using (AFS afs = new AFS(@"C:\Data\MyArchive.afs")) 112 | { 113 | // Read and store some properties 114 | 115 | header = afs.HeaderMagicType; 116 | attributesInfo = afs.AttributesInfoType; 117 | entryBlockAlignment = afs.EntryBlockAlignment; 118 | } 119 | 120 | // Create a new AFS object. 121 | 122 | using (AFS afs = new AFS()) 123 | { 124 | // Set the same properties to the new file 125 | 126 | afs.HeaderMagicType = header; 127 | afs.AttributesInfoType = attributesInfo; 128 | afs.EntryBlockAlignment = entryBlockAlignment; 129 | 130 | // Add some files 131 | 132 | afs.AddEntriesFromDirectory(@"C:\Data\Files"); 133 | 134 | // Finally save the AFS archive to the specified file. 135 | 136 | afs.SaveToFile(@"C:\Data\MyArchive.afs"); 137 | } 138 | ``` 139 | 140 | ### Get and print progress information 141 | 142 | ```cs 143 | // Create an AFS object out of an existing AFS archive. 144 | 145 | using (AFS afs = new AFS(@"C:\Data\MyArchive.afs")) 146 | { 147 | // Subscribe to the event so the extraction process will send info about its progress 148 | // and can be printed with the method Afs_NotifyProgress. 149 | 150 | afs.NotifyProgress += Afs_NotifyProgress; 151 | 152 | // Extract all the entries to the specified directory. 153 | 154 | afs.ExtractAllEntriesToDirectory(@"C:\Data\Files"); 155 | } 156 | 157 | static void Afs_NotifyProgress(NotificationType type, string message) 158 | { 159 | Console.WriteLine($"[{type}] - {message}"); 160 | } 161 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /AFSLib/AFS.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.IO; 5 | using System.Reflection; 6 | using System.Text; 7 | 8 | namespace AFSLib 9 | { 10 | /// 11 | /// Class that represents one AFS archive. It can be created from scratch or loaded from a file or stream. 12 | /// 13 | public class AFS : IDisposable 14 | { 15 | /// 16 | /// Each of the entries in the AFS object. 17 | /// 18 | public ReadOnlyCollection Entries => readonlyEntries; 19 | 20 | /// 21 | /// The header magic that the AFS file will have. 22 | /// 23 | public HeaderMagicType HeaderMagicType { get; set; } 24 | 25 | /// 26 | /// The location where the attributes info will be stored. Or if the file won't contain any attributes. 27 | /// 28 | public AttributesInfoType AttributesInfoType { get; set; } 29 | 30 | /// 31 | /// The amount of bytes the entry block will be aligned to. Minimum accepted value is 2048, which is what most games use. But some games have bigger values. 32 | /// 33 | public uint EntryBlockAlignment { get => entryBlockAlignment; set => entryBlockAlignment = Math.Max(value, MIN_ENTRY_BLOCK_ALIGNMENT_SIZE); } 34 | uint entryBlockAlignment; 35 | 36 | /// 37 | /// The amount of entries in this AFS object. 38 | /// 39 | public uint EntryCount => (uint)entries.Count; 40 | 41 | /// 42 | /// If the AFS object contains attributes or not. It will be false if AttributesInfoType == AttributesInfoType.NoAttributes. 43 | /// 44 | public bool ContainsAttributes => AttributesInfoType != AttributesInfoType.NoAttributes; 45 | 46 | /// 47 | /// Returns true if all attributes contain the size of the entry. 48 | /// Some AFS archives contain each entry's size in their attributes, while others contain custom developer data. 49 | /// To access this data use the CustomData property of the Entry. 50 | /// 51 | public bool AllAttributesContainEntrySize => DoAllAttributesContainEntrySize(); 52 | 53 | /// 54 | /// Event that will be called each time some process wants to report something. 55 | /// 56 | public event NotifyProgressDelegate NotifyProgress; 57 | 58 | /// 59 | /// Represents the method that will handle the NotifyProgress event. 60 | /// 61 | /// Type of notification. 62 | /// The notification message. 63 | public delegate void NotifyProgressDelegate(NotificationType type, string message); 64 | 65 | internal const uint HEADER_MAGIC_00 = 0x00534641; // AFS 66 | internal const uint HEADER_MAGIC_20 = 0x20534641; 67 | internal const uint HEADER_SIZE = 0x8; 68 | internal const uint ENTRY_INFO_ELEMENT_SIZE = 0x8; 69 | internal const uint ATTRIBUTE_INFO_SIZE = 0x8; 70 | internal const uint ATTRIBUTE_ELEMENT_SIZE = 0x30; 71 | internal const uint MAX_ENTRY_NAME_LENGTH = 0x20; 72 | internal const uint MIN_ENTRY_BLOCK_ALIGNMENT_SIZE = 0x800; 73 | internal const uint ALIGNMENT_SIZE = 0x800; 74 | 75 | internal const string DUMMY_ENTRY_NAME_FOR_BLANK_NAME = "_NO_NAME"; 76 | 77 | private readonly List entries; 78 | private readonly ReadOnlyCollection readonlyEntries; 79 | 80 | private Stream afsStream; 81 | private bool leaveAfsStreamOpen; 82 | 83 | private bool isDisposed; 84 | 85 | /// 86 | /// Create an empty AFS object. 87 | /// 88 | public AFS() 89 | { 90 | entries = new List(); 91 | readonlyEntries = entries.AsReadOnly(); 92 | duplicates = new Dictionary(); 93 | 94 | HeaderMagicType = HeaderMagicType.AFS_00; 95 | AttributesInfoType = AttributesInfoType.InfoAtBeginning; 96 | EntryBlockAlignment = 0x800; 97 | 98 | afsStream = null; 99 | leaveAfsStreamOpen = true; 100 | 101 | isDisposed = false; 102 | } 103 | 104 | /// 105 | /// Create an AFS object out of a stream. The stream will need to remain open until the AFS object is disposed, as it will need to access the stream's data during various operations. 106 | /// 107 | /// Stream containing the AFS file data. 108 | public AFS(Stream afsStream) : this() 109 | { 110 | if (afsStream == null) 111 | { 112 | throw new ArgumentNullException(nameof(afsStream)); 113 | } 114 | 115 | LoadFromStream(afsStream); 116 | leaveAfsStreamOpen = true; 117 | } 118 | 119 | /// 120 | /// Create an AFS object out of a file. The file will remain open until the AFS object is disposed. 121 | /// 122 | /// Path to the AFS file containing the data. 123 | public AFS(string afsFilePath) : this() 124 | { 125 | if (string.IsNullOrEmpty(afsFilePath)) 126 | { 127 | throw new ArgumentNullException(nameof(afsFilePath)); 128 | } 129 | 130 | if (!File.Exists(afsFilePath)) 131 | { 132 | throw new FileNotFoundException($"File \"{afsFilePath}\" has not been found.", afsFilePath); 133 | } 134 | 135 | LoadFromStream(File.OpenRead(afsFilePath)); 136 | leaveAfsStreamOpen = false; 137 | } 138 | 139 | /// 140 | /// Dispose the AFS object. 141 | /// 142 | public void Dispose() 143 | { 144 | CheckDisposed(); 145 | 146 | if (afsStream != null && !leaveAfsStreamOpen) 147 | { 148 | afsStream.Dispose(); 149 | afsStream = null; 150 | leaveAfsStreamOpen = true; 151 | } 152 | 153 | entries.Clear(); 154 | duplicates.Clear(); 155 | 156 | isDisposed = true; 157 | } 158 | 159 | /// 160 | /// Saves the contents of this AFS object into a file. 161 | /// 162 | /// The path to the file where the data is going to be saved. 163 | public void SaveToFile(string outputFilePath) 164 | { 165 | if (string.IsNullOrEmpty(outputFilePath)) 166 | { 167 | throw new ArgumentNullException(nameof(outputFilePath)); 168 | } 169 | 170 | using (FileStream outputStream = File.Create(outputFilePath)) 171 | { 172 | SaveToStream(outputStream); 173 | } 174 | } 175 | 176 | /// 177 | /// Saves the contents of this AFS object into a stream. 178 | /// 179 | /// The stream where the data is going to be saved. 180 | public void SaveToStream(Stream outputStream) 181 | { 182 | CheckDisposed(); 183 | 184 | if (outputStream == null) 185 | { 186 | throw new ArgumentNullException(nameof(outputStream)); 187 | } 188 | 189 | if (outputStream == afsStream) 190 | { 191 | throw new ArgumentException("Can't save into the same stream the AFS data is being read from.", nameof(outputStream)); 192 | } 193 | 194 | // Start creating the AFS file 195 | 196 | NotifyProgress?.Invoke(NotificationType.Info, "Creating AFS stream..."); 197 | 198 | using (BinaryWriter bw = new BinaryWriter(outputStream)) 199 | { 200 | bw.Write(HeaderMagicType == HeaderMagicType.AFS_20 ? HEADER_MAGIC_20 : HEADER_MAGIC_00); 201 | bw.Write(EntryCount); 202 | 203 | // Calculate the offset of each entry 204 | 205 | uint[] offsets = new uint[EntryCount]; 206 | 207 | uint firstEntryOffset = Utils.Pad(HEADER_SIZE + (ENTRY_INFO_ELEMENT_SIZE * EntryCount) + ATTRIBUTE_INFO_SIZE, EntryBlockAlignment); 208 | uint currentEntryOffset = firstEntryOffset; 209 | 210 | for (int e = 0; e < EntryCount; e++) 211 | { 212 | if (entries[e] is NullEntry) 213 | { 214 | offsets[e] = 0; 215 | } 216 | else 217 | { 218 | offsets[e] = currentEntryOffset; 219 | 220 | DataEntry dataEntry = entries[e] as DataEntry; 221 | currentEntryOffset += dataEntry.Size; 222 | currentEntryOffset = Utils.Pad(currentEntryOffset, ALIGNMENT_SIZE); 223 | } 224 | } 225 | 226 | // Write entries info 227 | 228 | for (int e = 0; e < EntryCount; e++) 229 | { 230 | NotifyProgress?.Invoke(NotificationType.Info, $"Writing entry info... {e + 1}/{EntryCount}"); 231 | 232 | if (entries[e] is NullEntry) 233 | { 234 | bw.Write((uint)0); 235 | bw.Write((uint)0); 236 | } 237 | else 238 | { 239 | DataEntry dataEntry = entries[e] as DataEntry; 240 | 241 | bw.Write(offsets[e]); 242 | bw.Write(dataEntry.Size); 243 | } 244 | } 245 | 246 | // Write attributes info if available 247 | 248 | outputStream.Position = HEADER_SIZE + (EntryCount * ENTRY_INFO_ELEMENT_SIZE); 249 | Utils.FillStreamWithZeroes(outputStream, firstEntryOffset - (uint)outputStream.Position); 250 | 251 | uint attributesInfoOffset = currentEntryOffset; 252 | 253 | if (ContainsAttributes) 254 | { 255 | if (AttributesInfoType == AttributesInfoType.InfoAtBeginning) 256 | outputStream.Position = HEADER_SIZE + (EntryCount * ENTRY_INFO_ELEMENT_SIZE); 257 | else if (AttributesInfoType == AttributesInfoType.InfoAtEnd) 258 | outputStream.Position = firstEntryOffset - ATTRIBUTE_INFO_SIZE; 259 | 260 | bw.Write(attributesInfoOffset); 261 | bw.Write(EntryCount * ATTRIBUTE_ELEMENT_SIZE); 262 | } 263 | 264 | // Write entries data to stream 265 | 266 | for (int e = 0; e < EntryCount; e++) 267 | { 268 | if (entries[e] is NullEntry) 269 | { 270 | NotifyProgress?.Invoke(NotificationType.Info, $"Null file... {e + 1}/{EntryCount}"); 271 | } 272 | else 273 | { 274 | NotifyProgress?.Invoke(NotificationType.Info, $"Writing entry... {e + 1}/{EntryCount}"); 275 | 276 | outputStream.Position = offsets[e]; 277 | 278 | using (Stream entryStream = entries[e].GetStream()) 279 | { 280 | entryStream.CopyTo(outputStream); 281 | } 282 | } 283 | } 284 | 285 | // Write attributes if available 286 | 287 | if (ContainsAttributes) 288 | { 289 | outputStream.Position = attributesInfoOffset; 290 | 291 | for (int e = 0; e < EntryCount; e++) 292 | { 293 | if (entries[e] is NullEntry) 294 | { 295 | NotifyProgress?.Invoke(NotificationType.Info, $"Null file... {e + 1}/{EntryCount}"); 296 | 297 | outputStream.Position += ATTRIBUTE_ELEMENT_SIZE; 298 | } 299 | else 300 | { 301 | NotifyProgress?.Invoke(NotificationType.Info, $"Writing attribute... {e + 1}/{EntryCount}"); 302 | 303 | DataEntry dataEntry = entries[e] as DataEntry; 304 | 305 | byte[] name = Encoding.Default.GetBytes(dataEntry.Name); 306 | outputStream.Write(name, 0, name.Length); 307 | outputStream.Position += MAX_ENTRY_NAME_LENGTH - name.Length; 308 | 309 | bw.Write((ushort)dataEntry.LastWriteTime.Year); 310 | bw.Write((ushort)dataEntry.LastWriteTime.Month); 311 | bw.Write((ushort)dataEntry.LastWriteTime.Day); 312 | bw.Write((ushort)dataEntry.LastWriteTime.Hour); 313 | bw.Write((ushort)dataEntry.LastWriteTime.Minute); 314 | bw.Write((ushort)dataEntry.LastWriteTime.Second); 315 | bw.Write(dataEntry.CustomData); 316 | } 317 | } 318 | } 319 | 320 | // Pad final zeroes 321 | 322 | uint currentPosition = (uint)outputStream.Position; 323 | uint endOfFile = Utils.Pad(currentPosition, ALIGNMENT_SIZE); 324 | Utils.FillStreamWithZeroes(outputStream, endOfFile - currentPosition); 325 | 326 | // Make sure the stream is the size of the AFS data (in case the stream was bigger) 327 | 328 | outputStream.SetLength(endOfFile); 329 | } 330 | 331 | NotifyProgress?.Invoke(NotificationType.Success, "AFS stream has been saved successfully."); 332 | } 333 | 334 | /// 335 | /// Adds a new entry from a file. 336 | /// 337 | /// Path to the file that will be added. 338 | /// The name of the entry. If null, it will be the name of the file in fileNamePath. 339 | /// A reference to the added entry. 340 | public FileEntry AddEntryFromFile(string filePath, string entryName = null) 341 | { 342 | CheckDisposed(); 343 | 344 | if (string.IsNullOrEmpty(filePath)) 345 | { 346 | throw new ArgumentNullException(nameof(filePath)); 347 | } 348 | 349 | if (!File.Exists(filePath)) 350 | { 351 | throw new FileNotFoundException($"File \"{filePath}\" has not been found.", filePath); 352 | } 353 | 354 | if (entryName == null) 355 | { 356 | entryName = Path.GetFileName(filePath); 357 | } 358 | 359 | FileEntry fileEntry = new FileEntry(filePath, entryName); 360 | Internal_AddEntry(fileEntry); 361 | UpdateSanitizedEntriesNames(); 362 | 363 | return fileEntry; 364 | } 365 | 366 | /// 367 | /// Adds a new entry from a stream. 368 | /// 369 | /// Stream that contains the file that will be added. 370 | /// The name of the entry. If null, it will be considered as string.Empty. 371 | /// A reference to the added entry. 372 | public StreamEntry AddEntryFromStream(Stream entryStream, string entryName) 373 | { 374 | CheckDisposed(); 375 | 376 | if (entryStream == null) 377 | { 378 | throw new ArgumentNullException(nameof(entryStream)); 379 | } 380 | 381 | if (entryName == null) 382 | { 383 | entryName = string.Empty; 384 | } 385 | 386 | StreamEntryInfo info = new StreamEntryInfo() 387 | { 388 | Offset = 0, 389 | Name = entryName, 390 | Size = (uint)entryStream.Length, 391 | LastWriteTime = DateTime.Now, 392 | CustomData = (uint)entryStream.Length 393 | }; 394 | 395 | StreamEntry streamEntry = new StreamEntry(entryStream, info); 396 | Internal_AddEntry(streamEntry); 397 | UpdateSanitizedEntriesNames(); 398 | 399 | return streamEntry; 400 | } 401 | 402 | /// 403 | /// Adds a new null entry. 404 | /// 405 | /// A reference to the added entry. 406 | public NullEntry AddNullEntry() 407 | { 408 | CheckDisposed(); 409 | 410 | NullEntry nullEntry = new NullEntry(); 411 | Internal_AddEntry(nullEntry); 412 | UpdateSanitizedEntriesNames(); 413 | 414 | return nullEntry; 415 | } 416 | 417 | /// 418 | /// Adds all files in the specified directory to the AFS object. 419 | /// 420 | /// The path to the directory. 421 | /// When true, it adds all files in the specified directory and its subdirectories. When false, it ignores any subdirectories. 422 | public void AddEntriesFromDirectory(string directory, bool recursiveSearch = false) 423 | { 424 | CheckDisposed(); 425 | 426 | if (string.IsNullOrEmpty(directory)) 427 | { 428 | throw new ArgumentNullException(nameof(directory)); 429 | } 430 | 431 | if (!Directory.Exists(directory)) 432 | { 433 | throw new DirectoryNotFoundException($"Directory \"{directory}\" has not been found."); 434 | } 435 | 436 | string[] files = Directory.GetFiles(directory, "*.*", recursiveSearch ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); 437 | 438 | for (int f = 0; f < files.Length; f++) 439 | { 440 | string entryName = Path.GetFileName(files[f]); 441 | Entry entry = new FileEntry(files[f], entryName); 442 | Internal_AddEntry(entry); 443 | } 444 | 445 | UpdateSanitizedEntriesNames(); 446 | } 447 | 448 | /// 449 | /// Removes an entry from the AFS object. 450 | /// 451 | /// The entry to remove. 452 | public void RemoveEntry(Entry entry) 453 | { 454 | CheckDisposed(); 455 | 456 | if (entry == null) 457 | { 458 | throw new ArgumentNullException(nameof(entry)); 459 | } 460 | 461 | if (entries.Contains(entry)) 462 | { 463 | Internal_RemoveEntry(entry); 464 | UpdateSanitizedEntriesNames(); 465 | } 466 | } 467 | 468 | /// 469 | /// Extracts one entry to a file. 470 | /// 471 | /// The entry to extract. 472 | /// The path to the file where the entry will be saved. If it doesn't exist, it will be created. 473 | public void ExtractEntryToFile(Entry entry, string outputFilePath) 474 | { 475 | CheckDisposed(); 476 | 477 | if (entry == null) 478 | { 479 | throw new ArgumentNullException(nameof(entry)); 480 | } 481 | 482 | if (entry is NullEntry) 483 | { 484 | NotifyProgress?.Invoke(NotificationType.Warning, $"Trying to extract a null entry. Ignored."); 485 | return; 486 | } 487 | 488 | if (string.IsNullOrEmpty(outputFilePath)) 489 | { 490 | throw new ArgumentNullException(nameof(outputFilePath)); 491 | } 492 | 493 | string directory = Path.GetDirectoryName(outputFilePath); 494 | if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) 495 | { 496 | Directory.CreateDirectory(directory); 497 | } 498 | 499 | using (FileStream outputStream = File.Create(outputFilePath)) 500 | using (Stream entryStream = entry.GetStream()) 501 | { 502 | entryStream.CopyTo(outputStream); 503 | } 504 | 505 | if (ContainsAttributes) 506 | { 507 | DataEntry dataEntry = entry as DataEntry; 508 | 509 | try 510 | { 511 | File.SetLastWriteTime(outputFilePath, dataEntry.LastWriteTime); 512 | } 513 | catch (ArgumentOutOfRangeException) 514 | { 515 | File.SetLastWriteTime(outputFilePath, DateTime.Now); 516 | NotifyProgress?.Invoke(NotificationType.Warning, "Invalid date/time. Setting current date/time."); 517 | } 518 | } 519 | } 520 | 521 | /// 522 | /// Extracts all the entries from the AFS object. 523 | /// 524 | /// The directory where the entries will be saved. If it doesn't exist, it will be created. 525 | public void ExtractAllEntriesToDirectory(string outputDirectory) 526 | { 527 | CheckDisposed(); 528 | 529 | if (string.IsNullOrEmpty(outputDirectory)) 530 | { 531 | throw new ArgumentNullException(nameof(outputDirectory)); 532 | } 533 | 534 | if (!Directory.Exists(outputDirectory)) Directory.CreateDirectory(outputDirectory); 535 | 536 | for (int e = 0; e < EntryCount; e++) 537 | { 538 | if (entries[e] is NullEntry) 539 | { 540 | NotifyProgress?.Invoke(NotificationType.Warning, $"Null entry. Skipping... {e + 1}/{EntryCount}"); 541 | continue; 542 | } 543 | 544 | NotifyProgress?.Invoke(NotificationType.Info, $"Extracting entry... {e + 1}/{EntryCount}"); 545 | 546 | DataEntry dataEntry = entries[e] as DataEntry; 547 | 548 | string outputFilePath = Path.Combine(outputDirectory, dataEntry.SanitizedName); 549 | if (File.Exists(outputFilePath)) 550 | { 551 | NotifyProgress?.Invoke(NotificationType.Warning, $"File \"{outputFilePath}\" already exists. Overwriting..."); 552 | } 553 | 554 | ExtractEntryToFile(entries[e], outputFilePath); 555 | } 556 | 557 | NotifyProgress?.Invoke(NotificationType.Success, $"Finished extracting all entries successfully."); 558 | } 559 | 560 | private void LoadFromStream(Stream afsStream) 561 | { 562 | this.afsStream = afsStream; 563 | 564 | using (BinaryReader br = new BinaryReader(afsStream, Encoding.UTF8, true)) 565 | { 566 | // Check if the Magic is valid 567 | 568 | uint magic = br.ReadUInt32(); 569 | 570 | if (magic == HEADER_MAGIC_00) 571 | { 572 | HeaderMagicType = HeaderMagicType.AFS_00; 573 | } 574 | else if (magic == HEADER_MAGIC_20) 575 | { 576 | HeaderMagicType = HeaderMagicType.AFS_20; 577 | } 578 | else 579 | { 580 | throw new InvalidDataException("Stream doesn't seem to contain valid AFS data."); 581 | } 582 | 583 | // Start gathering info about entries and attributes 584 | 585 | uint entryCount = br.ReadUInt32(); 586 | StreamEntryInfo[] entriesInfo = new StreamEntryInfo[entryCount]; 587 | 588 | uint entryBlockStartOffset = 0; 589 | uint entryBlockEndOffset = 0; 590 | 591 | for (int e = 0; e < entryCount; e++) 592 | { 593 | entriesInfo[e].Offset = br.ReadUInt32(); 594 | entriesInfo[e].Size = br.ReadUInt32(); 595 | 596 | if (entriesInfo[e].IsNull) 597 | { 598 | continue; 599 | } 600 | 601 | if (entryBlockStartOffset == 0) entryBlockStartOffset = entriesInfo[e].Offset; 602 | entryBlockEndOffset = entriesInfo[e].Offset + entriesInfo[e].Size; 603 | } 604 | 605 | // Calculate the entry block alignment 606 | 607 | uint alignment = MIN_ENTRY_BLOCK_ALIGNMENT_SIZE; 608 | uint endInfoBlockOffset = (uint)afsStream.Position + ATTRIBUTE_INFO_SIZE; 609 | while (endInfoBlockOffset + alignment < entryBlockStartOffset) alignment <<= 1; 610 | EntryBlockAlignment = alignment; 611 | 612 | // Find where attribute info is located 613 | 614 | AttributesInfoType = AttributesInfoType.NoAttributes; 615 | 616 | uint attributeDataOffset = br.ReadUInt32(); 617 | uint attributeDataSize = br.ReadUInt32(); 618 | 619 | bool isAttributeInfoValid = IsAttributeInfoValid(attributeDataOffset, attributeDataSize, (uint)afsStream.Length, entryBlockEndOffset); 620 | 621 | if (isAttributeInfoValid) 622 | { 623 | AttributesInfoType = AttributesInfoType.InfoAtBeginning; 624 | } 625 | else 626 | { 627 | afsStream.Position = entryBlockStartOffset - ATTRIBUTE_INFO_SIZE; 628 | attributeDataOffset = br.ReadUInt32(); 629 | attributeDataSize = br.ReadUInt32(); 630 | 631 | isAttributeInfoValid = IsAttributeInfoValid(attributeDataOffset, attributeDataSize, (uint)afsStream.Length, entryBlockEndOffset); 632 | 633 | if (isAttributeInfoValid) 634 | { 635 | AttributesInfoType = AttributesInfoType.InfoAtEnd; 636 | } 637 | } 638 | 639 | // Read attribute data if there is any 640 | 641 | if (ContainsAttributes) 642 | { 643 | afsStream.Position = attributeDataOffset; 644 | 645 | for (int e = 0; e < entryCount; e++) 646 | { 647 | if (entriesInfo[e].IsNull) 648 | { 649 | // It's a null entry, so ignore attribute data 650 | 651 | afsStream.Position += ATTRIBUTE_ELEMENT_SIZE; 652 | 653 | continue; 654 | } 655 | else 656 | { 657 | // It's a valid entry, so read attribute data 658 | 659 | byte[] name = new byte[MAX_ENTRY_NAME_LENGTH]; 660 | afsStream.Read(name, 0, name.Length); 661 | 662 | entriesInfo[e].Name = Utils.GetStringFromBytes(name); 663 | 664 | try 665 | { 666 | entriesInfo[e].LastWriteTime = new DateTime(br.ReadUInt16(), br.ReadUInt16(), br.ReadUInt16(), br.ReadUInt16(), br.ReadUInt16(), br.ReadUInt16()); 667 | } 668 | catch (ArgumentOutOfRangeException) 669 | { 670 | entriesInfo[e].LastWriteTime = default; 671 | NotifyProgress?.Invoke(NotificationType.Warning, "Invalid date/time. Ignoring."); 672 | } 673 | 674 | entriesInfo[e].CustomData = br.ReadUInt32(); 675 | } 676 | } 677 | } 678 | else 679 | { 680 | for (int e = 0; e < entryCount; e++) 681 | { 682 | entriesInfo[e].Name = $"{e:00000000}"; 683 | } 684 | } 685 | 686 | // After gathering all necessary info, create the entries. 687 | 688 | for (int e = 0; e < entryCount; e++) 689 | { 690 | Entry entry; 691 | if (entriesInfo[e].IsNull) entry = new NullEntry(); 692 | else entry = new StreamEntry(afsStream, entriesInfo[e]); 693 | Internal_AddEntry(entry); 694 | } 695 | 696 | UpdateSanitizedEntriesNames(); 697 | } 698 | } 699 | 700 | private void Internal_AddEntry(Entry entry) 701 | { 702 | entries.Add(entry); 703 | 704 | DataEntry dataEntry = entry as DataEntry; 705 | if (dataEntry != null) 706 | { 707 | dataEntry.NameChanged += UpdateSanitizedEntriesNames; 708 | } 709 | } 710 | 711 | private void Internal_RemoveEntry(Entry entry) 712 | { 713 | DataEntry dataEntry = entry as DataEntry; 714 | if (dataEntry != null) 715 | { 716 | dataEntry.NameChanged -= UpdateSanitizedEntriesNames; 717 | } 718 | 719 | entries.Remove(entry); 720 | } 721 | 722 | private bool IsAttributeInfoValid(uint attributesOffset, uint attributesSize, uint afsFileSize, uint dataBlockEndOffset) 723 | { 724 | // If zeroes are found, info is not valid. 725 | if (attributesOffset == 0) return false; 726 | if (attributesSize == 0) return false; 727 | 728 | // Check if this info makes sense, as there are times where random 729 | // data can be found instead of attribute offset and size. 730 | if (attributesSize > afsFileSize - dataBlockEndOffset) return false; 731 | if (attributesSize < EntryCount * ATTRIBUTE_ELEMENT_SIZE) return false; 732 | if (attributesOffset < dataBlockEndOffset) return false; 733 | if (attributesOffset > afsFileSize - attributesSize) return false; 734 | 735 | // If the above conditions are not met, it looks like it's valid attribute info 736 | return true; 737 | } 738 | 739 | private bool DoAllAttributesContainEntrySize() 740 | { 741 | if (!ContainsAttributes) return false; 742 | 743 | for (int e = 0; e < EntryCount; e++) 744 | { 745 | if (entries[e] is NullEntry) continue; 746 | 747 | DataEntry dataEntry = entries[e] as DataEntry; 748 | 749 | if (!dataEntry.CustomDataContainsSize) return false; 750 | } 751 | 752 | return true; 753 | } 754 | 755 | private void CheckDisposed() 756 | { 757 | if (isDisposed) throw new ObjectDisposedException(GetType().Name); 758 | } 759 | 760 | #region Name sanitization 761 | 762 | private readonly Dictionary duplicates; 763 | 764 | private void UpdateSanitizedEntriesNames() 765 | { 766 | // There can be multiple files with the same name, so keep track of duplicates 767 | 768 | duplicates.Clear(); 769 | 770 | for (int e = 0; e < EntryCount; e++) 771 | { 772 | if (entries[e] is NullEntry) continue; 773 | 774 | DataEntry dataEntry = entries[e] as DataEntry; 775 | 776 | string sanitizedName = SanitizeName(dataEntry.Name); 777 | 778 | bool found = duplicates.TryGetValue(sanitizedName, out uint duplicateCount); 779 | 780 | if (found) duplicates[sanitizedName] = ++duplicateCount; 781 | else duplicates.Add(sanitizedName, 0); 782 | 783 | if (duplicateCount > 0) 784 | { 785 | string nameWithoutExtension = Path.ChangeExtension(sanitizedName, null); 786 | string nameDuplicate = $" ({duplicateCount})"; 787 | string nameExtension = Path.GetExtension(sanitizedName); 788 | 789 | sanitizedName = nameWithoutExtension + nameDuplicate + nameExtension; 790 | } 791 | 792 | dataEntry.SanitizedName = sanitizedName; 793 | } 794 | } 795 | 796 | private static string SanitizeName(string name) 797 | { 798 | if (string.IsNullOrWhiteSpace(name)) 799 | { 800 | // The game "Winback 2: Project Poseidon" has attributes with empty file names. 801 | // Give the files a dummy name for them to extract properly. 802 | return DUMMY_ENTRY_NAME_FOR_BLANK_NAME; 803 | } 804 | 805 | // There are some cases where instead of a file name, an AFS file will store a path, like in Soul Calibur 2 or Crimson Tears. 806 | // Let's make sure there aren't any invalid characters in the path so the OS doesn't complain. 807 | 808 | string sanitizedName = name; 809 | 810 | for (int ipc = 0; ipc < invalidPathChars.Length; ipc++) 811 | { 812 | sanitizedName = sanitizedName.Replace(invalidPathChars[ipc], string.Empty); 813 | } 814 | 815 | // Also remove any ":" in case there are drive letters in the path (like, again, in Soul Calibur 2) 816 | 817 | sanitizedName = sanitizedName.Replace(":", string.Empty); 818 | 819 | return sanitizedName; 820 | } 821 | 822 | #endregion 823 | 824 | #region Statics 825 | 826 | private static readonly string[] invalidPathChars; 827 | private static readonly string[] invalidFileNameChars; 828 | 829 | static AFS() 830 | { 831 | char[] pChars = Path.GetInvalidPathChars(); 832 | char[] fChars = Path.GetInvalidFileNameChars(); 833 | 834 | invalidPathChars = new string[pChars.Length]; 835 | for (int ipc = 0; ipc < pChars.Length; ipc++) 836 | { 837 | invalidPathChars[ipc] = pChars[ipc].ToString(); 838 | } 839 | 840 | invalidFileNameChars = new string[fChars.Length]; 841 | for (int ifc = 0; ifc < fChars.Length; ifc++) 842 | { 843 | invalidFileNameChars[ifc] = fChars[ifc].ToString(); 844 | } 845 | } 846 | 847 | /// 848 | /// Get the version of AFSLib. 849 | /// 850 | public static Version GetVersion() 851 | { 852 | return Assembly.GetExecutingAssembly().GetName().Version; 853 | } 854 | 855 | #endregion 856 | 857 | #region Deprecated 858 | 859 | /// 860 | /// Adds a new entry from a file. 861 | /// 862 | /// The name of the entry. 863 | /// Path to the file that will be added. 864 | [Obsolete("This method is deprecated since version 2.0.0, please use AddEntryFromFile(string, string) instead.")] 865 | public void AddEntry(string entryName, string fileNamePath) 866 | { 867 | AddEntryFromFile(fileNamePath, entryName); 868 | } 869 | 870 | /// 871 | /// Adds a new entry from a stream. 872 | /// 873 | /// The name of the entry. 874 | /// Stream that contains the file that will be added. 875 | [Obsolete("This method is deprecated since version 2.0.0, please use AddEntryFromStream(Stream, string) instead.")] 876 | public void AddEntry(string entryName, Stream entryStream) 877 | { 878 | AddEntryFromStream(entryStream, entryName); 879 | } 880 | 881 | /// 882 | /// Extracts one entry to a file. 883 | /// 884 | /// The entry to extract. 885 | /// The path to the file where the entry will be saved. If it doesn't exist, it will be created. 886 | [Obsolete("This method is deprecated since version 2.0.0, please use ExtractEntryToFile(Entry, string) instead.")] 887 | public void ExtractEntry(Entry entry, string outputFilePath) 888 | { 889 | ExtractEntryToFile(entry, outputFilePath); 890 | } 891 | 892 | /// 893 | /// Extracts all the entries from the AFS object. 894 | /// 895 | /// The directory where the entries will be saved. If it doesn't exist, it will be created. 896 | [Obsolete("This method is deprecated since version 2.0.0, please use ExtractAllEntriesToDirectory(string) instead.")] 897 | public void ExtractAllEntries(string outputDirectory) 898 | { 899 | ExtractAllEntriesToDirectory(outputDirectory); 900 | } 901 | 902 | #endregion 903 | } 904 | } --------------------------------------------------------------------------------