Update bundle reader

- Replaced creation of a duplicated file/memory stream with OffsetStream.
- Added separate processing of uncompressed bundles (including streamed bundles). They will be read directly from disk.
- Added progress report on LZMA decompression process.
This commit is contained in:
VaDiM
2025-07-24 17:24:41 +03:00
parent efca2a7557
commit c20c07b5f2
7 changed files with 191 additions and 143 deletions

View File

@ -321,16 +321,18 @@ namespace AssetStudio
Logger.Debug($"Bundle offset: {reader.Position}");
var bundleStream = new OffsetStream(reader);
var bundleReader = new FileReader(reader.FullPath, bundleStream);
var isLoaded = false;
try
{
var bundleFile = new BundleFile(bundleReader, CustomBlockInfoCompression, CustomBlockCompression, specifiedUnityVersion);
var isLoaded = LoadBundleFiles(bundleReader, bundleFile, originalPath);
isLoaded = LoadBundleFiles(bundleReader, bundleFile, originalPath);
if (!isLoaded)
return false;
while (bundleFile.IsMultiBundle && isLoaded)
while (bundleFile.IsDataAfterBundle && isLoaded)
{
isLoaded = false;
bundleStream.Offset = reader.Position;
bundleReader = new FileReader($"{reader.FullPath}_0x{bundleStream.Offset:X}", bundleStream);
if (bundleReader.FileType != FileType.BundleFile)
@ -345,7 +347,7 @@ namespace AssetStudio
bundleReader.FileName = $"{reader.FileName}_0x{bundleStream.Offset:X}";
}
Logger.Info($"[MultiBundle] Loading \"{reader.FileName}\" from offset: 0x{bundleStream.Offset:X}");
bundleFile = new BundleFile(bundleReader, CustomBlockInfoCompression, CustomBlockCompression, specifiedUnityVersion);
bundleFile = new BundleFile(bundleReader, CustomBlockInfoCompression, CustomBlockCompression, specifiedUnityVersion, isMultiBundle: true);
isLoaded = LoadBundleFiles(bundleReader, bundleFile, originalPath ?? reader.FullPath);
}
return isLoaded;
@ -367,6 +369,7 @@ namespace AssetStudio
}
finally
{
if (!isLoaded)
bundleReader.Dispose();
}
}
@ -375,6 +378,9 @@ namespace AssetStudio
{
foreach (var file in bundleFile.fileList)
{
if (file.stream == null)
continue;
file.stream.Position = 0; //go to file offset
var dummyPath = Path.Combine(Path.GetDirectoryName(reader.FullPath), file.fileName);
var subReader = new FileReader(dummyPath, file.stream);
if (subReader.FileType == FileType.AssetsFile)

View File

@ -1,6 +1,7 @@
// LzmaDecoder.cs
using System;
using AssetStudio;
namespace SevenZip.Compression.LZMA
{
@ -247,6 +248,8 @@ namespace SevenZip.Compression.LZMA
m_OutWindow.PutByte(b);
nowPos64++;
}
Progress.Reset();
while (nowPos64 < outSize64)
{
// UInt64 next = Math.Min(nowPos64 + (1 << 18), outSize64);
@ -338,6 +341,8 @@ namespace SevenZip.Compression.LZMA
}
m_OutWindow.CopyBlock(rep0, len);
nowPos64 += len;
Progress.Report((int)(nowPos64 * 100f / outSize64), 100);
}
}
}

View File

@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
namespace AssetStudio
{
@ -42,7 +43,7 @@ namespace AssetStudio
public class BundleFile
{
public readonly bool IsMultiBundle;
public readonly bool IsDataAfterBundle;
public class Header
{
@ -75,10 +76,11 @@ namespace AssetStudio
private StorageBlock[] m_BlocksInfo;
private Node[] m_DirectoryInfo;
public StreamFile[] fileList;
public List<StreamFile> fileList;
public BundleFile(FileReader reader, CompressionType customBlockInfoCompression, CompressionType customBlockCompression, UnityVersion specUnityVer = null)
public BundleFile(FileReader reader, CompressionType customBlockInfoCompression, CompressionType customBlockCompression, UnityVersion specUnityVer = null, bool isMultiBundle = false)
{
Stream blocksStream;
m_Header = new Header();
m_Header.signature = reader.ReadStringToNull();
m_Header.version = reader.ReadUInt32();
@ -98,11 +100,11 @@ namespace AssetStudio
goto case "UnityFS";
}
ReadHeaderAndBlocksInfo(reader);
using (var blocksStream = CreateBlocksStream(reader.FullPath))
using (reader)
{
ReadBlocksAndDirectory(reader, blocksStream);
ReadFiles(blocksStream, reader.FullPath);
blocksStream = ReadBlocksAndDirectory(reader);
}
ReadFiles(blocksStream);
break;
case "UnityFS":
ReadHeader(reader);
@ -115,7 +117,7 @@ namespace AssetStudio
}
else if (streamSize - bundleSize > 200)
{
IsMultiBundle = true;
IsDataAfterBundle = true;
}
var unityVer = m_Header.unityRevision;
@ -133,11 +135,19 @@ namespace AssetStudio
UnityCnCheck(reader, customBlockInfoCompression, unityVer);
ReadBlocksInfoAndDirectory(reader, customBlockInfoCompression, unityVer);
using (var blocksStream = CreateBlocksStream(reader.FullPath))
if (!isMultiBundle && IsUncompressedBundle)
{
ReadBlocks(reader, customBlockCompression, blocksStream);
ReadFiles(blocksStream, reader.FullPath);
ReadFiles(reader.BaseStream, reader.Position);
break;
}
blocksStream = ReadBlocks(reader, customBlockCompression);
ReadFiles(blocksStream);
if (!IsDataAfterBundle)
reader.Close();
break;
}
}
@ -156,7 +166,7 @@ namespace AssetStudio
m_BlocksInfo = new StorageBlock[1];
for (int i = 0; i < levelCount; i++)
{
var storageBlock = new StorageBlock()
var storageBlock = new StorageBlock
{
compressedSize = reader.ReadUInt32(),
uncompressedSize = reader.ReadUInt32(),
@ -179,23 +189,15 @@ namespace AssetStudio
private Stream CreateBlocksStream(string path)
{
Stream blocksStream;
var uncompressedSizeSum = m_BlocksInfo.Sum(x => x.uncompressedSize);
if (uncompressedSizeSum >= int.MaxValue)
{
/*var memoryMappedFile = MemoryMappedFile.CreateNew(null, uncompressedSizeSum);
assetsDataStream = memoryMappedFile.CreateViewStream();*/
blocksStream = new FileStream(path + ".temp", FileMode.Create, FileAccess.ReadWrite, FileShare.None, 4096, FileOptions.DeleteOnClose);
}
else
{
blocksStream = new MemoryStream((int)uncompressedSizeSum);
}
return blocksStream;
return uncompressedSizeSum >= int.MaxValue
? new FileStream(path + ".temp", FileMode.Create, FileAccess.ReadWrite, FileShare.None, 4096, FileOptions.DeleteOnClose)
: (Stream) new MemoryStream((int)uncompressedSizeSum);
}
private void ReadBlocksAndDirectory(FileReader reader, Stream blocksStream)
private Stream ReadBlocksAndDirectory(FileReader reader)
{
var blocksStream = CreateBlocksStream(reader.FullPath);
var isCompressed = m_Header.signature == "UnityWeb";
foreach (var blockInfo in m_BlocksInfo)
{
@ -213,10 +215,11 @@ namespace AssetStudio
blocksStream.Write(uncompressedBytes, 0, uncompressedBytes.Length);
}
blocksStream.Position = 0;
var blocksReader = new EndianBinaryReader(blocksStream);
var nodesCount = blocksReader.ReadInt32();
m_DirectoryInfo = new Node[nodesCount];
for (int i = 0; i < nodesCount; i++)
for (var i = 0; i < nodesCount; i++)
{
m_DirectoryInfo[i] = new Node
{
@ -225,33 +228,27 @@ namespace AssetStudio
size = blocksReader.ReadUInt32()
};
}
return blocksStream;
}
public void ReadFiles(Stream blocksStream, string path)
private void ReadFiles(Stream inputStream, long blocksOffset = 0)
{
fileList = new StreamFile[m_DirectoryInfo.Length];
for (int i = 0; i < m_DirectoryInfo.Length; i++)
fileList = new List<StreamFile>(m_DirectoryInfo.Length);
foreach (var node in m_DirectoryInfo)
{
var node = m_DirectoryInfo[i];
var file = new StreamFile();
fileList[i] = file;
fileList.Add(file);
file.path = node.path;
file.fileName = Path.GetFileName(node.path);
if (node.size >= int.MaxValue)
try
{
/*var memoryMappedFile = MemoryMappedFile.CreateNew(null, entryinfo_size);
file.stream = memoryMappedFile.CreateViewStream();*/
var extractPath = path + "_unpacked" + Path.DirectorySeparatorChar;
Directory.CreateDirectory(extractPath);
file.stream = new FileStream(extractPath + file.fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite);
file.stream = new OffsetStream(inputStream, node.offset + blocksOffset, node.size);
}
else
catch (IOException e)
{
file.stream = new MemoryStream((int)node.size);
Logger.Warning($"Failed to access {file.fileName} file.\n{e}");
}
blocksStream.Position = node.offset;
blocksStream.CopyTo(file.stream, node.size);
file.stream.Position = 0;
}
}
@ -365,7 +362,7 @@ namespace AssetStudio
if (numWrite != uncompressedSize)
{
var msg = $"{compressionType} blockInfo decompression error. {errorMsg}\nWrite {numWrite} bytes but expected {uncompressedSize} bytes.";
var exMsg = compressionType > CompressionType.Lz4HC
var exMsg = compressionType > CompressionType.Lz4HC || customBlockInfoCompression != CompressionType.Auto
? "Wrong compression type or blockInfo data might be encrypted."
: "BlockInfo data might be encrypted.";
throw new IOException($"{msg}\n{exMsg}\n");
@ -376,7 +373,7 @@ namespace AssetStudio
var uncompressedDataHash = blocksInfoReader.ReadBytes(16);
var blocksInfoCount = blocksInfoReader.ReadInt32();
m_BlocksInfo = new StorageBlock[blocksInfoCount];
for (int i = 0; i < blocksInfoCount; i++)
for (var i = 0; i < blocksInfoCount; i++)
{
m_BlocksInfo[i] = new StorageBlock
{
@ -388,7 +385,7 @@ namespace AssetStudio
var nodesCount = blocksInfoReader.ReadInt32();
m_DirectoryInfo = new Node[nodesCount];
for (int i = 0; i < nodesCount; i++)
for (var i = 0; i < nodesCount; i++)
{
m_DirectoryInfo[i] = new Node
{
@ -405,30 +402,46 @@ namespace AssetStudio
}
}
private void ReadBlocks(FileReader reader, CompressionType customBlockCompression, Stream blocksStream)
private Stream ReadBlocks(FileReader reader, CompressionType customBlockCompression)
{
Logger.Debug($"Block compression: {(CompressionType)m_BlocksInfo.Max(x => x.flags)}");
var blocksStream = CreateBlocksStream(reader.FullPath);
var blocksCompression = m_BlocksInfo.Max(x => (CompressionType)(x.flags & StorageBlockFlags.CompressionTypeMask));
Logger.Debug($"BlockData compression: {blocksCompression}");
Logger.Debug($"BlockData count: {m_BlocksInfo.Length}");
var showCustomTypeWarning = true;
if (customBlockCompression == CompressionType.Auto)
{
if (blocksCompression > CompressionType.Lzham && Enum.IsDefined(typeof(CompressionType), blocksCompression))
{
Logger.Warning($"Non-standard block compression type: {(int)blocksCompression}. Trying to decompress as {blocksCompression} archive..");
}
}
else
{
Logger.Info($"Custom block compression type: {customBlockCompression}");
blocksCompression = customBlockCompression;
}
byte[] sharedCompressedBuff = null;
byte[] sharedUncompressedBuff = null;
if (blocksCompression != CompressionType.Lzma && blocksCompression != CompressionType.Lzham)
{
var blockSize = (int)m_BlocksInfo.Max(x => x.uncompressedSize);
Logger.Debug($"BlockSize: {blockSize}");
sharedCompressedBuff = BigArrayPool<byte>.Shared.Rent(blockSize);
sharedUncompressedBuff = BigArrayPool<byte>.Shared.Rent(blockSize);
}
try
{
foreach (var blockInfo in m_BlocksInfo)
{
var compressionType = (CompressionType)(blockInfo.flags & StorageBlockFlags.CompressionTypeMask);
if (customBlockCompression == CompressionType.Auto)
{
if (showCustomTypeWarning && compressionType > CompressionType.Lzham && Enum.IsDefined(typeof(CompressionType), compressionType))
{
Logger.Warning($"Non-standard block compression type: {(int)compressionType}. Trying to decompress as {compressionType} archive..");
showCustomTypeWarning = false;
}
}
else if (compressionType != CompressionType.None)
if (customBlockCompression != CompressionType.Auto && compressionType > 0)
{
compressionType = customBlockCompression;
if (showCustomTypeWarning)
{
Logger.Info($"Custom block compression type: {customBlockCompression}");
showCustomTypeWarning = false;
}
}
long numWrite;
@ -436,43 +449,32 @@ namespace AssetStudio
switch (compressionType)
{
case CompressionType.None:
{
reader.BaseStream.CopyTo(blocksStream, blockInfo.compressedSize);
numWrite = blockInfo.compressedSize;
break;
}
case CompressionType.Lzma:
{
Logger.Info("Decompressing LZMA stream...");
numWrite = BundleDecompressionHelper.DecompressLzmaStream(reader.BaseStream, blocksStream, blockInfo.compressedSize, blockInfo.uncompressedSize, ref errorMsg);
break;
}
case CompressionType.Lz4:
case CompressionType.Lz4HC:
case CompressionType.Zstd:
case CompressionType.Oodle:
{
var compressedSize = (int)blockInfo.compressedSize;
var uncompressedSize = (int)blockInfo.uncompressedSize;
var compressedBytes = BigArrayPool<byte>.Shared.Rent(compressedSize);
var uncompressedBytes = BigArrayPool<byte>.Shared.Rent(uncompressedSize);
try
{
_ = reader.Read(compressedBytes, 0, compressedSize);
var compressedSpan = new ReadOnlySpan<byte>(compressedBytes, 0, compressedSize);
var uncompressedSpan = new Span<byte>(uncompressedBytes, 0, uncompressedSize);
sharedCompressedBuff.AsSpan().Clear();
sharedUncompressedBuff.AsSpan().Clear();
_ = reader.Read(sharedCompressedBuff, 0, compressedSize);
var compressedSpan = new ReadOnlySpan<byte>(sharedCompressedBuff, 0, compressedSize);
var uncompressedSpan = new Span<byte>(sharedUncompressedBuff, 0, uncompressedSize);
numWrite = BundleDecompressionHelper.DecompressBlock(compressionType, compressedSpan, uncompressedSpan, ref errorMsg);
if (numWrite == uncompressedSize)
blocksStream.Write(uncompressedBytes, 0, uncompressedSize);
}
finally
{
BigArrayPool<byte>.Shared.Return(compressedBytes, clearArray: true);
BigArrayPool<byte>.Shared.Return(uncompressedBytes, clearArray: true);
}
blocksStream.Write(sharedUncompressedBuff, 0, uncompressedSize);
break;
}
case CompressionType.Lzham:
throw new IOException($"Unsupported block compression type: {compressionType}.\n");
default:
@ -482,13 +484,23 @@ namespace AssetStudio
if (numWrite != blockInfo.uncompressedSize)
{
var msg = $"{compressionType} block decompression error. {errorMsg}\nWrite {numWrite} bytes but expected {blockInfo.uncompressedSize} bytes.";
var exMsg = compressionType > CompressionType.Lz4HC
var exMsg = compressionType > CompressionType.Lz4HC || customBlockCompression != CompressionType.Auto
? "Wrong compression type or block data might be encrypted."
: "Block data might be encrypted.";
throw new IOException($"{msg}\n{exMsg}\n");
}
}
blocksStream.Position = 0;
}
finally
{
if (sharedCompressedBuff != null)
BigArrayPool<byte>.Shared.Return(sharedCompressedBuff, clearArray: true);
if (sharedUncompressedBuff != null)
BigArrayPool<byte>.Shared.Return(sharedUncompressedBuff, clearArray: true);
}
return blocksStream;
}
private void UnityCnCheck(FileReader reader, CompressionType customBlockInfoCompression, UnityVersion unityVer)
@ -525,5 +537,7 @@ namespace AssetStudio
}
throw new NotSupportedException("Unsupported bundle file. UnityCN encryption was detected.");
}
private bool IsUncompressedBundle => m_BlocksInfo.All(x => (CompressionType)(x.flags & StorageBlockFlags.CompressionTypeMask) == CompressionType.None);
}
}

View File

@ -5,12 +5,15 @@ namespace AssetStudio
public class OffsetStream : Stream
{
private readonly Stream _baseStream;
private readonly long _length;
private long _offset;
public override bool CanRead => _baseStream.CanRead;
public override bool CanSeek => _baseStream.CanSeek;
public override bool CanWrite => false;
public override long Length => _baseStream.Length - _offset;
public override long Length => _length > 0
? _length
: _baseStream.Length - _offset;
public override long Position
{
@ -40,6 +43,13 @@ namespace AssetStudio
Offset = reader.Position;
}
public OffsetStream(Stream stream, long offset, long length)
{
_baseStream = stream;
_length = length;
Offset = offset;
}
public override void Flush() { }
public override int Read(byte[] buffer, int offset, int count)

View File

@ -6,7 +6,7 @@ namespace AssetStudio
{
public class WebFile
{
public StreamFile[] fileList;
public List<StreamFile> fileList;
private class WebData
{
@ -30,16 +30,15 @@ namespace AssetStudio
data.path = Encoding.UTF8.GetString(reader.ReadBytes(pathLength));
dataList.Add(data);
}
fileList = new StreamFile[dataList.Count];
for (int i = 0; i < dataList.Count; i++)
fileList = new List<StreamFile>(dataList.Count);
foreach (var data in dataList)
{
var data = dataList[i];
var file = new StreamFile();
file.path = data.path;
file.fileName = Path.GetFileName(data.path);
reader.BaseStream.Position = data.dataOffset;
file.stream = new MemoryStream(reader.ReadBytes(data.dataLength));
fileList[i] = file;
fileList.Add(file);
}
}
}

View File

@ -96,11 +96,11 @@ namespace AssetStudioCLI
var bundleReader = new FileReader(reader.FullPath, bundleStream);
var bundleFile = new BundleFile(bundleReader, assetsManager.CustomBlockInfoCompression, assetsManager.CustomBlockCompression, assetsManager.SpecifyUnityVersion);
var extractPath = Path.Combine(savePath, reader.FileName + "_unpacked");
if (bundleFile.fileList.Length > 0)
if (bundleFile.fileList.Count > 0)
{
count += ExtractStreamFile(extractPath, bundleFile.fileList);
}
while (bundleFile.IsMultiBundle)
while (bundleFile.IsDataAfterBundle)
{
bundleStream.Offset = reader.Position;
bundleReader = new FileReader($"{reader.FullPath}_0x{bundleStream.Offset:X}", bundleStream);
@ -113,8 +113,8 @@ namespace AssetStudioCLI
bundleReader.FileName = $"{reader.FileName}_0x{bundleStream.Offset:X}";
}
Logger.Info($"[MultiBundle] Decompressing \"{reader.FileName}\" from offset: 0x{bundleStream.Offset:X}..");
bundleFile = new BundleFile(bundleReader, assetsManager.CustomBlockInfoCompression, assetsManager.CustomBlockCompression, assetsManager.SpecifyUnityVersion);
if (bundleFile.fileList.Length > 0)
bundleFile = new BundleFile(bundleReader, assetsManager.CustomBlockInfoCompression, assetsManager.CustomBlockCompression, assetsManager.SpecifyUnityVersion, isMultiBundle: true);
if (bundleFile.fileList.Count > 0)
{
count += ExtractStreamFile(extractPath, bundleFile.fileList);
}
@ -128,19 +128,21 @@ namespace AssetStudioCLI
Logger.Info($"Decompressing {reader.FileName} ...");
var webFile = new WebFile(reader);
reader.Dispose();
if (webFile.fileList.Length > 0)
if (webFile.fileList.Count > 0)
{
var extractPath = Path.Combine(savePath, reader.FileName + "_unpacked");
return ExtractStreamFile(extractPath, webFile.fileList);
return ExtractStreamFile(extractPath, webFile.fileList, isOffsetStream: false);
}
return 0;
}
private static int ExtractStreamFile(string extractPath, StreamFile[] fileList)
private static int ExtractStreamFile(string extractPath, List<StreamFile> fileList, bool isOffsetStream = true)
{
var extractedCount = 0;
foreach (var file in fileList)
{
if (file.stream == null)
continue;
var filePath = Path.Combine(extractPath, file.path);
var fileDirectory = Path.GetDirectoryName(filePath);
if (!Directory.Exists(fileDirectory))
@ -151,11 +153,16 @@ namespace AssetStudioCLI
{
using (var fileStream = File.Create(filePath))
{
file.stream.CopyTo(fileStream);
file.stream.Position = 0;
file.stream.CopyTo(fileStream, file.stream.Length);
}
extractedCount += 1;
extractedCount++;
}
file.stream.Dispose();
if (!isOffsetStream) file.stream.Dispose();
}
if (isOffsetStream && fileList.Count > 0)
{
fileList[0].stream?.Dispose();
}
return extractedCount;
}

View File

@ -143,11 +143,11 @@ namespace AssetStudioGUI
var bundleReader = new FileReader(reader.FullPath, bundleStream);
var bundleFile = new BundleFile(bundleReader, assetsManager.CustomBlockInfoCompression, assetsManager.CustomBlockCompression, assetsManager.SpecifyUnityVersion);
var extractPath = Path.Combine(savePath, reader.FileName + "_unpacked");
if (bundleFile.fileList.Length > 0)
if (bundleFile.fileList.Count > 0)
{
count += ExtractStreamFile(extractPath, bundleFile.fileList);
}
while (bundleFile.IsMultiBundle)
while (bundleFile.IsDataAfterBundle)
{
bundleStream.Offset = reader.Position;
bundleReader = new FileReader($"{reader.FullPath}_0x{bundleStream.Offset:X}", bundleStream);
@ -160,8 +160,8 @@ namespace AssetStudioGUI
bundleReader.FileName = $"{reader.FileName}_0x{bundleStream.Offset:X}";
}
Logger.Info($"[MultiBundle] Decompressing \"{reader.FileName}\" from offset: 0x{bundleStream.Offset:X}..");
bundleFile = new BundleFile(bundleReader, assetsManager.CustomBlockInfoCompression, assetsManager.CustomBlockCompression, assetsManager.SpecifyUnityVersion);
if (bundleFile.fileList.Length > 0)
bundleFile = new BundleFile(bundleReader, assetsManager.CustomBlockInfoCompression, assetsManager.CustomBlockCompression, assetsManager.SpecifyUnityVersion, isMultiBundle: true);
if (bundleFile.fileList.Count > 0)
{
count += ExtractStreamFile(extractPath, bundleFile.fileList);
}
@ -175,19 +175,21 @@ namespace AssetStudioGUI
Logger.Info($"Decompressing {reader.FileName} ...");
var webFile = new WebFile(reader);
reader.Dispose();
if (webFile.fileList.Length > 0)
if (webFile.fileList.Count > 0)
{
var extractPath = Path.Combine(savePath, reader.FileName + "_unpacked");
return ExtractStreamFile(extractPath, webFile.fileList);
return ExtractStreamFile(extractPath, webFile.fileList, isOffsetStream: false);
}
return 0;
}
private static int ExtractStreamFile(string extractPath, StreamFile[] fileList)
private static int ExtractStreamFile(string extractPath, List<StreamFile> fileList, bool isOffsetStream = true)
{
int extractedCount = 0;
var extractedCount = 0;
foreach (var file in fileList)
{
if (file.stream == null)
continue;
var filePath = Path.Combine(extractPath, file.path);
var fileDirectory = Path.GetDirectoryName(filePath);
if (!Directory.Exists(fileDirectory))
@ -198,11 +200,16 @@ namespace AssetStudioGUI
{
using (var fileStream = File.Create(filePath))
{
file.stream.CopyTo(fileStream);
file.stream.Position = 0;
file.stream.CopyTo(fileStream, file.stream.Length);
}
extractedCount += 1;
extractedCount++;
}
file.stream.Dispose();
if (!isOffsetStream) file.stream.Dispose();
}
if (isOffsetStream && fileList.Count > 0)
{
fileList[0].stream?.Dispose();
}
return extractedCount;
}