├── 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 | 
4 | 
5 | 
6 | 
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 | }
--------------------------------------------------------------------------------