1
0
mirror of https://github.com/SineVector241/VoiceCraft-MCBE_Proximity_Chat.git synced 2024-11-24 14:36:13 +00:00
VoiceCraft-MCBE_Proximity_Chat/ATL/AudioData/Utils/FileStructureHelper.cs
2024-07-13 11:16:08 +10:00

709 lines
31 KiB
C#

using Commons;
using HashDepot;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace ATL.AudioData
{
/// <summary>
/// Helper class used to :
/// - Record location and size of specific chunks of data within a structured file, called "Zones"
/// - Record location, value and type of headers describing Zones
/// - Modify these headers as Zones appear, disappear, expand or shrink
/// </summary>
public class FileStructureHelper
{
/// <summary>
/// Default zone name to be used when no naming is necessary (simple cases where there is a but a single Zone to describe)
/// </summary>
public const string DEFAULT_ZONE_NAME = "default";
/// <summary>
/// Zone name to be used for padding
/// </summary>
public const string PADDING_ZONE_NAME = "padding";
/// <summary>
/// Zone name to be used to store post-processing elements
/// </summary>
public const string POST_PROCESSING_ZONE_NAME = "post-processing";
/// <summary>
/// Type of action to react to
/// </summary>
public enum ACTION
{
/// <summary>
/// No action
/// </summary>
None = -1,
/// <summary>
/// Existing zone is edited, and not removed
/// </summary>
Edit = 0,
/// <summary>
/// New zone is added
/// </summary>
Add = 1,
/// <summary>
/// Existing zone is removed
/// </summary>
Delete = 2
};
/// <summary>
/// Container class describing a frame header
/// </summary>
public class FrameHeader
{
/// <summary>
/// Header types
/// </summary>
public enum TYPE
{
/// <summary>
/// Counter : counts the underlying number of frames
/// </summary>
Counter = 0,
/// <summary>
/// Size : documents the size of a given frame / group of frames
/// </summary>
Size = 1,
/// <summary>
/// Index (absolute) : documents the offset (position of 1st byte) of a given frame
/// </summary>
Index = 2,
/// <summary>
/// Index (relative) : documents the offset (position of 1st byte) of a given frame, relative to the header's position
/// </summary>
RelativeIndex = 3
};
/// <summary>
/// Header type (allowed values are TYPE_XXX within FrameHeader class)
/// </summary>
public readonly TYPE Type;
/// <summary>
/// Position of the header
/// </summary>
public readonly long Position;
/// <summary>
/// True if header value is stored using little-endian convention; false if big-endian
/// </summary>
public readonly bool IsLittleEndian;
/// <summary>
/// Zone where the header is located physically
/// </summary>
public readonly string ParentZone;
/// <summary>
/// Zone to which the header value is pointing to (index-type header only; for post-processing only)
/// </summary>
public readonly string ValueZone;
/// <summary>
/// Current value of the header (counter : number of frames / size : frame size / index : frame index (absolute) / rindex : frame index (relative to header position))
/// </summary>
public object Value { get; set; }
/// <summary>
/// Constructs a new frame header using the given field values
/// </summary>
public FrameHeader(TYPE type, long position, object value, bool isLittleEndian = true, string parentZone = "", string valueZone = "")
{
Type = type; Position = position; Value = value; IsLittleEndian = isLittleEndian; ParentZone = parentZone; ValueZone = valueZone;
}
}
/// <summary>
/// Container class describing a chunk/frame within a structured file
/// </summary>
public class Zone
{
/// <summary>
/// Zone name (any unique value will do; used as internal reference only)
/// </summary>
public string Name { get; set; }
/// <summary>
/// Offset in bytes
/// </summary>
public long Offset { get; set; }
/// <summary>
/// Size in bytes
/// </summary>
public long Size { get; set; }
/// <summary>
/// Data sequence that has to be written in the zone when the zone does not contain any other data
/// </summary>
public byte[] CoreSignature { get; set; }
/// <summary>
/// Indicates whether the zone contents are deletable by ATL (e.g. non-metadata zone is not deletable)
/// </summary>
public bool IsDeletable { get; set; }
/// <summary>
/// Generic usage flag for storing information
/// </summary>
public byte Flag { get; set; }
/// <summary>
/// Size descriptors and item counters referencing the zone elsehwere on the file
/// </summary>
public IList<FrameHeader> Headers { get; set; }
/// <summary>
/// True if the zone might shrink or enlarge, false if it must keep its original size
/// </summary>
public bool IsResizable { get; set; }
/// <summary>
/// True if the zone can't be edited in any way
/// </summary>
public bool IsReadonly => 0 == Size && !IsResizable && !IsDeletable;
/// <summary>
/// Construct a new Zone using the given field values
/// </summary>
public Zone(string name, long offset, long size, byte[] coreSignature, bool isDeletable = true, byte flag = 0, bool resizable = true)
{
Name = name; Offset = offset; Size = size; CoreSignature = coreSignature; IsDeletable = isDeletable; Flag = flag; IsResizable = resizable;
Headers = new List<FrameHeader>();
}
/// <summary>
/// Remove all headers
/// </summary>
public void Clear()
{
Headers?.Clear();
}
}
private sealed class ZoneInfo
{
public string Name { get; }
public int RegionId { get; }
public ZoneInfo(string name, int regionId)
{
Name = name;
RegionId = regionId;
}
public override string ToString()
{
return RegionId + ":" + Name;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
// Actually check the type, should not throw exception from Equals override
if (obj.GetType() != this.GetType()) return false;
return RegionId == ((ZoneInfo)obj).RegionId
&& Name == ((ZoneInfo)obj).Name;
}
public override int GetHashCode()
{
return (int)FNV1a.Hash32(Utils.Latin1Encoding.GetBytes(ToString()));
}
}
// Recorded zones
private readonly IDictionary<string, Zone> zones;
// Stores offset variations caused by zone editing (add/remove/shrink/expand) within current file
// 1st dictionary key : region id (-1 = file-wide offset correction that records cumulative changes across all regions)
// 2nd dictionary key : zone information
// KVP Key : initial end offset of given zone (i.e. position of last byte within zone)
// KVP Value : variation applied to given zone (can be positive or negative)
private readonly IDictionary<int, IDictionary<ZoneInfo, KeyValuePair<long, long>>> dynamicOffsetCorrection = new Dictionary<int, IDictionary<ZoneInfo, KeyValuePair<long, long>>>();
// True if attached file uses little-endian convention for number representation; false if big-endian
private readonly bool isLittleEndian;
// Auto-incremented index for internal naming of post-processing zones
private int postProcessingIndex;
/// <summary>
/// Names of recorded zones
/// </summary>
public ICollection<string> ZoneNames => zones.Keys;
/// <summary>
/// Recorded zones, sorted by offset
/// </summary>
public ICollection<Zone> Zones
{
get
{
// 1. Ignore zones declared but not added
// 2. Sort by offset
return zones.Values.Where(zone => zone.Offset > -1).OrderBy(zone => zone.Offset).ThenBy(zone => zone.Name).ToList();
}
}
/// <summary>
/// Construct a new FileStructureHelper
/// </summary>
/// <param name="isLittleEndian">True if unerlying file uses little-endian convention for number representation; false if big-endian</param>
public FileStructureHelper(bool isLittleEndian = true)
{
this.isLittleEndian = isLittleEndian;
zones = new Dictionary<string, Zone>();
// Init global region
dynamicOffsetCorrection.Add(-1, new Dictionary<ZoneInfo, KeyValuePair<long, long>>());
}
/// <summary>
/// Clear all recorded Zones
/// </summary>
public void Clear()
{
if (null != zones)
{
foreach (string s in zones.Keys)
{
zones[s].Clear();
}
zones.Clear();
}
dynamicOffsetCorrection.Clear();
dynamicOffsetCorrection.Add(-1, new Dictionary<ZoneInfo, KeyValuePair<long, long>>());
}
/// <summary>
/// Retrieve a zone by its name
/// </summary>
/// <param name="name">Name of the zone to retrieve</param>
/// <returns>The zone corresponding to the given name; null if not found</returns>
public Zone GetZone(string name)
{
return zones.TryGetValue(name, out var zone) ? zone : null;
}
/// <summary>
/// Record a new zone by copying the given zone
/// </summary>
/// <param name="zone">Zone to be recorded</param>
public void AddZone(Zone zone)
{
AddZone(zone.Offset, zone.Size, zone.CoreSignature, zone.Name);
foreach (FrameHeader header in zone.Headers)
{
addZoneHeader(zone.Name, header.Type, header.Position, header.Value, header.IsLittleEndian);
}
}
/// <summary>
/// Record a new zone using the given fields
/// </summary>
public void AddZone(long offset, long size, string name = DEFAULT_ZONE_NAME, bool isDeletable = true, bool resizable = true)
{
AddZone(offset, size, Array.Empty<byte>(), name, isDeletable, resizable);
}
/// <summary>
/// Record a new zone using the given fields
/// </summary>
public void AddZone(long offset, long size, byte[] coreSignature, string name = DEFAULT_ZONE_NAME, bool isDeletable = true, bool resizable = true)
{
if (!zones.ContainsKey(name))
{
zones.Add(name, new Zone(name, offset, size, coreSignature, isDeletable, 0, resizable));
}
else // Existing zone might already contain headers
{
zones[name].Name = name;
zones[name].Offset = offset;
zones[name].Size = size;
zones[name].CoreSignature = coreSignature;
zones[name].IsDeletable = isDeletable;
zones[name].IsResizable = resizable;
}
}
/// <summary>
/// Remove the zone identified with the given name
/// </summary>
public void RemoveZone(string name)
{
zones.Remove(name);
}
/// <summary>
/// Remove the zones starting with the given name
/// </summary>
public void RemoveZonesStartingWith(string name)
{
var keys = new HashSet<string>(zones.Keys); // Don't iterate directly on the collection we want to remove stuff from
foreach (var zoneName in keys.Where(zoneName => zoneName.StartsWith(name, StringComparison.OrdinalIgnoreCase)))
{
zones.Remove(zoneName);
}
}
/// <summary>
/// Record a new Counter-type header using the given fields and attach it to the zone of given name
/// </summary>
public void AddCounter(long position, object value, string zone = DEFAULT_ZONE_NAME, string parentZone = "")
{
addZoneHeader(zone, FrameHeader.TYPE.Counter, position, value, isLittleEndian, parentZone);
}
/// <summary>
/// Record a new Size-type header using the given fields and attach it to the zone of given name
/// </summary>
public void AddSize(long position, object value, string zone = DEFAULT_ZONE_NAME, string parentZone = "")
{
addZoneHeader(zone, FrameHeader.TYPE.Size, position, value, isLittleEndian, parentZone);
}
/// <summary>
/// Record a new Index-type header using the given fields and attach it to the zone of given name
/// </summary>
public void AddIndex(long position, object value, bool relative = false, string zone = DEFAULT_ZONE_NAME, string parentZone = "")
{
addZoneHeader(zone, relative ? FrameHeader.TYPE.RelativeIndex : FrameHeader.TYPE.Index, position, value, isLittleEndian, parentZone);
}
/// <summary>
/// Record a new Index-type header using the given fields and attach it to the zone of given name, using a position relative to that zone's offset
/// </summary>
public void AddPostProcessingIndex(long pendingPosition, object value, bool relative, string valueZone, string positionZone, string parentZone = "")
{
long finalPosition = getCorrectedOffset(zones[positionZone].Offset) + pendingPosition;
string zoneName = POST_PROCESSING_ZONE_NAME + "." + ++postProcessingIndex;
AddZone(finalPosition, 0, zoneName);
addZoneHeader(zoneName, relative ? FrameHeader.TYPE.RelativeIndex : FrameHeader.TYPE.Index, finalPosition, value, isLittleEndian, parentZone, valueZone);
}
/// <summary>
/// Declare a zone in advance; useful when reading header frames of containing upper frames, without having reached tag frame itself
/// </summary>
/// <param name="zone"></param>
public void DeclareZone(string zone)
{
AddZone(-1, 0, zone);
}
/// <summary>
/// Record a new header using the given fields and attach it to the zone of given name
/// </summary>
private void addZoneHeader(string zone, FrameHeader.TYPE type, long position, object value, bool iisLittleEndian, string parentZone = "", string valueZone = "")
{
if (!zones.ContainsKey(zone)) DeclareZone(zone);
zones[zone].Headers.Add(new FrameHeader(type, position, value, iisLittleEndian, parentZone, valueZone));
}
/// <summary>
/// Update all headers at the given position to the given value
/// (useful when multiple zones refer to the very same header)
/// </summary>
/// <param name="position">Position of header to be updated</param>
/// <param name="type">Type of header to be updated</param>
/// <param name="newValue">New value to be assigned to header</param>
private void updateAllHeadersAtPosition(long position, FrameHeader.TYPE type, object newValue)
{
// NB : this method should perform quite badly -- evolve to using position-based dictionary if any performance issue arise
foreach (Zone frame in zones.Values)
{
foreach (FrameHeader header in frame.Headers)
{
if (position == header.Position && type == header.Type)
{
header.Value = newValue;
}
}
}
}
/// <summary>
/// Perform the addition between the two given values and encodes the result to an array of bytes, according to the type of the reference value
/// </summary>
/// <param name="value">Reference value</param>
/// <param name="delta">Value to add</param>
/// <param name="updatedValue">Updated value (out parameter; will be returned as same type as reference value)</param>
/// <returns>Resulting value after the addition, encoded into an array of bytes, as the same type of the reference value</returns>
private static byte[] addToValue(object value, long delta, out object updatedValue)
{
switch (value)
{
case byte b:
updatedValue = (byte)(b + delta);
return new[] { (byte)updatedValue };
case short s:
updatedValue = (short)(s + delta);
return BitConverter.GetBytes((short)updatedValue);
case ushort value1:
updatedValue = (ushort)(value1 + delta);
return BitConverter.GetBytes((ushort)updatedValue);
case int i:
updatedValue = (int)(i + delta);
return BitConverter.GetBytes((int)updatedValue);
case uint u:
updatedValue = (uint)(u + delta);
return BitConverter.GetBytes((uint)updatedValue);
case long l:
updatedValue = l + delta;
return BitConverter.GetBytes((long)updatedValue);
// Need to tweak because ulong + int is illegal according to the compiler
case ulong value1:
{
if (delta > 0)
{
updatedValue = value1 + (ulong)delta;
}
else
{
updatedValue = value1 - (ulong)(-delta);
}
return BitConverter.GetBytes((ulong)updatedValue);
}
default:
updatedValue = value;
return null;
}
}
private static bool isValueGT(object value, long addition, long comparison)
{
return value switch
{
int i => i + addition >= comparison,
uint u => u + addition >= comparison,
long l => l + addition >= comparison,
_ => throw new NotSupportedException("Value type not supported in comparison")
};
}
/// <summary>
/// Return the the given zone's offset corrected according to the position shifts already applied by previous calls to <see cref="RewriteHeaders"/>
/// e.g. if offset is 30 and 10 bytes have been inserted at position 15, corrected offset will be 40
/// </summary>
/// <param name="zone">Name of the zone to get the corrected offset for</param>
/// <returns>Corrected offset of the zone with the given name</returns>
public long getCorrectedOffset(string zone)
{
return getCorrectedOffset(zones[zone].Offset);
}
/// <summary>
/// Return the the given offset corrected according to the position shifts already applied by previous calls to <see cref="RewriteHeaders"/>
/// e.g. if offset is 30 and 10 bytes have been inserted at position 15, corrected offset will be 40
/// </summary>
/// <param name="offset">Offset to correct</param>
/// <param name="excludeRegion">Id of the region whose offset corrections to ignore</param>
/// <returns>Corrected offset</returns>
public long getCorrectedOffset(long offset, int excludeRegion = -1)
{
long offsetPositionCorrection = 0;
foreach (ZoneInfo info in dynamicOffsetCorrection[-1].Keys) // Search in global repo
{
if (-1 == excludeRegion || excludeRegion != info.RegionId)
{
KeyValuePair<long, long> offsetDelta = dynamicOffsetCorrection[-1][info];
if (offset >= offsetDelta.Key) offsetPositionCorrection += offsetDelta.Value;
}
}
return offset + offsetPositionCorrection;
}
/// <summary>
/// Perform post-processing modifications to the given stream
/// </summary>
/// <param name="writer">Stream to write modifications to</param>
public void PostProcessing(Stream writer)
{
foreach (var zoneName in zones.Keys.Where(zoneName => zoneName.StartsWith(POST_PROCESSING_ZONE_NAME)))
{
RewriteHeaders(writer, null, 0, ACTION.Edit, zoneName);
}
}
/// <summary>
/// Rewrite all zone headers in the given stream according to the given size evolution and the given action
/// </summary>
/// <param name="fullScopeWriter">Full stream to write modifications to</param>
/// <param name="bufferedWriter">Buffered stream to write modifications to</param>
/// <param name="deltaSize">Evolution of zone size (in bytes; positive or negative)</param>
/// <param name="action">Action applied to zone</param>
/// <param name="zoneName">Name of zone to process</param>
/// <param name="globalOffsetCorrection">Offset correction to apply to the zone to process</param>
/// <param name="regionId">ID of the current buffer region; -1 if working on the file itself (global offset correction)</param>
/// <returns></returns>
public bool RewriteHeaders(
Stream fullScopeWriter,
Stream bufferedWriter,
long deltaSize,
ACTION action,
string zoneName = DEFAULT_ZONE_NAME,
long globalOffsetCorrection = 0,
int regionId = -1)
{
long delta;
long offsetPositionCorrection;
long offsetValueCorrection;
byte[] value;
object updatedValue;
bool passedParentZone;
bool passedValueZone;
if (null == zones) return false;
if (!zones.TryGetValue(zoneName, out Zone currentZone)) return true; // No effect
// Get the dynamic correction map from the proper region
if (!dynamicOffsetCorrection.ContainsKey(regionId))
dynamicOffsetCorrection.Add(regionId, new Dictionary<ZoneInfo, KeyValuePair<long, long>>());
var localDynamicOffsetCorrection = dynamicOffsetCorrection[regionId];
// Don't reprocess the position of a post-processing zone
bool isPostReprocessing = zoneName.StartsWith(POST_PROCESSING_ZONE_NAME);
// == Update the current zone's headers
foreach (FrameHeader header in currentZone.Headers)
{
// === Update values
offsetPositionCorrection = -globalOffsetCorrection;
offsetValueCorrection = 0;
delta = 0;
passedParentZone = false;
passedValueZone = false;
foreach (ZoneInfo dynamicZone in localDynamicOffsetCorrection.Keys)
{
// Don't need to process zones located further than we are
if (dynamicZone.Name == header.ParentZone) passedParentZone = true;
if (dynamicZone.Name == header.ValueZone) passedValueZone = true;
if (passedParentZone && passedValueZone) continue;
KeyValuePair<long, long> offsetDelta = localDynamicOffsetCorrection[dynamicZone];
if (header.Position >= offsetDelta.Key && !passedParentZone && !isPostReprocessing) offsetPositionCorrection += offsetDelta.Value;
if ((FrameHeader.TYPE.Index == header.Type || FrameHeader.TYPE.RelativeIndex == header.Type) && isValueGT(header.Value, 0, offsetDelta.Key) && !passedValueZone) offsetValueCorrection += offsetDelta.Value;
}
// If we're about to write outside the buffered writer and the full-scope writer is available, switch to it
Stream s;
if (null == bufferedWriter) s = fullScopeWriter;
else if (header.Position + offsetPositionCorrection < 0 || header.Position + offsetPositionCorrection > bufferedWriter.Length)
{
if (null == fullScopeWriter) throw new InvalidDataException("Trying to write outside the buffered writer");
Logging.LogDelegator.GetLogDelegate()(Logging.Log.LV_DEBUG, "Trying to write outside the buffered writer - switching to full-scope writer");
s = fullScopeWriter;
offsetPositionCorrection = 0;
}
else s = bufferedWriter;
// === Rewrite headers
// If we're going to delete the zone, and the header is located inside it, don't write it !
if (header.ParentZone == zoneName && ACTION.Delete == action) continue;
if (FrameHeader.TYPE.Counter == header.Type || FrameHeader.TYPE.Size == header.Type)
{
if (FrameHeader.TYPE.Counter == header.Type)
{
switch (action)
{
case ACTION.Add: delta = 1; break;
case ACTION.Delete: delta = -1; break;
default: delta = 0; break;
}
}
else if (FrameHeader.TYPE.Size == header.Type)
{
delta = deltaSize;
}
s.Seek(header.Position + offsetPositionCorrection, SeekOrigin.Begin);
value = addToValue(header.Value, delta, out updatedValue);
if (null == value) throw new NotSupportedException("Value type not supported for " + zoneName + "@" + header.Position + " : " + header.Value.GetType());
// The very same frame header is referenced from another frame and must be updated to its new value
updateAllHeadersAtPosition(header.Position, header.Type, updatedValue);
if (!header.IsLittleEndian) Array.Reverse(value);
s.Write(value, 0, value.Length);
}
else if (FrameHeader.TYPE.Index == header.Type || FrameHeader.TYPE.RelativeIndex == header.Type)
{
long headerPosition = header.Position + offsetPositionCorrection;
long headerOffsetCorrection = (FrameHeader.TYPE.RelativeIndex == header.Type) ? headerPosition : 0;
value = null;
if (action != ACTION.Delete)
{
value = header.Value switch
{
long headerValue => BitConverter.GetBytes(headerValue + offsetValueCorrection -
headerOffsetCorrection),
int headerValue => BitConverter.GetBytes((int)(headerValue + offsetValueCorrection -
headerOffsetCorrection)),
uint headerValue => BitConverter.GetBytes((uint)(headerValue + offsetValueCorrection -
headerOffsetCorrection)),
_ => value
};
if (!header.IsLittleEndian) Array.Reverse(value);
}
else
{
value = header.Value switch
{
long _ => BitConverter.GetBytes((long)0),
int _ => BitConverter.GetBytes(0),
uint _ => BitConverter.GetBytes((uint)0),
_ => value
};
}
if (null == value) throw new NotSupportedException("Value type not supported for index in " + zoneName + "@" + header.Position + " : " + header.Value.GetType());
s.Seek(headerPosition, SeekOrigin.Begin);
s.Write(value, 0, value.Length);
} // Index & relative index types
} // Loop through headers
// Record size variations into dynamic offset corrections
if (deltaSize != 0)
{
ZoneInfo zoneInfo = new ZoneInfo(zoneName, regionId);
// Update local dynamic offset correction if non-null
if (!localDynamicOffsetCorrection.ContainsKey(zoneInfo))
localDynamicOffsetCorrection.Add(zoneInfo, new KeyValuePair<long, long>(currentZone.Offset + currentZone.Size, deltaSize));
// If working with local dynamic offset correction, update global dynamic offset correction
if (regionId > -1)
{
IDictionary<ZoneInfo, KeyValuePair<long, long>> globalRegion = dynamicOffsetCorrection[-1];
// Add new region
if (!globalRegion.ContainsKey(zoneInfo))
globalRegion.Add(zoneInfo, new KeyValuePair<long, long>(currentZone.Offset + currentZone.Size, deltaSize));
else // Increment current delta to existing region
{
KeyValuePair<long, long> currentValues = globalRegion[zoneInfo];
globalRegion[zoneInfo] = new KeyValuePair<long, long>(currentValues.Key, currentValues.Value + deltaSize);
}
}
}
return true;
}
}
}