diff --git a/SabreTools.Library/Filtering/ExtraIni.cs b/SabreTools.Library/Filtering/ExtraIni.cs
new file mode 100644
index 00000000..6c91e3ae
--- /dev/null
+++ b/SabreTools.Library/Filtering/ExtraIni.cs
@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+
+namespace SabreTools.Library.Filtering
+{
+ public class ExtraIni
+ {
+ ///
+ /// List of extras to apply
+ ///
+ public List Items { get; set; } = new List();
+ }
+}
diff --git a/SabreTools.Library/Filtering/ExtraIniItem.cs b/SabreTools.Library/Filtering/ExtraIniItem.cs
new file mode 100644
index 00000000..21cdedf6
--- /dev/null
+++ b/SabreTools.Library/Filtering/ExtraIniItem.cs
@@ -0,0 +1,123 @@
+using System;
+using System.Collections.Generic;
+
+using SabreTools.Library.Data;
+using SabreTools.Library.DatItems;
+using SabreTools.Library.IO;
+using SabreTools.Library.Tools;
+
+namespace SabreTools.Library.Filtering
+{
+ public class ExtraIniItem
+ {
+ #region Fields
+
+ ///
+ /// Field to update with INI information
+ ///
+ public Field Field { get; set; }
+
+ ///
+ /// Mappings from value to machine name
+ ///
+ public Dictionary> Mappings { get; set; } = new Dictionary>();
+
+ #endregion
+
+ #region Extras Population
+
+ ///
+ /// Populate item using a field:file input
+ ///
+ /// Field and file combination
+ public void PopulateFromInput(string ini)
+ {
+ // If we don't even have a possible field and file combination
+ if (!ini.Contains(":"))
+ {
+ Globals.Logger.Warning($"'{ini}` is not a valid INI extras string. Valid INI extras strings are of the form 'key:value'. Please refer to README.1ST or the help feature for more details.");
+ return;
+ }
+
+ string iniTrimmed = ini.Trim('"', ' ', '\t');
+ string iniFieldString = iniTrimmed.Split(':')[0].ToLowerInvariant().Trim('"', ' ', '\t');
+ string iniFileString = iniTrimmed.Substring(iniFieldString.Length + 1).Trim('"', ' ', '\t');
+
+ Field = iniFieldString.AsField();
+ PopulateFromFile(iniFileString);
+ }
+
+ ///
+ /// Populate the dictionary from an INI file
+ ///
+ /// Path to INI file to populate from
+ ///
+ /// The INI file format that is supported here is not exactly the same
+ /// as a traditional one. This expects a MAME extras format, which usually
+ /// doesn't contain key value pairs and always at least contains one section
+ /// called `ROOT_FOLDER`. If that's the name of a section, then we assume
+ /// the value is boolean. If there's another section name, then that is set
+ /// as the value instead.
+ ///
+ public void PopulateFromFile(string ini)
+ {
+ // Prepare all intenral variables
+ IniReader ir = ini.GetIniReader(false);
+ bool foundRootFolder = false;
+
+ // If we got a null reader, just return
+ if (ir == null)
+ return;
+
+ // Otherwise, read the file to the end
+ try
+ {
+ ir.ReadNextLine();
+ while (!ir.EndOfStream)
+ {
+ // We don't care about whitespace or comments
+ if (ir.RowType == IniRowType.None || ir.RowType == IniRowType.Comment)
+ {
+ ir.ReadNextLine();
+ continue;
+ }
+
+ // If we have a section, just read it in
+ if (ir.RowType == IniRowType.SectionHeader)
+ {
+ ir.ReadNextLine();
+
+ // If we've found the start of the extras, set the flag
+ if (string.Equals(ir.Section, "ROOT_FOLDER", StringComparison.OrdinalIgnoreCase))
+ foundRootFolder = true;
+
+ continue;
+ }
+
+ // If we have a value, then we start populating the dictionary
+ else if (foundRootFolder)
+ {
+ // Get the key and value
+ string key = ir.Section;
+ string value = ir.Line.Trim();
+
+ // Ensure the key exists
+ if (!Mappings.ContainsKey(key))
+ Mappings[key] = new List();
+
+ // Add the new mapping
+ Mappings[key].Add(value);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Globals.Logger.Warning($"Exception found while parsing '{ini}': {ex}");
+ }
+
+ ir.Dispose();
+ }
+
+ #endregion
+ }
+}