mirror of
https://github.com/SabreTools/BinaryObjectScanner.git
synced 2026-02-17 05:45:14 +00:00
620 lines
21 KiB
C#
620 lines
21 KiB
C#
/*
|
|
* gsf-doc-meta-data.c:
|
|
*
|
|
* Copyright (C) 2002-2006 Dom Lachowicz (cinamod@hotmail.com)
|
|
* Jody Goldberg (jody@gnome.org)
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of version 2.1 of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
|
|
* USA
|
|
*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using LibGSF.Input;
|
|
using LibGSF.Output;
|
|
using static LibGSF.GsfMSOleUtils;
|
|
using static LibGSF.GsfUtils;
|
|
|
|
namespace LibGSF
|
|
{
|
|
// TODO: Can this be made internal?
|
|
public class GsfDocProp
|
|
{
|
|
#region Properties
|
|
|
|
public string Name { get; set; }
|
|
|
|
public object Value { get; set; }
|
|
|
|
/// <summary>
|
|
/// Optionally null
|
|
/// </summary>
|
|
public string LinkedTo { get; set; }
|
|
|
|
public uint RefCount { get; set; }
|
|
|
|
#endregion
|
|
|
|
#region Constructor
|
|
|
|
/// <summary>
|
|
/// Private constructor
|
|
/// </summary>
|
|
private GsfDocProp() { }
|
|
|
|
/// <param name="name">The name of the property.</param>
|
|
/// <returns>A new GsfDocProp.</returns>
|
|
public static GsfDocProp Create(string name)
|
|
{
|
|
if (name == null)
|
|
return null;
|
|
|
|
return new GsfDocProp
|
|
{
|
|
Name = name,
|
|
Value = null,
|
|
LinkedTo = null,
|
|
};
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Functions
|
|
|
|
/// <summary>
|
|
/// Release the given property.
|
|
/// </summary>
|
|
public void Free()
|
|
{
|
|
RefCount--;
|
|
if (RefCount == 0)
|
|
{
|
|
LinkedTo = null;
|
|
if (Value != null)
|
|
Value = null;
|
|
|
|
Name = null;
|
|
}
|
|
}
|
|
|
|
public GsfDocProp Reference()
|
|
{
|
|
RefCount++;
|
|
return this;
|
|
}
|
|
|
|
/// <returns>The current value of prop, and replaces it with <paramref name="val"/>.</returns>
|
|
public object SwapValue(object val)
|
|
{
|
|
object old_val = Value;
|
|
Value = val;
|
|
return old_val;
|
|
}
|
|
|
|
/// <summary>
|
|
/// A debugging utility to dump prop as text via Console
|
|
/// </summary>
|
|
/// <remarks>New in 1.14.2</remarks>
|
|
public void Dump()
|
|
{
|
|
if (Value is List<GsfDocProp> va)
|
|
{
|
|
for (int i = 0; i < va.Count; i++)
|
|
{
|
|
Console.WriteLine($"\t[{i}] = Name: {va[i].Name}, Value: {va[i].Value}, Link: {va[i].LinkedTo}, Ref: {va[i].RefCount}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine($"\t= {Value}");
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
// TODO: Can this be made internal?
|
|
public class GsfDocMetaData
|
|
{
|
|
#region Properties
|
|
|
|
internal Dictionary<string, GsfDocProp> Table = new Dictionary<string, GsfDocProp>();
|
|
|
|
#endregion
|
|
|
|
#region Constructor
|
|
|
|
/// <summary>
|
|
/// Private constructor
|
|
/// </summary>
|
|
private GsfDocMetaData() { }
|
|
|
|
/// <returns>A new metadata property collection</returns>
|
|
public static GsfDocMetaData Create() => new GsfDocMetaData();
|
|
|
|
#endregion
|
|
|
|
#region Functions
|
|
|
|
/// <returns>
|
|
/// The property with <paramref name="name"/> in meta. The caller can
|
|
/// modify the property value and link but not the name.
|
|
/// </returns>
|
|
public GsfDocProp Lookup(string name)
|
|
{
|
|
if (name == null)
|
|
return null;
|
|
|
|
if (!Table.ContainsKey(name))
|
|
return null;
|
|
|
|
return Table[name];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Take ownership of <paramref name="name"/> and <paramref name="value"/> and insert a property into meta.
|
|
/// If a property exists with @name, it is replaced (The link is lost)
|
|
/// </summary>
|
|
/// <param name="name">The id.</param>
|
|
public void Insert(string name, object value)
|
|
{
|
|
if (name == null)
|
|
return;
|
|
|
|
GsfDocProp docProp = GsfDocProp.Create(name);
|
|
docProp.Value = value;
|
|
docProp.LinkedTo = null;
|
|
docProp.RefCount = 1;
|
|
|
|
Table[name] = docProp;
|
|
}
|
|
|
|
/// <summary>
|
|
/// If <paramref name="name"/> does not exist in the collection, do nothing. If @name does exist,
|
|
/// remove it and its value from the collection
|
|
/// </summary>
|
|
/// <param name="name">The non-null string name of the property</param>
|
|
public void Remove(string name)
|
|
{
|
|
if (name == null)
|
|
return;
|
|
|
|
if (!Table.ContainsKey(name))
|
|
return;
|
|
|
|
Table.Remove(name);
|
|
}
|
|
|
|
/// <returns>The property with <paramref name="name"/> in meta.</returns>
|
|
public GsfDocProp Steal(string name)
|
|
{
|
|
if (name == null)
|
|
return null;
|
|
|
|
if (!Table.ContainsKey(name))
|
|
return null;
|
|
|
|
GsfDocProp prop = Table[name];
|
|
if (prop != null)
|
|
Table.Remove(name);
|
|
|
|
return prop;
|
|
}
|
|
|
|
public void Store(GsfDocProp prop)
|
|
{
|
|
if (prop == null)
|
|
return;
|
|
|
|
if (prop != Lookup(prop.Name))
|
|
return;
|
|
|
|
Table[prop.Name] = prop;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Iterate through each (key, value) pair in this collection
|
|
/// </summary>
|
|
/// <param name="func">The function called once for each element in the collection</param>
|
|
/// <param name="user_data">Any supplied user data</param>
|
|
public void ForEach(Action<string, GsfDocProp, object> func, object user_data)
|
|
{
|
|
if (Table.Count == 0)
|
|
return;
|
|
|
|
// Sort the pairs by property name in order to generate consistent files
|
|
List<KeyValuePair<string, GsfDocProp>> pairs = Table.ToList();
|
|
pairs.Sort((a, b) => DerefStrcmp(a.Key, b.Key));
|
|
|
|
for (int i = 0; i < pairs.Count; i++)
|
|
{
|
|
func(pairs[i].Key, pairs[i].Value, user_data);
|
|
}
|
|
}
|
|
|
|
/// <returns>The number of items in this collection</returns>
|
|
public int Size() => Table.Count;
|
|
|
|
/// <summary>
|
|
/// A debugging utility to dump the content of meta via Console
|
|
/// </summary>
|
|
public void Dump()
|
|
{
|
|
ForEach(PrintProperty, null);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region MS-OLE
|
|
|
|
/// <summary>
|
|
/// Read a stream formated as a set of MS OLE properties from <paramref name="input"/> and store the
|
|
/// results in <paramref name="accum"/>.
|
|
/// </summary>
|
|
/// <returns>an Exception if there was an error.</returns>
|
|
/// <remarks>Since: 1.14.24</remarks>
|
|
public Exception ReadFromMSOLE(GsfInput input)
|
|
{
|
|
GsfDocProp prop;
|
|
|
|
// http://bugzilla.gnome.org/show_bug.cgi?id=352055
|
|
// psiwin generates files with empty property sections
|
|
if (input.Size <= 0)
|
|
return null;
|
|
|
|
byte[] data = input.Read(28, null);
|
|
if (data == null)
|
|
return new Exception("Unable to read MS property stream header");
|
|
|
|
/*
|
|
* Validate the Property Set Header.
|
|
* Format (bytes):
|
|
* 00 - 01 Byte order 0xfffe
|
|
* 02 - 03 Format 0
|
|
* 04 - 05 OS Version high word is the OS
|
|
* 06 - 07 low word is the OS version
|
|
* 0 = win16
|
|
* 1 = mac
|
|
* 2 = win32
|
|
* 08 - 23 Class Identifier Usually Format ID
|
|
* 24 - 27 Section count Should be at least 1
|
|
*/
|
|
ushort os = GSF_LE_GET_GUINT16(data, 6);
|
|
ushort version = GSF_LE_GET_GUINT16(data, 2);
|
|
uint num_sections = BitConverter.ToUInt32(data, 24);
|
|
|
|
if (GSF_LE_GET_GUINT16(data, 0) != 0xfffe
|
|
|| (version != 0 && version != 1)
|
|
|| os > 2
|
|
|| num_sections > input.Size / 20
|
|
|| num_sections > 100) // Arbitrary sanity check
|
|
{
|
|
return new Exception("Invalid MS property stream header");
|
|
}
|
|
|
|
// Extract the section info
|
|
/*
|
|
* The Format ID/Offset list follows.
|
|
* Format:
|
|
* 00 - 16 Section Name Format ID
|
|
* 16 - 19 Section Offset The offset is the number of
|
|
* bytes from the start of the
|
|
* whole stream to where the
|
|
* section begins.
|
|
*/
|
|
GsfMSOleMetaDataSection[] sections = new GsfMSOleMetaDataSection[num_sections];
|
|
for (uint i = 0; i < num_sections; i++)
|
|
{
|
|
data = input.Read(20, null);
|
|
if (data == null)
|
|
return new Exception("Unable to read MS property stream header");
|
|
|
|
byte[] guid = new ReadOnlySpan<byte>(data, 0, 16).ToArray();
|
|
if (guid.SequenceEqual(ComponentGUID))
|
|
{
|
|
sections[i].Type = GsfMSOLEMetaDataType.COMPONENT_PROP;
|
|
}
|
|
else if (guid.SequenceEqual(DocumentGUID))
|
|
{
|
|
sections[i].Type = GsfMSOLEMetaDataType.DOC_PROP;
|
|
}
|
|
else if (guid.SequenceEqual(UserGUID))
|
|
{
|
|
sections[i].Type = GsfMSOLEMetaDataType.USER_PROP;
|
|
}
|
|
else
|
|
{
|
|
sections[i].Type = GsfMSOLEMetaDataType.USER_PROP;
|
|
Console.Error.WriteLine("Unknown property section type, treating it as USER");
|
|
}
|
|
|
|
sections[i].Offset = BitConverter.ToUInt32(data, 16);
|
|
}
|
|
|
|
/*
|
|
* A section is the third part of the property set stream.
|
|
* Format (bytes):
|
|
* 00 - 03 Section size A byte count for the section (which is inclusive
|
|
* of the byte count itself and should always be a
|
|
* multiple of 4);
|
|
* 04 - 07 Property count A count of the number of properties
|
|
* 08 - xx An array of 32-bit Property ID/Offset pairs
|
|
* yy - zz An array of Property Type indicators/Value pairs
|
|
*/
|
|
for (uint i = 0; i < num_sections; i++)
|
|
{
|
|
if (input.Seek(sections[i].Offset, SeekOrigin.Begin) || (data = input.Read(8, null)) == null)
|
|
return new Exception("Invalid MS property section");
|
|
|
|
sections[i].IConvHandle = null;
|
|
sections[i].CharSize = 1;
|
|
sections[i].Dict = null;
|
|
sections[i].Size = BitConverter.ToUInt32(data, 0); // Includes header
|
|
sections[i].NumProps = BitConverter.ToUInt32(data, 4);
|
|
|
|
if (sections[i].NumProps <= 0)
|
|
continue;
|
|
|
|
if (sections[i].NumProps > input.Remaining() / 8)
|
|
return new Exception("Invalid MS property stream header or file truncated");
|
|
|
|
if (sections[i].Offset + sections[i].Size > input.Size)
|
|
return new Exception("Invalid MS property stream header or file truncated");
|
|
|
|
/*
|
|
* Get and save all the Property ID/Offset pairs.
|
|
* Format (bytes):
|
|
* 00 - 03 id Property ID
|
|
* 04 - 07 offset The distance from the start of the section to the
|
|
* start of the Property Type/Value pair.
|
|
*/
|
|
GsfMSOleMetaDataProp[] props = new GsfMSOleMetaDataProp[sections[i].NumProps];
|
|
for (uint j = 0; j < sections[i].NumProps; j++)
|
|
{
|
|
if ((data = input.Read(8, null)) == null)
|
|
return new Exception("Invalid MS property section");
|
|
|
|
props[j].Id = BitConverter.ToUInt32(data, 0);
|
|
props[j].Offset = BitConverter.ToUInt32(data, 4);
|
|
}
|
|
|
|
// FIXME: Should we check that ids are distinct?
|
|
|
|
// Order prop info by offset to facilitate bounds checking
|
|
List<GsfMSOleMetaDataProp> tempProps = new List<GsfMSOleMetaDataProp>(props);
|
|
tempProps.Sort(PropertyCompare);
|
|
props = tempProps.ToArray();
|
|
|
|
// Sanity checks.
|
|
for (uint j = 0; j < sections[i].NumProps; j++)
|
|
{
|
|
uint end = (uint)((j == sections[i].NumProps - 1) ? sections[i].Size : props[j + 1].Offset);
|
|
if (props[j].Offset < 0 || props[j].Offset + 4 > end)
|
|
return new Exception("Invalid MS property section");
|
|
}
|
|
|
|
// Find and process the code page.
|
|
// Property ID 1 is reserved as an indicator of the code page.
|
|
sections[i].IConvHandle = null;
|
|
sections[i].CharSize = 1;
|
|
|
|
for (uint j = 0; j < sections[i].NumProps; j++) // First codepage
|
|
{
|
|
if (props[j].Id == 1)
|
|
{
|
|
sections[i].PropertyRead(input, props, j, this);
|
|
if ((prop = Lookup(GsfMetaNames.GSF_META_NAME_CODEPAGE)) != null)
|
|
{
|
|
object val = prop.Value;
|
|
if (val != null && val is int)
|
|
{
|
|
int codepage = (int)val;
|
|
sections[i].IConvHandle = Encoding.GetEncoding(codepage);
|
|
sections[i].CharSize = (uint)CodePageCharSize(codepage);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (sections[i].IConvHandle == null)
|
|
sections[i].IConvHandle = Encoding.GetEncoding(1252);
|
|
|
|
// Find and process the Property Set Dictionary
|
|
// Property ID 0 is reserved as an indicator of the dictionary.
|
|
// For User Defined Sections, Property ID 0 is NOT a dictionary.
|
|
|
|
for (uint j = 0; j < sections[i].NumProps; j++) // The dictionary
|
|
{
|
|
if (props[j].Id == 0)
|
|
sections[i].PropertyRead(input, props, j, this);
|
|
}
|
|
|
|
// Process all the properties
|
|
for (uint j = 0; j < sections[i].NumProps; j++) // The rest
|
|
{
|
|
if (props[j].Id > 1)
|
|
sections[i].PropertyRead(input, props, j, this);
|
|
}
|
|
|
|
sections[i].IConvHandle = null;
|
|
if (sections[i].Dict != null)
|
|
sections[i].Dict = null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary></summary>
|
|
/// <param name="doc_not_component">A kludge to differentiate DocumentSummary from Summary</param>
|
|
/// <returns>True on success</returns>
|
|
/// <remarks>Since: 1.14.24</remarks>
|
|
public bool WriteToMSOLE(GsfOutput output, bool doc_not_component)
|
|
{
|
|
byte[] header =
|
|
{
|
|
0xfe, 0xff, // Byte order
|
|
0, 0, // Format
|
|
0x04, 0x0a, // OS : XP == 0xA04
|
|
0x02, 0x00, // Win32 == 2
|
|
0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, // Clasid = 0
|
|
};
|
|
|
|
bool success = false;
|
|
byte[] buf = new byte[4];
|
|
|
|
const int default_codepage = 1252;
|
|
|
|
WritePropState state = new WritePropState
|
|
{
|
|
CodePage = 0,
|
|
IConvHandle = null,
|
|
CharSize = 1,
|
|
Output = output,
|
|
Dict = null,
|
|
BuiltIn = new WritePropStatePropList
|
|
{
|
|
Count = 1, // Codepage
|
|
Props = null,
|
|
},
|
|
User = new WritePropStatePropList
|
|
{
|
|
Count = 2, // Codepage and Dictionary
|
|
Props = null,
|
|
},
|
|
DocNotComponent = doc_not_component,
|
|
};
|
|
|
|
foreach (var prop in Table)
|
|
{
|
|
state.CountProperties(prop.Key, prop.Value);
|
|
}
|
|
|
|
state.IConvHandle = Encoding.GetEncoding(default_codepage);
|
|
if (state.CodePage == 0)
|
|
{
|
|
state.GuessCodePage(false);
|
|
if (state.Dict != null)
|
|
state.GuessCodePage(true);
|
|
|
|
if (state.CodePage == 0)
|
|
state.CodePage = default_codepage;
|
|
}
|
|
|
|
state.IConvHandle = Encoding.GetEncoding(state.CodePage);
|
|
state.CharSize = CodePageCharSize(state.CodePage);
|
|
|
|
// Write stream header
|
|
GSF_LE_SET_GUINT32(buf, 0, (uint)(state.Dict != null ? 2 : 1));
|
|
if (!output.Write(header.Length, header) || !output.Write(4, buf))
|
|
{
|
|
state.IConvHandle = null;
|
|
state.BuiltIn.Props = null;
|
|
state.User.Props = null;
|
|
state.Dict = null;
|
|
return success;
|
|
}
|
|
|
|
// Write section header(s)
|
|
GSF_LE_SET_GUINT32(buf, 0, (uint)(state.Dict != null ? 0x44 : 0x30));
|
|
if (!output.Write(16, doc_not_component ? DocumentGUID : ComponentGUID) || !output.Write(4, buf))
|
|
{
|
|
state.IConvHandle = null;
|
|
state.BuiltIn.Props = null;
|
|
state.User.Props = null;
|
|
state.Dict = null;
|
|
return success;
|
|
}
|
|
|
|
if (state.Dict != null)
|
|
{
|
|
GSF_LE_SET_GUINT32(buf, 0, 0);
|
|
if (!output.Write(UserGUID.Length, UserGUID) || !output.Write(4, buf)) // Bogus position, fix it later
|
|
{
|
|
state.IConvHandle = null;
|
|
state.BuiltIn.Props = null;
|
|
state.User.Props = null;
|
|
state.Dict = null;
|
|
return success;
|
|
}
|
|
}
|
|
|
|
// Write section(s)
|
|
if (!state.WriteSection(false))
|
|
{
|
|
state.IConvHandle = null;
|
|
state.BuiltIn.Props = null;
|
|
state.User.Props = null;
|
|
state.Dict = null;
|
|
return success;
|
|
}
|
|
|
|
if (state.Dict != null)
|
|
{
|
|
long baseOffset = state.Output.CurrentOffset;
|
|
GSF_LE_SET_GUINT32(buf, 0, (uint)baseOffset);
|
|
if (!state.Output.Seek(0x40, SeekOrigin.Begin)
|
|
|| !output.Write(4, buf)
|
|
|| !state.Output.Seek(0, SeekOrigin.End)
|
|
|| !state.WriteSection(true))
|
|
{
|
|
state.IConvHandle = null;
|
|
state.BuiltIn.Props = null;
|
|
state.User.Props = null;
|
|
state.Dict = null;
|
|
return success;
|
|
}
|
|
}
|
|
|
|
state.IConvHandle = null;
|
|
state.BuiltIn.Props = null;
|
|
state.User.Props = null;
|
|
state.Dict = null;
|
|
return true;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Utilities
|
|
|
|
private static int DerefStrcmp(string a, string b) => a.CompareTo(b);
|
|
|
|
private static void PrintProperty(string name, GsfDocProp prop, object user_data)
|
|
{
|
|
if (prop.LinkedTo != null)
|
|
Console.WriteLine($"prop '{name}' LINKED TO -> {prop.LinkedTo}");
|
|
else
|
|
Console.WriteLine($"prop '{name}'");
|
|
|
|
prop.Dump();
|
|
}
|
|
|
|
private int CodePageCharSize(int codepage) => (codepage == 1200 || codepage == 1201 ? 2 : 1);
|
|
|
|
private static int PropertyCompare(GsfMSOleMetaDataProp prop_a, GsfMSOleMetaDataProp prop_b)
|
|
{
|
|
if (prop_a.Offset < prop_b.Offset)
|
|
return -1;
|
|
else if (prop_a.Offset > prop_b.Offset)
|
|
return +1;
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|