diff --git a/BurnOutSharp.Builders/PFF.cs b/BurnOutSharp.Builders/PFF.cs
new file mode 100644
index 00000000..cf2b25d3
--- /dev/null
+++ b/BurnOutSharp.Builders/PFF.cs
@@ -0,0 +1,206 @@
+using System.IO;
+using System.Text;
+using BurnOutSharp.Models.PFF;
+using BurnOutSharp.Utilities;
+using static BurnOutSharp.Models.PFF.Constants;
+
+namespace BurnOutSharp.Builders
+{
+ public class PFF
+ {
+ #region Byte Data
+
+ ///
+ /// Parse a byte array into a PFF archive
+ ///
+ /// Byte array to parse
+ /// Offset into the byte array
+ /// Filled archive on success, null on error
+ public static Archive ParseArchive(byte[] data, int offset)
+ {
+ // If the data is invalid
+ if (data == null)
+ return null;
+
+ // If the offset is out of bounds
+ if (offset < 0 || offset >= data.Length)
+ return null;
+
+ // Create a memory stream and parse that
+ MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset);
+ return ParseArchive(dataStream);
+ }
+
+ #endregion
+
+ #region Stream Data
+
+ ///
+ /// Parse a Stream into a PFF archive
+ ///
+ /// Stream to parse
+ /// Filled archive on success, null on error
+ public static Archive ParseArchive(Stream data)
+ {
+ // If the data is invalid
+ if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead)
+ return null;
+
+ // If the offset is out of bounds
+ if (data.Position < 0 || data.Position >= data.Length)
+ return null;
+
+ // Cache the current offset
+ int initialOffset = (int)data.Position;
+
+ // Create a new archive to fill
+ var archive = new Archive();
+
+ #region Header
+
+ // Try to parse the header
+ var header = ParseHeader(data);
+ if (header == null)
+ return null;
+
+ // Set the archive header
+ archive.Header = header;
+
+ #endregion
+
+ #region Segments
+
+ // Get the segments
+ long offset = header.FileListOffset;
+ if (offset < 0 || offset >= data.Length)
+ return null;
+
+ // Seek to the segments
+ data.Seek(offset, SeekOrigin.Begin);
+
+ // Create the segments array
+ archive.Segments = new Segment[header.NumberOfFiles];
+
+ // Read all segments in turn
+ for (int i = 0; i < header.NumberOfFiles; i++)
+ {
+ var file = ParseSegment(data, header.FileSegmentSize);
+ if (file == null)
+ return null;
+
+ archive.Segments[i] = file;
+ }
+
+ #endregion
+
+ #region Footer
+
+ // Get the footer offset
+ offset = header.FileListOffset + (header.FileSegmentSize * header.NumberOfFiles);
+ if (offset < 0 || offset >= data.Length)
+ return null;
+
+ // Seek to the footer
+ data.Seek(offset, SeekOrigin.Begin);
+
+ // Try to parse the footer
+ var footer = ParseFooter(data);
+ if (footer == null)
+ return null;
+
+ // Set the archive footer
+ archive.Footer = footer;
+
+ #endregion
+
+ return archive;
+ }
+
+ ///
+ /// Parse a Stream into a header
+ ///
+ /// Stream to parse
+ /// Filled header on success, null on error
+ private static Header ParseHeader(Stream data)
+ {
+ // TODO: Use marshalling here instead of building
+ Header header = new Header();
+
+ header.HeaderSize = data.ReadUInt32();
+ byte[] signature = data.ReadBytes(4);
+ header.Signature = Encoding.ASCII.GetString(signature);
+ header.NumberOfFiles = data.ReadUInt32();
+ header.FileSegmentSize = data.ReadUInt32();
+ switch (header.Signature)
+ {
+ case Version2SignatureString:
+ if (header.FileSegmentSize != Version2SegmentSize)
+ return null;
+ break;
+
+ // Version 3 can sometimes have Version 2 segment sizes
+ case Version3SignatureString:
+ if (header.FileSegmentSize != Version2SegmentSize && header.FileSegmentSize != Version3SegmentSize)
+ return null;
+ break;
+
+ case Version4SignatureString:
+ if (header.FileSegmentSize != Version4SegmentSize)
+ return null;
+ break;
+
+ default:
+ return null;
+ }
+
+ header.FileListOffset = data.ReadUInt32();
+
+ return header;
+ }
+
+ ///
+ /// Parse a Stream into a footer
+ ///
+ /// Stream to parse
+ /// Filled footer on success, null on error
+ private static Footer ParseFooter(Stream data)
+ {
+ // TODO: Use marshalling here instead of building
+ Footer footer = new Footer();
+
+ footer.SystemIP = data.ReadUInt32();
+ footer.Reserved = data.ReadUInt32();
+ byte[] kingTag = data.ReadBytes(4);
+ footer.KingTag = Encoding.ASCII.GetString(kingTag);
+
+ return footer;
+ }
+
+ ///
+ /// Parse a Stream into a file entry
+ ///
+ /// Stream to parse
+ /// PFF segment size
+ /// Filled file entry on success, null on error
+ private static Segment ParseSegment(Stream data, uint segmentSize)
+ {
+ // TODO: Use marshalling here instead of building
+ Segment segment = new Segment();
+
+ segment.Deleted = data.ReadUInt32();
+ segment.FileLocation = data.ReadUInt32();
+ segment.FileSize = data.ReadUInt32();
+ segment.PackedDate = data.ReadUInt32();
+ byte[] fileName = data.ReadBytes(0x10);
+ segment.FileName = Encoding.ASCII.GetString(fileName).TrimEnd('\0');
+ if (segmentSize > Version2SegmentSize)
+ segment.ModifiedDate = data.ReadUInt32();
+ if (segmentSize > Version3SegmentSize)
+ segment.CompressionLevel = data.ReadUInt32();
+
+ return segment;
+ }
+
+ #endregion
+ }
+}
diff --git a/BurnOutSharp.Models/PFF/Archive.cs b/BurnOutSharp.Models/PFF/Archive.cs
new file mode 100644
index 00000000..3dc04e13
--- /dev/null
+++ b/BurnOutSharp.Models/PFF/Archive.cs
@@ -0,0 +1,24 @@
+namespace BurnOutSharp.Models.PFF
+{
+ ///
+ /// PFF archive
+ ///
+ ///
+ public sealed class Archive
+ {
+ ///
+ /// Archive header
+ ///
+ public Header Header { get; set; }
+
+ ///
+ /// Segments
+ ///
+ public Segment[] Segments { get; set; }
+
+ ///
+ /// Footer
+ ///
+ public Footer Footer { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/BurnOutSharp.Models/PFF/Constants.cs b/BurnOutSharp.Models/PFF/Constants.cs
new file mode 100644
index 00000000..3b43ae26
--- /dev/null
+++ b/BurnOutSharp.Models/PFF/Constants.cs
@@ -0,0 +1,21 @@
+namespace BurnOutSharp.Models.PFF
+{
+ ///
+ public static class Constants
+ {
+ // Version 1 not confirmed
+ // public const string Version1SignatureString = "PFF1";
+ // public const uint Version1HeaderSize = 0x00000000;
+
+ public const string Version2SignatureString = "PFF2";
+ public const uint Version2SegmentSize = 0x00000020;
+
+ public const string Version3SignatureString = "PFF3";
+ public const uint Version3SegmentSize = 0x00000024;
+
+ public const string Version4SignatureString = "PFF4";
+ public const uint Version4SegmentSize = 0x00000028;
+
+ public const string FooterKingTag = "KING";
+ }
+}
\ No newline at end of file
diff --git a/BurnOutSharp.Models/PFF/Footer.cs b/BurnOutSharp.Models/PFF/Footer.cs
new file mode 100644
index 00000000..c47f8bbd
--- /dev/null
+++ b/BurnOutSharp.Models/PFF/Footer.cs
@@ -0,0 +1,24 @@
+namespace BurnOutSharp.Models.PFF
+{
+ ///
+ /// PFF file footer
+ ///
+ ///
+ public sealed class Footer
+ {
+ ///
+ /// Current system IP
+ ///
+ public uint SystemIP;
+
+ ///
+ /// Reserved
+ ///
+ public uint Reserved;
+
+ ///
+ /// King tag
+ ///
+ public string KingTag;
+ }
+}
\ No newline at end of file
diff --git a/BurnOutSharp.Models/PFF/Header.cs b/BurnOutSharp.Models/PFF/Header.cs
new file mode 100644
index 00000000..32b21fc9
--- /dev/null
+++ b/BurnOutSharp.Models/PFF/Header.cs
@@ -0,0 +1,36 @@
+namespace BurnOutSharp.Models.PFF
+{
+ ///
+ /// PFF archive header
+ ///
+ /// Versions 2, 3, and 4 supported
+ ///
+ public sealed class Header
+ {
+ ///
+ /// Size of the following header
+ ///
+ public uint HeaderSize;
+
+ ///
+ /// Signature
+ ///
+ /// Versions 2 and 3 share the same signature but different header sizes
+ public string Signature;
+
+ ///
+ /// Number of files
+ ///
+ public uint NumberOfFiles;
+
+ ///
+ /// File segment size
+ ///
+ public uint FileSegmentSize;
+
+ ///
+ /// File list offset
+ ///
+ public uint FileListOffset;
+ }
+}
\ No newline at end of file
diff --git a/BurnOutSharp.Models/PFF/Segment.cs b/BurnOutSharp.Models/PFF/Segment.cs
new file mode 100644
index 00000000..9b38cb4d
--- /dev/null
+++ b/BurnOutSharp.Models/PFF/Segment.cs
@@ -0,0 +1,46 @@
+namespace BurnOutSharp.Models.PFF
+{
+ ///
+ /// PFF segment identifier
+ ///
+ ///
+ public sealed class Segment
+ {
+ ///
+ /// Deleted flag
+ ///
+ public uint Deleted;
+
+ ///
+ /// File location
+ ///
+ public uint FileLocation;
+
+ ///
+ /// File size
+ ///
+ public uint FileSize;
+
+ ///
+ /// Packed date
+ ///
+ public uint PackedDate;
+
+ ///
+ /// File name
+ ///
+ public string FileName;
+
+ ///
+ /// Modified date
+ ///
+ /// Only for versions 3 and 4
+ public uint ModifiedDate;
+
+ ///
+ /// Compression level
+ ///
+ /// Only for version 4
+ public uint CompressionLevel;
+ }
+}
\ No newline at end of file
diff --git a/BurnOutSharp.Wrappers/PFF.cs b/BurnOutSharp.Wrappers/PFF.cs
new file mode 100644
index 00000000..411eab54
--- /dev/null
+++ b/BurnOutSharp.Wrappers/PFF.cs
@@ -0,0 +1,273 @@
+using System.IO;
+using System.Text;
+
+namespace BurnOutSharp.Wrappers
+{
+ public class PFF : WrapperBase
+ {
+ #region Pass-Through Properties
+
+ #region Header
+
+ ///
+ public uint HeaderSize => _archive.Header.HeaderSize;
+
+ ///
+ public string Signature => _archive.Header.Signature;
+
+ ///
+ public uint NumberOfFiles => _archive.Header.NumberOfFiles;
+
+ ///
+ public uint FileSegmentSize => _archive.Header.FileSegmentSize;
+
+ ///
+ public uint FileListOffset => _archive.Header.FileListOffset;
+
+ #endregion
+
+ #region Segments
+
+ ///
+ public Models.PFF.Segment[] Segments => _archive.Segments;
+
+ #endregion
+
+ #region Footer
+
+ ///
+ public uint SystemIP => _archive.Footer.SystemIP;
+
+ ///
+ public uint Reserved => _archive.Footer.Reserved;
+
+ ///
+ public string KingTag => _archive.Footer.KingTag;
+
+ #endregion
+
+ #endregion
+
+ #region Instance Variables
+
+ ///
+ /// Internal representation of the archive
+ ///
+ private Models.PFF.Archive _archive;
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Private constructor
+ ///
+ private PFF() { }
+
+ ///
+ /// Create a PFF archive from a byte array and offset
+ ///
+ /// Byte array representing the archive
+ /// Offset within the array to parse
+ /// A PFF archive wrapper on success, null on failure
+ public static PFF Create(byte[] data, int offset)
+ {
+ // If the data is invalid
+ if (data == null)
+ return null;
+
+ // If the offset is out of bounds
+ if (offset < 0 || offset >= data.Length)
+ return null;
+
+ // Create a memory stream and use that
+ MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset);
+ return Create(dataStream);
+ }
+
+ ///
+ /// Create a PFF archive from a Stream
+ ///
+ /// Stream representing the archive
+ /// A PFF archive wrapper on success, null on failure
+ public static PFF Create(Stream data)
+ {
+ // If the data is invalid
+ if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead)
+ return null;
+
+ var archive = Builders.PFF.ParseArchive(data);
+ if (archive == null)
+ return null;
+
+ var wrapper = new PFF
+ {
+ _archive = archive,
+ _dataSource = DataSource.Stream,
+ _streamData = data,
+ };
+ return wrapper;
+ }
+
+ #endregion
+
+ #region Data
+
+ ///
+ /// Extract all segments from the PFF to an output directory
+ ///
+ /// Output directory to write to
+ /// True if all segments extracted, false otherwise
+ public bool ExtractAll(string outputDirectory)
+ {
+ // If we have no segments
+ if (Segments == null || Segments.Length == 0)
+ return false;
+
+ // Loop through and extract all files to the output
+ bool allExtracted = true;
+ for (int i = 0; i < Segments.Length; i++)
+ {
+ allExtracted &= ExtractSegment(i, outputDirectory);
+ }
+
+ return allExtracted;
+ }
+
+ ///
+ /// Extract a segment from the PFF to an output directory by index
+ ///
+ /// Segment index to extract
+ /// Output directory to write to
+ /// True if the segment extracted, false otherwise
+ public bool ExtractSegment(int index, string outputDirectory)
+ {
+ // If we have no segments
+ if (NumberOfFiles == 0 || Segments == null || Segments.Length == 0)
+ return false;
+
+ // If we have an invalid index
+ if (index < 0 || index >= Segments.Length)
+ return false;
+
+ // Get the segment information
+ var file = Segments[index];
+
+ // Get the read index and length
+ int offset = (int)file.FileLocation;
+ int size = (int)file.FileSize;
+
+ try
+ {
+ // Ensure the output directory exists
+ Directory.CreateDirectory(outputDirectory);
+
+ // Create the output path
+ string filePath = Path.Combine(outputDirectory, file.FileName);
+ using (FileStream fs = File.OpenWrite(filePath))
+ {
+ // Read the data block
+ byte[] data = ReadFromDataSource(offset, size);
+
+ // Write the data -- TODO: Compressed data?
+ fs.Write(data, 0, size);
+ }
+
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ #endregion
+
+ #region Printing
+
+ ///
+ public override StringBuilder PrettyPrint()
+ {
+ StringBuilder builder = new StringBuilder();
+
+ builder.AppendLine("PFF Information:");
+ builder.AppendLine("-------------------------");
+ builder.AppendLine();
+
+ PrintHeader(builder);
+ PrintSegments(builder);
+ PrintFooter(builder);
+
+ return builder;
+ }
+
+ ///
+ /// Print header information
+ ///
+ /// StringBuilder to append information to
+ private void PrintHeader(StringBuilder builder)
+ {
+ builder.AppendLine(" Header Information:");
+ builder.AppendLine(" -------------------------");
+ builder.AppendLine($" Header size: {HeaderSize} (0x{HeaderSize:X})");
+ builder.AppendLine($" Signature: {Signature}");
+ builder.AppendLine($" Number of files: {NumberOfFiles} (0x{NumberOfFiles:X})");
+ builder.AppendLine($" File segment size: {FileSegmentSize} (0x{FileSegmentSize:X})");
+ builder.AppendLine($" File list offset: {FileListOffset} (0x{FileListOffset:X})");
+ builder.AppendLine();
+ }
+
+ ///
+ /// Print segmentsinformation
+ ///
+ /// StringBuilder to append information to
+ private void PrintSegments(StringBuilder builder)
+ {
+ builder.AppendLine(" Segments Information:");
+ builder.AppendLine(" -------------------------");
+ if (NumberOfFiles == 0 || Segments == null || Segments.Length == 0)
+ {
+ builder.AppendLine(" No segments");
+ }
+ else
+ {
+ for (int i = 0; i < Segments.Length; i++)
+ {
+ var segment = Segments[i];
+ builder.AppendLine($" Segment {i}");
+ builder.AppendLine($" Deleted: {segment.Deleted} (0x{segment.Deleted:X})");
+ builder.AppendLine($" File location: {segment.FileLocation} (0x{segment.FileLocation:X})");
+ builder.AppendLine($" File size: {segment.FileSize} (0x{segment.FileSize:X})");
+ builder.AppendLine($" Packed date: {segment.PackedDate} (0x{segment.PackedDate:X})");
+ builder.AppendLine($" File name: {segment.FileName ?? "[NULL]"}");
+ builder.AppendLine($" Modified date: {segment.ModifiedDate} (0x{segment.ModifiedDate:X})");
+ builder.AppendLine($" Compression level: {segment.CompressionLevel} (0x{segment.CompressionLevel:X})");
+ }
+ }
+ builder.AppendLine();
+ }
+
+ ///
+ /// Print footer information
+ ///
+ /// StringBuilder to append information to
+ private void PrintFooter(StringBuilder builder)
+ {
+ builder.AppendLine(" Footer Information:");
+ builder.AppendLine(" -------------------------");
+ builder.AppendLine($" System IP: {SystemIP} (0x{SystemIP:X})");
+ builder.AppendLine($" Reserved: {Reserved} (0x{Reserved:X})");
+ builder.AppendLine($" King tag: {KingTag ?? "[NULL]"}");
+ builder.AppendLine();
+ }
+
+#if NET6_0_OR_GREATER
+
+ ///
+ public override string ExportJSON() => System.Text.Json.JsonSerializer.Serialize(_archive, _jsonSerializerOptions);
+
+#endif
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/BurnOutSharp/Enums.cs b/BurnOutSharp/Enums.cs
index 118dd818..7ebfd4f6 100644
--- a/BurnOutSharp/Enums.cs
+++ b/BurnOutSharp/Enums.cs
@@ -115,6 +115,11 @@
///
PAK,
+ ///
+ /// NovaLogic Game Archive Format
+ ///
+ PFF,
+
///
/// PKWARE ZIP archive and derivatives
///
diff --git a/BurnOutSharp/FileType/PFF.cs b/BurnOutSharp/FileType/PFF.cs
new file mode 100644
index 00000000..bc6fa3a6
--- /dev/null
+++ b/BurnOutSharp/FileType/PFF.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Concurrent;
+using System.IO;
+using BurnOutSharp.Interfaces;
+using static BurnOutSharp.Utilities.Dictionary;
+
+namespace BurnOutSharp.FileType
+{
+ ///
+ /// NovaLogic Game Archive Format
+ ///
+ public class PFF : IScannable
+ {
+ ///
+ public ConcurrentDictionary> Scan(Scanner scanner, string file)
+ {
+ if (!File.Exists(file))
+ return null;
+
+ using (var fs = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read))
+ {
+ return Scan(scanner, fs, file);
+ }
+ }
+
+ ///
+ public ConcurrentDictionary> Scan(Scanner scanner, Stream stream, string file)
+ {
+ // If the PFF file itself fails
+ try
+ {
+ string tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(tempPath);
+
+ // Create the wrapper
+ Wrappers.PFF bfpk = Wrappers.PFF.Create(stream);
+ if (bfpk == null)
+ return null;
+
+ // Extract all files
+ bfpk.ExtractAll(tempPath);
+
+ // Collect and format all found protections
+ var protections = scanner.GetProtections(tempPath);
+
+ // If temp directory cleanup fails
+ try
+ {
+ Directory.Delete(tempPath, true);
+ }
+ catch (Exception ex)
+ {
+ if (scanner.IncludeDebug) Console.WriteLine(ex);
+ }
+
+ // Remove temporary path references
+ StripFromKeys(protections, tempPath);
+
+ return protections;
+ }
+ catch (Exception ex)
+ {
+ if (scanner.IncludeDebug) Console.WriteLine(ex);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/BurnOutSharp/Scanner.cs b/BurnOutSharp/Scanner.cs
index 1f9f7a9d..3c1d2bd6 100644
--- a/BurnOutSharp/Scanner.cs
+++ b/BurnOutSharp/Scanner.cs
@@ -513,6 +513,14 @@ namespace BurnOutSharp
AppendToDictionary(protections, subProtections);
}
+ // PFF
+ if (fileName != null && scannable is PFF)
+ {
+ var subProtections = scannable.Scan(this, fileName);
+ PrependToKeys(subProtections, fileName);
+ AppendToDictionary(protections, subProtections);
+ }
+
// PKZIP archive (and derivatives)
if (scannable is PKZIP)
{
diff --git a/BurnOutSharp/Tools/Utilities.cs b/BurnOutSharp/Tools/Utilities.cs
index 4ec16983..84f10f27 100644
--- a/BurnOutSharp/Tools/Utilities.cs
+++ b/BurnOutSharp/Tools/Utilities.cs
@@ -202,6 +202,22 @@ namespace BurnOutSharp.Tools
#endregion
+ #region PFF
+
+ // Version 2
+ if (magic.StartsWith(new byte?[] { 0x14, 0x00, 0x00, 0x00, 0x50, 0x46, 0x46, 0x32 }))
+ return SupportedFileType.PFF;
+
+ // Version 3
+ if (magic.StartsWith(new byte?[] { 0x14, 0x00, 0x00, 0x00, 0x50, 0x46, 0x46, 0x33 }))
+ return SupportedFileType.PFF;
+
+ // Version 4
+ if (magic.StartsWith(new byte?[] { 0x14, 0x00, 0x00, 0x00, 0x50, 0x46, 0x46, 0x34 }))
+ return SupportedFileType.PFF;
+
+ #endregion
+
#region PKZIP
// PKZIP (Unknown)
@@ -382,7 +398,7 @@ namespace BurnOutSharp.Tools
return SupportedFileType.BDPlusSVM;
#endregion
-
+
#region BFPK
// No extensions registered for BFPK
@@ -434,7 +450,7 @@ namespace BurnOutSharp.Tools
return SupportedFileType.CIA;
#endregion
-
+
#region Executable
// DOS MZ executable file format (and descendants)
@@ -544,6 +560,13 @@ namespace BurnOutSharp.Tools
#endregion
+ #region PFF
+
+ if (extension.Equals("pff", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.PFF;
+
+ #endregion
+
#region PKZIP
// PKZIP
@@ -791,6 +814,7 @@ namespace BurnOutSharp.Tools
//case SupportedFileType.NCF: return new FileType.NCF();
//case SupportedFileType.Nitro: return new FileType.Nitro();
case SupportedFileType.PAK: return new FileType.PAK();
+ case SupportedFileType.PFF: return new FileType.PFF();
case SupportedFileType.PKZIP: return new FileType.PKZIP();
case SupportedFileType.PLJ: return new FileType.PLJ();
//case SupportedFileType.Quantum: return new FileType.Quantum();
diff --git a/README.md b/README.md
index 400aae56..3a52afc6 100644
--- a/README.md
+++ b/README.md
@@ -177,6 +177,7 @@ Below is a list of container formats that are supported in some way:
| Nintendo 3DS cart image | Yes | Yes | No | |
| Nintendo CIA archive | Yes | Yes | No | |
| Nintendo DS/DSi cart image | Yes | Yes | No | |
+| NovaLogic Game Archive Format (PFF) | Yes | Yes | Yes | |
| PKZIP and derived files (ZIP, etc.) | No | Yes | Yes | Via `SharpCompress` |
| PlayJ audio file (PLJ) | Yes* | Yes | No | Undocumented file format, many fields printed |
| Portable Executable | Yes | Yes | No* | Some packed executables are supported |
diff --git a/Test/Extractor.cs b/Test/Extractor.cs
index cafb7f4a..ee17b0b8 100644
--- a/Test/Extractor.cs
+++ b/Test/Extractor.cs
@@ -532,7 +532,34 @@ namespace Test
}
catch (Exception ex)
{
- Console.WriteLine($"Something went wrong extracting MS-CAB: {ex}");
+ Console.WriteLine($"Something went wrong extracting PAK: {ex}");
+ Console.WriteLine();
+ }
+ }
+
+ // PFF
+ else if (ft == SupportedFileType.PFF)
+ {
+ // Build the archive information
+ Console.WriteLine("Extracting PFF contents");
+ Console.WriteLine();
+
+ var pff = PFF.Create(stream);
+ if (pff == null)
+ {
+ Console.WriteLine("Something went wrong parsing PFF");
+ Console.WriteLine();
+ return;
+ }
+
+ try
+ {
+ // Extract the PFF contents to the directory
+ pff.ExtractAll(outputDirectory);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Something went wrong extracting PFF: {ex}");
Console.WriteLine();
}
}
diff --git a/Test/Printer.cs b/Test/Printer.cs
index 55da7e75..d18351a1 100644
--- a/Test/Printer.cs
+++ b/Test/Printer.cs
@@ -195,6 +195,12 @@ namespace Test
wrapper = PAK.Create(stream);
break;
+ // PFF
+ case SupportedFileType.PFF:
+ wrapperName = "NovaLogic Game Archive Format";
+ wrapper = PFF.Create(stream);
+ break;
+
// PLJ
case SupportedFileType.PLJ:
wrapperName = "PlayJ audio file";