diff --git a/BinaryObjectScanner/Protection/CopyX.cs b/BinaryObjectScanner/Protection/CopyX.cs
new file mode 100644
index 00000000..c0d54e43
--- /dev/null
+++ b/BinaryObjectScanner/Protection/CopyX.cs
@@ -0,0 +1,193 @@
+using System;
+#if NET40_OR_GREATER || NETCOREAPP
+using System.Collections.Concurrent;
+#endif
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Linq;
+using BinaryObjectScanner.Interfaces;
+using SabreTools.IO;
+using SabreTools.IO.Extensions;
+using SabreTools.Matching;
+using SabreTools.Matching.Content;
+using SabreTools.Matching.Paths;
+using SabreTools.Serialization.Wrappers;
+
+namespace BinaryObjectScanner.Protection
+{
+ // TODO: Technically not necessary, but just check for light/pro first and only if it isn't found look for the other.
+ // It should be an Or situation and not an And situation.
+ // TODO: Figure out if Light and Professional are what designate rings and rings+disccheck
+ public class CopyX : IPathCheck, IPortableExecutableCheck
+ {
+ // https://web.archive.org/web/20011016234742/http://www.optimal-online.de:80/product/copy_x.htm
+ // There are four kinds of copy-X; Light, Profesisonal, audio, and Trial Maker.
+ // Audio is for Audio CDs. Might be scannable, might not. Samples needed to confirm.
+ // No samples of Trial are known at the moment, so it can't be checked for either.
+ // There are two kinds of copy-X generally observed; those with only rings, and those with rings and a disc check.
+ // These comments assume with 0 evidence that the former is Light and the latter is Professional.
+ // Because there is no evidence, only copy-X is being returned for now. This check has pre-emptively separated
+ // the two, just for when a designation can be applied for sure.
+
+ // Overall:
+ // Whenever these comments state "at the end of" or "at the start of" pertaining to the filesystem, they refer
+ // to alphabetical order, because this is how copy-X images seem to be mastered usually (not always, but w/e).
+ // Both Light and Professional have a directory at the end of the image. The files within this directory are
+ // intersected by the physical ring.
+ // This file is usually called ZDAT, but not always. At least one instance of Light calls it ZDATA. At least one
+ // instance of Professional calls it System.
+ // Seemingly it can be anything. It doesn't help that most known samples are specifically from one company's
+ // games, Tivola. Still, most use ZDAT.
+
+ // Professional:
+ // All instances of professional contain a disc check, performed via optgraph.dll.
+ // All instances of professional contain in the directory at the end of the image 3 files. gov_[something].x64,
+ // iofile.x64, and sound.x64.
+ // Due to gov's minor name variance, sound.x64 sometimes being intersected by a ring at the start, and
+ // iofile.x64 being referenced directly in optgraph.x64, only iofile.x64 is being checked for now.
+ // TODO: optgraph.dll also contains DRM to prevent kernel debugger SoftICE from being used, via a process called
+ // SoftICE-Test. It is not currently known if this is specifically part of copy-X, or if it's an external
+ // solution employed by both copy-X and also other companies. If it's the latter, it should have its own check.
+ // It has none here since it wouldn't be necessary.
+
+ // Light:
+ // All instances of light contain 1 or more files in the directory at the end of the image. They all consist of
+ // either 0x00, or some data that matches between entries (and also is present in the 3 Professional files),
+ // except for the parts with the rings running through them.
+ // TODO: Check the last directory alphabetically and not just ZDAT*
+
+ ///
+ public string? CheckPortableExecutable(string file, PortableExecutable pex, bool includeDebug)
+ {
+
+ // Checks for Professional
+ // PEX checks intentionally only detect Professional
+
+ var sections = pex.Model.SectionTable;
+ if (sections == null)
+ return null;
+
+ if (pex.OverlayStrings != null)
+ {
+ // Checks if main executable contains reference to optgraph.dll.
+ // This might be better removed later, as Redump ID 82475 is a false positive, and also doesn't actually
+ // contain the actual optgraph.dll file.
+ // TODO: Find a way to check for situations like Redump ID 48393, where the string is spaced out with
+ // 0x00 between letters and does not show up on string checks.
+ // TODO: This might need to check every single section. Unsure until more samples are acquired.
+ // TODO: TKKG also has an NE 3.1x executable with a reference. This can be added later.
+ // Samples: Redump ID 108150
+ if (pex.OverlayStrings.Any(s => s.Contains("optgraph.dll")))
+ return "copy-X";
+ }
+
+ var strs = pex.GetFirstSectionStrings(".rdata");
+ if (strs != null)
+ {
+ // Samples: Redump ID 82475, German Emergency 2 Deluxe, Redump ID 48393
+ if (strs.Any(s => s.Contains("optgraph.dll")))
+ return "copy-X";
+ }
+
+ return null;
+ }
+
+ ///
+#if NET20 || NET35
+ public Queue CheckDirectoryPath(string path, IEnumerable? files)
+#else
+ public ConcurrentQueue CheckDirectoryPath(string path, IEnumerable? files)
+#endif
+ {
+#if NET20 || NET35
+ var protections = new Queue();
+#else
+ var protections = new ConcurrentQueue();
+#endif
+
+ // Checks for Light
+ // Directory checks intentionally only detect Light
+
+ if (files == null)
+ return protections;
+
+ // Excludes files with .x64 extension to avoid flagging Professional files.
+ // Sorts list of files in ZDAT* so just the first file gets pulled, later ones have a chance of the ring
+ // intersecting the start of the file.
+ var fileList = files.Where(f => !f.EndsWith(".x64", StringComparison.OrdinalIgnoreCase))
+ .Where(f =>
+ {
+ // TODO: Compensate for the check being run a directory or more higher
+ f = f.Remove(0, path.Length);
+ f = f.TrimStart('/', '\\');
+ return f.StartsWith("ZDAT", StringComparison.OrdinalIgnoreCase);
+ })
+ .OrderBy(f => f)
+ .ToList();
+
+ if (fileList.Count > 0)
+ {
+ try
+ {
+ using var stream = File.OpenRead(fileList[0]);
+ byte[] block = stream.ReadBytes(1024);
+
+ var matchers = new List
+ {
+ // Checks if the file contains 0x00
+ // Samples: Redump ID 81628
+ new(Enumerable.Repeat(0x00, 1024).ToArray(), "copy-X"),
+
+ // Checks for whatever this data is.
+ // Samples: Redump ID 84759, Redump ID 107929. Professional discs also have this data, hence the exclusion check.
+ new(
+ [
+ 0x02, 0xFE, 0x4A, 0x4F, 0x52, 0x4B, 0x1C, 0xE0,
+ 0x79, 0x8C, 0x7F, 0x85, 0x04, 0x00, 0x46, 0x46,
+ 0x49, 0x46, 0x07, 0xF9, 0x9F, 0xA0, 0xA1, 0x9D,
+ 0xDA, 0xB6, 0x2C, 0x2D, 0x2D, 0x2C, 0xFF, 0x00,
+ 0x6F, 0x6E, 0x71, 0x6A, 0xFC, 0x06, 0x64, 0x62,
+ 0x65, 0x5F, 0xFB, 0x06, 0x31, 0x31, 0x31, 0x31,
+ 0x00, 0x00, 0x1D, 0x1D, 0x1F, 0x1D, 0xFE, 0xFD,
+ 0x51, 0x57, 0x56, 0x51, 0xFB, 0x06, 0x33, 0x34,
+ ], "copy-X"),
+ };
+ var match = MatchUtil.GetFirstMatch(fileList[0], block, matchers, false);
+ if (!string.IsNullOrEmpty(match))
+ protections.Enqueue(match!);
+ }
+ catch { }
+ }
+
+ return protections;
+ }
+
+ ///
+ public string? CheckFilePath(string path)
+ {
+
+ // Checks for Professional
+ // File Path checks intentionally only detect Professional
+
+ var matchers = new List
+ {
+ // Samples: Redump ID 108150, Redump ID 48393
+
+ // File responsible for disc check
+ new(new FilePathMatch("optgraph.dll"), "copy-X"),
+
+ // Seemingly comorbid file, referenced in above file
+ new(new FilePathMatch("iofile.x64"), "copy-X"),
+
+ // Seemingly comorbid file
+ new(new FilePathMatch("sound.x64"), "copy-X"),
+
+ // Seemingly comorbid file
+ // Check commented out until implementation can be decided
+ // new(new FilePathMatch("gov_*.x64"), "copy-X"),
+ };
+ return MatchUtil.GetFirstMatch(path, matchers, any: true);
+ }
+ }
+}
diff --git a/README.md b/README.md
index 68ce7ce2..340fa263 100644
--- a/README.md
+++ b/README.md
@@ -73,6 +73,7 @@ Below is a list of protections detected by BinaryObjectScanner. The two columns
| Cenga ProtectDVD | True | True | |
| Channelware | True | True | Version finding and detection of later versions unimplemented |
| ChosenBytes CodeLock | True | True | Partially unconfirmed² |
+| copy-X | True | True | |
| CopyKiller | True | True | |
| CopyLok/CodeLok | True | False | |
| CrypKey | True | True | |
@@ -133,7 +134,7 @@ Below is a list of protections detected by BinaryObjectScanner. The two columns
| Sysiphus / Sysiphus DVD | True | False | |
| TAGES | True | True | Partially unconfirmed² |
| Themida/WinLicense/Code Virtualizer | True | False | Only certain products/versions currently detected |
-| Tivola Ring Protection | False | True | |
+| ~~Tivola Ring Protection~~ | False | True | Existing checks found to actually be indicators of copy-X, rather than some Tivola-specific ring protection. |
| TZCopyProtection | False | True | Partially unconfirmed² |
| Uplay | True | True | |
| Windows Media Data Session DRM | True | True | |