├── .gitattributes
├── .gitignore
├── Bin2Object.sln
├── Bin2Object
├── Attributes.cs
├── Bin2Object.csproj
├── BinaryObjectReader.cs
├── BinaryObjectStream.cs
├── BinaryObjectWriter.cs
└── Endianness.cs
├── LICENSE
├── README.md
└── Tests
├── Tests.cs
└── Tests.csproj
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.userosscache
8 | *.sln.docstates
9 |
10 | # User-specific files (MonoDevelop/Xamarin Studio)
11 | *.userprefs
12 |
13 | # Build results
14 | [Dd]ebug/
15 | [Dd]ebugPublic/
16 | [Rr]elease/
17 | [Rr]eleases/
18 | x64/
19 | x86/
20 | bld/
21 | [Bb]in/
22 | [Oo]bj/
23 | [Ll]og/
24 |
25 | # Visual Studio 2015 cache/options directory
26 | .vs/
27 | # Uncomment if you have tasks that create the project's static files in wwwroot
28 | #wwwroot/
29 |
30 | # MSTest test Results
31 | [Tt]est[Rr]esult*/
32 | [Bb]uild[Ll]og.*
33 |
34 | # NUNIT
35 | *.VisualState.xml
36 | TestResult.xml
37 |
38 | # Build Results of an ATL Project
39 | [Dd]ebugPS/
40 | [Rr]eleasePS/
41 | dlldata.c
42 |
43 | # DNX
44 | project.lock.json
45 | artifacts/
46 |
47 | *_i.c
48 | *_p.c
49 | *_i.h
50 | *.ilk
51 | *.meta
52 | *.obj
53 | *.pch
54 | *.pdb
55 | *.pgc
56 | *.pgd
57 | *.rsp
58 | *.sbr
59 | *.tlb
60 | *.tli
61 | *.tlh
62 | *.tmp
63 | *.tmp_proj
64 | *.log
65 | *.vspscc
66 | *.vssscc
67 | .builds
68 | *.pidb
69 | *.svclog
70 | *.scc
71 |
72 | # Chutzpah Test files
73 | _Chutzpah*
74 |
75 | # Visual C++ cache files
76 | ipch/
77 | *.aps
78 | *.ncb
79 | *.opendb
80 | *.opensdf
81 | *.sdf
82 | *.cachefile
83 | *.VC.db
84 | *.VC.VC.opendb
85 |
86 | # Visual Studio profiler
87 | *.psess
88 | *.vsp
89 | *.vspx
90 | *.sap
91 |
92 | # TFS 2012 Local Workspace
93 | $tf/
94 |
95 | # Guidance Automation Toolkit
96 | *.gpState
97 |
98 | # ReSharper is a .NET coding add-in
99 | _ReSharper*/
100 | *.[Rr]e[Ss]harper
101 | *.DotSettings.user
102 |
103 | # JustCode is a .NET coding add-in
104 | .JustCode
105 |
106 | # TeamCity is a build add-in
107 | _TeamCity*
108 |
109 | # DotCover is a Code Coverage Tool
110 | *.dotCover
111 |
112 | # NCrunch
113 | _NCrunch_*
114 | .*crunch*.local.xml
115 | nCrunchTemp_*
116 |
117 | # MightyMoose
118 | *.mm.*
119 | AutoTest.Net/
120 |
121 | # Web workbench (sass)
122 | .sass-cache/
123 |
124 | # Installshield output folder
125 | [Ee]xpress/
126 |
127 | # DocProject is a documentation generator add-in
128 | DocProject/buildhelp/
129 | DocProject/Help/*.HxT
130 | DocProject/Help/*.HxC
131 | DocProject/Help/*.hhc
132 | DocProject/Help/*.hhk
133 | DocProject/Help/*.hhp
134 | DocProject/Help/Html2
135 | DocProject/Help/html
136 |
137 | # Click-Once directory
138 | publish/
139 |
140 | # Publish Web Output
141 | *.[Pp]ublish.xml
142 | *.azurePubxml
143 | # TODO: Comment the next line if you want to checkin your web deploy settings
144 | # but database connection strings (with potential passwords) will be unencrypted
145 | *.pubxml
146 | *.publishproj
147 |
148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
149 | # checkin your Azure Web App publish settings, but sensitive information contained
150 | # in these scripts will be unencrypted
151 | PublishScripts/
152 |
153 | # NuGet Packages
154 | *.nupkg
155 | # The packages folder can be ignored because of Package Restore
156 | **/packages/*
157 | # except build/, which is used as an MSBuild target.
158 | !**/packages/build/
159 | # Uncomment if necessary however generally it will be regenerated when needed
160 | #!**/packages/repositories.config
161 | # NuGet v3's project.json files produces more ignoreable files
162 | *.nuget.props
163 | *.nuget.targets
164 |
165 | # Microsoft Azure Build Output
166 | csx/
167 | *.build.csdef
168 |
169 | # Microsoft Azure Emulator
170 | ecf/
171 | rcf/
172 |
173 | # Windows Store app package directories and files
174 | AppPackages/
175 | BundleArtifacts/
176 | Package.StoreAssociation.xml
177 | _pkginfo.txt
178 |
179 | # Visual Studio cache files
180 | # files ending in .cache can be ignored
181 | *.[Cc]ache
182 | # but keep track of directories ending in .cache
183 | !*.[Cc]ache/
184 |
185 | # Others
186 | ClientBin/
187 | ~$*
188 | *~
189 | *.dbmdl
190 | *.dbproj.schemaview
191 | *.pfx
192 | *.publishsettings
193 | node_modules/
194 | orleans.codegen.cs
195 |
196 | # Since there are multiple workflows, uncomment next line to ignore bower_components
197 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
198 | #bower_components/
199 |
200 | # RIA/Silverlight projects
201 | Generated_Code/
202 |
203 | # Backup & report files from converting an old project file
204 | # to a newer Visual Studio version. Backup files are not needed,
205 | # because we have git ;-)
206 | _UpgradeReport_Files/
207 | Backup*/
208 | UpgradeLog*.XML
209 | UpgradeLog*.htm
210 |
211 | # SQL Server files
212 | *.mdf
213 | *.ldf
214 |
215 | # Business Intelligence projects
216 | *.rdl.data
217 | *.bim.layout
218 | *.bim_*.settings
219 |
220 | # Microsoft Fakes
221 | FakesAssemblies/
222 |
223 | # GhostDoc plugin setting file
224 | *.GhostDoc.xml
225 |
226 | # Node.js Tools for Visual Studio
227 | .ntvs_analysis.dat
228 |
229 | # Visual Studio 6 build log
230 | *.plg
231 |
232 | # Visual Studio 6 workspace options file
233 | *.opt
234 |
235 | # Visual Studio LightSwitch build output
236 | **/*.HTMLClient/GeneratedArtifacts
237 | **/*.DesktopClient/GeneratedArtifacts
238 | **/*.DesktopClient/ModelManifest.xml
239 | **/*.Server/GeneratedArtifacts
240 | **/*.Server/ModelManifest.xml
241 | _Pvt_Extensions
242 |
243 | # Paket dependency manager
244 | .paket/paket.exe
245 | paket-files/
246 |
247 | # FAKE - F# Make
248 | .fake/
249 |
250 | # JetBrains Rider
251 | .idea/
252 | *.sln.iml
253 |
--------------------------------------------------------------------------------
/Bin2Object.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.29411.108
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bin2Object", "Bin2Object\Bin2Object.csproj", "{865AA871-F000-4794-A28A-17EAB5D53579}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{D5EEAFD0-6721-4482-97A3-B7A1FDE300AE}"
9 | ProjectSection(ProjectDependencies) = postProject
10 | {865AA871-F000-4794-A28A-17EAB5D53579} = {865AA871-F000-4794-A28A-17EAB5D53579}
11 | EndProjectSection
12 | EndProject
13 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6531D379-7A7E-4C66-B98F-C1E7D4F83009}"
14 | ProjectSection(SolutionItems) = preProject
15 | LICENSE = LICENSE
16 | README.md = README.md
17 | EndProjectSection
18 | EndProject
19 | Global
20 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
21 | Debug|Any CPU = Debug|Any CPU
22 | Release|Any CPU = Release|Any CPU
23 | EndGlobalSection
24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
25 | {865AA871-F000-4794-A28A-17EAB5D53579}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
26 | {865AA871-F000-4794-A28A-17EAB5D53579}.Debug|Any CPU.Build.0 = Debug|Any CPU
27 | {865AA871-F000-4794-A28A-17EAB5D53579}.Release|Any CPU.ActiveCfg = Release|Any CPU
28 | {865AA871-F000-4794-A28A-17EAB5D53579}.Release|Any CPU.Build.0 = Release|Any CPU
29 | {D5EEAFD0-6721-4482-97A3-B7A1FDE300AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
30 | {D5EEAFD0-6721-4482-97A3-B7A1FDE300AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
31 | {D5EEAFD0-6721-4482-97A3-B7A1FDE300AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
32 | {D5EEAFD0-6721-4482-97A3-B7A1FDE300AE}.Release|Any CPU.Build.0 = Release|Any CPU
33 | EndGlobalSection
34 | GlobalSection(SolutionProperties) = preSolution
35 | HideSolutionNode = FALSE
36 | EndGlobalSection
37 | GlobalSection(ExtensibilityGlobals) = postSolution
38 | SolutionGuid = {7734C402-03EE-47BE-A869-058DB5683DF9}
39 | EndGlobalSection
40 | EndGlobal
41 |
--------------------------------------------------------------------------------
/Bin2Object/Attributes.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2016 Perfare - https://github.com/Perfare/Il2CppDumper/
2 | // Copyright (c) 2016 Alican Çubukçuoğlu - https://github.com/AlicanC/AlicanC-s-Modern-Warfare-2-Tool/
3 | // Copyright (c) 2017-2021 Katy Coe - http://www.djkaty.com - https://github.com/djkaty/Bin2Object/
4 |
5 | using System;
6 |
7 | namespace NoisyCowStudios.Bin2Object
8 | {
9 | [AttributeUsage(AttributeTargets.Field)]
10 | public class ArrayLengthAttribute : Attribute
11 | {
12 | public string FieldName { get; set; }
13 | public int FixedSize { get; set; }
14 | }
15 |
16 | [AttributeUsage(AttributeTargets.Field)]
17 | public class StringAttribute : Attribute
18 | {
19 | public bool IsNullTerminated { get; set; }
20 | public int FixedSize { get; set; }
21 | }
22 |
23 | [AttributeUsage(AttributeTargets.Field, AllowMultiple = true)]
24 | public class VersionAttribute : Attribute
25 | {
26 | public double Min { get; set; } = -1;
27 | public double Max { get; set; } = -1;
28 | }
29 |
30 | [AttributeUsage(AttributeTargets.Field)]
31 | public class SkipWhenReadingAttribute : Attribute { }
32 | }
33 |
--------------------------------------------------------------------------------
/Bin2Object/Bin2Object.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.1
5 | any
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/Bin2Object/BinaryObjectReader.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2016 Perfare - https://github.com/Perfare/Il2CppDumper/
2 | // Copyright (c) 2016 Alican Çubukçuoğlu - https://github.com/AlicanC/AlicanC-s-Modern-Warfare-2-Tool/
3 | // Copyright (c) 2017-2021 Katy Coe - http://www.djkaty.com - https://github.com/djkaty/Bin2Object/
4 |
5 | using System;
6 | using System.Collections.Generic;
7 | using System.IO;
8 | using System.Linq;
9 | using System.Reflection;
10 | using System.Text;
11 |
12 | namespace NoisyCowStudios.Bin2Object
13 | {
14 | public class BinaryObjectReader : BinaryReader
15 | {
16 | // Method cache for primitive mappings
17 | private Dictionary readMethodCache { get; }
18 |
19 | // Generic method cache to dramatically speed up repeated calls to ReadObject with the same T
20 | private Dictionary readObjectGenericCache = new Dictionary();
21 |
22 | // VersionAttribute cache to dramatically speed up repeated calls to ReadObject with the same T
23 | private Dictionary>> readObjectVersionCache = new Dictionary>>();
24 |
25 | // Thread synchronization objects (for thread safety)
26 | private object readLock = new object();
27 |
28 | // Initialization
29 | public BinaryObjectReader(Stream stream, Endianness endianness = Endianness.Little, bool leaveOpen = false) : base(stream, Encoding.Default, leaveOpen) {
30 | Endianness = endianness;
31 |
32 | readMethodCache = typeof(BinaryObjectReader).GetMethods().Where(m => m.Name.StartsWith("Read") && !m.GetParameters().Any()).GroupBy(m => m.ReturnType).ToDictionary(kv => kv.Key.Name, kv => kv.First());
33 | }
34 |
35 | // Position in the stream
36 | public long Position {
37 | get => BaseStream.Position;
38 | set => BaseStream.Position = value;
39 | }
40 |
41 | // Allows you to specify primitive types which should be read as different types in the stream
42 | // Key: type in object; Value: type in stream
43 | public Dictionary PrimitiveMappings { get; } = new Dictionary();
44 |
45 | // Allows you to specify object types which should be read as different types in the stream
46 | // The fields of the read object will be copied to the fields of the target object with matching names
47 | // Key: object type to return; Value: object type to read from stream
48 | public Dictionary ObjectMappings { get; } = new Dictionary();
49 |
50 | public Endianness Endianness { get; set; }
51 |
52 | public double Version { get; set; } = 1;
53 |
54 | public Encoding Encoding { get; set; } = Encoding.UTF8;
55 |
56 | public override byte[] ReadBytes(int count) {
57 | var bytes = base.ReadBytes(count);
58 | return (Endianness == Endianness.Little ? bytes : bytes.Reverse().ToArray());
59 | }
60 |
61 | public override long ReadInt64() => BitConverter.ToInt64(ReadBytes(8), 0);
62 |
63 | public override ulong ReadUInt64() => BitConverter.ToUInt64(ReadBytes(8), 0);
64 |
65 | public override int ReadInt32() => BitConverter.ToInt32(ReadBytes(4), 0);
66 |
67 | public override uint ReadUInt32() => BitConverter.ToUInt32(ReadBytes(4), 0);
68 |
69 | public override short ReadInt16() => BitConverter.ToInt16(ReadBytes(2), 0);
70 |
71 | public override ushort ReadUInt16() => BitConverter.ToUInt16(ReadBytes(2), 0);
72 |
73 | public byte[] ReadBytes(long addr, int count) {
74 | lock (readLock) {
75 | Position = addr;
76 | return ReadBytes(count);
77 | }
78 | }
79 |
80 | public long ReadInt64(long addr) {
81 | lock (readLock) {
82 | Position = addr;
83 | return ReadInt64();
84 | }
85 | }
86 |
87 | public ulong ReadUInt64(long addr) {
88 | lock (readLock) {
89 | Position = addr;
90 | return ReadUInt64();
91 | }
92 | }
93 |
94 | public int ReadInt32(long addr) {
95 | lock (readLock) {
96 | Position = addr;
97 | return ReadInt32();
98 | }
99 | }
100 |
101 | public uint ReadUInt32(long addr) {
102 | lock (readLock) {
103 | Position = addr;
104 | return ReadUInt32();
105 | }
106 | }
107 |
108 | public short ReadInt16(long addr) {
109 | lock (readLock) {
110 | Position = addr;
111 | return ReadInt16();
112 | }
113 | }
114 |
115 | public ushort ReadUInt16(long addr) {
116 | lock (readLock) {
117 | Position = addr;
118 | return ReadUInt16();
119 | }
120 | }
121 |
122 | public byte ReadByte(long addr) {
123 | lock (readLock) {
124 | Position = addr;
125 | return ReadByte();
126 | }
127 | }
128 |
129 | public bool ReadBoolean(long addr) {
130 | lock (readLock) {
131 | Position = addr;
132 | return ReadBoolean();
133 | }
134 | }
135 |
136 | public T ReadObject(long addr) where T : new() {
137 | lock (readLock) {
138 | Position = addr;
139 | return ReadObject();
140 | }
141 | }
142 |
143 | private object ReadPrimitive(Type t) {
144 |
145 | // Checked for mapped primitive types
146 | if (PrimitiveMappings.TryGetValue(t.Name, out Type mapping)) {
147 | var mappedReader = readMethodCache[mapping.Name];
148 | var result = mappedReader.Invoke(this, null);
149 | return Convert.ChangeType(result, t);
150 | }
151 |
152 | // Unmapped primitive (eliminating obj causes Visual Studio 16.3.5 to crash)
153 | object obj = t.Name switch {
154 | "Int64" => ReadInt64(),
155 | "UInt64" => ReadUInt64(),
156 | "Int32" => ReadInt32(),
157 | "UInt32" => ReadUInt32(),
158 | "Int16" => ReadInt16(),
159 | "UInt16" => ReadUInt16(),
160 | "Byte" => ReadByte(),
161 | "Boolean" => ReadBoolean(),
162 | _ => throw new ArgumentException("Unsupported primitive type specified: " + t.FullName)
163 | };
164 | return obj;
165 | }
166 |
167 | // Copy matching named fields from one object to another
168 | // Fields in the source not in the target and vice versa are ignored
169 | private T MapObject(object from) where T : new() {
170 | var t = new T();
171 |
172 | var fromType = from.GetType();
173 | var toTypeFields = typeof(T).GetFields();
174 |
175 | // Iterate source fields
176 | foreach (var f in fromType.GetFields()) {
177 |
178 | // Find target field with matching name - ignore the rest
179 | if (toTypeFields.FirstOrDefault(tf => tf.Name == f.Name) is FieldInfo targetField) {
180 | targetField.SetValue(t, Convert.ChangeType(f.GetValue(from), targetField.FieldType));
181 | }
182 | }
183 | return t;
184 | }
185 |
186 | public T ReadObject() where T : new() {
187 |
188 | var type = typeof(T);
189 |
190 | // Object is actually a primitive
191 | if (type.IsPrimitive) {
192 | return (T) ReadPrimitive(type);
193 | }
194 |
195 | // Check for object mapping
196 | if (ObjectMappings.TryGetValue(type, out var streamType)) {
197 | if (!readObjectGenericCache.TryGetValue(streamType.FullName, out MethodInfo mi2)) {
198 | var us = GetType().GetMethod("ReadObject", Type.EmptyTypes);
199 | mi2 = us.MakeGenericMethod(streamType);
200 | readObjectGenericCache.Add(streamType.FullName, mi2);
201 | }
202 | var obj = mi2.Invoke(this, null);
203 |
204 | return MapObject(obj);
205 | }
206 |
207 | var t = new T();
208 |
209 | // First time caching
210 | if (!readObjectVersionCache.ContainsKey(type)) {
211 | var fields = new Dictionary>();
212 | foreach (var i in type.GetFields())
213 | if (i.GetCustomAttribute(false) is SkipWhenReadingAttribute)
214 | fields.Add(i, new List<(double, double)> { (-2, -2) });
215 | else
216 | fields.Add(i, i.GetCustomAttributes(false).Select(v => (v.Min, v.Max)).ToList());
217 |
218 | readObjectVersionCache.Add(type, fields);
219 | }
220 |
221 | foreach (var (i, versions) in readObjectVersionCache[type]) {
222 | // Skip fields with SkipWhenReading set
223 | if (versions.FirstOrDefault() == (-2, -2))
224 | continue;
225 |
226 | // Only process fields for our selected object versioning (always process if none supplied)
227 | if (versions.Any() && !versions.Any(v => (v.Min <= Version || v.Min == -1) && (v.Max >= Version || v.Max == -1)))
228 | continue;
229 |
230 | // String
231 | if (i.FieldType == typeof(string)) {
232 | var attr = i.GetCustomAttribute(false);
233 |
234 | // No String attribute? Use a null-terminated string by default
235 | if (attr == null || attr.IsNullTerminated)
236 | i.SetValue(t, ReadNullTerminatedString());
237 | else {
238 | if (attr.FixedSize <= 0)
239 | throw new ArgumentException("String attribute for array field " + i.Name + " configuration invalid");
240 | i.SetValue(t, ReadFixedLengthString(attr.FixedSize));
241 | }
242 | }
243 |
244 | // Array
245 | else if (i.FieldType.IsArray) {
246 | var attr = i.GetCustomAttribute(false) ??
247 | throw new InvalidOperationException("Array field " + i.Name + " must have ArrayLength attribute");
248 |
249 | int lengthPrimitive;
250 |
251 | if (attr.FieldName != null) {
252 | var field = type.GetField(attr.FieldName) ??
253 | throw new ArgumentException("Array field " + i.Name + " has invalid FieldName in ArrayLength attribute");
254 | lengthPrimitive = Convert.ToInt32(field.GetValue(t));
255 | }
256 | else if (attr.FixedSize > 0) {
257 | lengthPrimitive = attr.FixedSize;
258 | }
259 | else {
260 | throw new ArgumentException("ArrayLength attribute for array field " + i.Name + " configuration invalid");
261 | }
262 |
263 | var us = GetType().GetMethod("ReadArray", new[] {typeof(int)});
264 | var mi2 = us.MakeGenericMethod(i.FieldType.GetElementType());
265 | i.SetValue(t, mi2.Invoke(this, new object[] { lengthPrimitive }));
266 | }
267 |
268 | // Primitive type
269 | // This is unnecessary but saves on many generic Invoke calls which are really slow
270 | else if (i.FieldType.IsPrimitive) {
271 | i.SetValue(t, ReadPrimitive(i.FieldType));
272 | }
273 |
274 | // Object
275 | else {
276 | if (!readObjectGenericCache.TryGetValue(i.FieldType.FullName, out MethodInfo mi2)) {
277 | var us = GetType().GetMethod("ReadObject", Type.EmptyTypes);
278 | mi2 = us.MakeGenericMethod(i.FieldType);
279 | readObjectGenericCache.Add(i.FieldType.FullName, mi2);
280 | }
281 | i.SetValue(t, mi2.Invoke(this, null));
282 | }
283 | }
284 | return t;
285 | }
286 |
287 | public T[] ReadArray(long addr, int count) where T : new() {
288 | lock (readLock) {
289 | Position = addr;
290 | return ReadArray(count);
291 | }
292 | }
293 |
294 | public T[] ReadArray(int count) where T : new() {
295 | T[] t = new T[count];
296 |
297 | if (!typeof(T).IsPrimitive) {
298 | for (int i = 0; i < count; i++)
299 | t[i] = ReadObject();
300 | } else {
301 | var type = typeof(T);
302 |
303 | // Checked for mapped primitive types
304 | if (PrimitiveMappings.TryGetValue(type.Name, out Type mapping)) {
305 | var mappedReader = readMethodCache[mapping.Name];
306 |
307 | for (var i = 0; i < count; i++)
308 | t[i] = (T) Convert.ChangeType(mappedReader.Invoke(this, null), type);
309 | } else {
310 | // Unmapped primitive (eliminating obj causes Visual Studio 16.3.5 to crash)
311 | for (var i = 0; i < count; i++)
312 | t[i] = (T) (type.Name switch {
313 | "Int64" => (object) ReadInt64(),
314 | "UInt64" => ReadUInt64(),
315 | "Int32" => ReadInt32(),
316 | "UInt32" => ReadUInt32(),
317 | "Int16" => ReadInt16(),
318 | "UInt16" => ReadUInt16(),
319 | "Byte" => ReadByte(),
320 | "Boolean" => ReadBoolean() ? 1ul : 0ul,
321 | _ => throw new ArgumentException("Unsupported primitive type specified: " + type.FullName)
322 | });
323 | }
324 | }
325 | return t;
326 | }
327 |
328 | public string ReadNullTerminatedString(long addr, Encoding encoding = null) {
329 | lock (readLock) {
330 | Position = addr;
331 | return ReadNullTerminatedString(encoding);
332 | }
333 | }
334 |
335 | public string ReadNullTerminatedString(Encoding encoding = null) {
336 | List bytes = new List();
337 | byte b;
338 | while ((b = ReadByte()) != 0)
339 | bytes.Add(b);
340 | return encoding?.GetString(bytes.ToArray()) ?? Encoding.GetString(bytes.ToArray());
341 | }
342 |
343 | public string ReadFixedLengthString(long addr, int length, Encoding encoding = null) {
344 | lock (readLock) {
345 | Position = addr;
346 | return ReadFixedLengthString(length, encoding);
347 | }
348 | }
349 |
350 | public string ReadFixedLengthString(int length, Encoding encoding = null) {
351 | byte[] b = ReadArray(length);
352 | List bytes = new List();
353 | foreach (var c in b)
354 | if (c == 0)
355 | break;
356 | else
357 | bytes.Add(c);
358 | return encoding?.GetString(bytes.ToArray()) ?? Encoding.GetString(bytes.ToArray());
359 | }
360 | }
361 | }
362 |
--------------------------------------------------------------------------------
/Bin2Object/BinaryObjectStream.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty/Bin2Object/
2 |
3 | using System;
4 | using System.Collections.Generic;
5 | using System.IO;
6 | using System.Text;
7 |
8 | namespace NoisyCowStudios.Bin2Object
9 | {
10 | // Combined BinaryObjectReader and BinaryObjectWriter that works with a MemoryStream
11 | public class BinaryObjectStream : MemoryStream {
12 | // The reader and writer
13 | public BinaryObjectReader Reader { get; }
14 | public BinaryObjectWriter Writer { get; }
15 |
16 | public Endianness Endianness {
17 | get => Reader.Endianness;
18 | set {
19 | Reader.Endianness = value;
20 | Writer.Endianness = value;
21 | }
22 | }
23 |
24 | public double Version {
25 | get => Reader.Version;
26 | set {
27 | Reader.Version = value;
28 | Writer.Version = value;
29 | }
30 | }
31 |
32 | public Encoding Encoding {
33 | get => Reader.Encoding;
34 | set {
35 | Reader.Encoding = value;
36 | Writer.Encoding = value;
37 | }
38 | }
39 |
40 | // Add a primitive mapping to the reader and writer
41 | public void AddPrimitiveMapping(Type objType, Type streamType) {
42 | Reader.PrimitiveMappings.Add(objType.Name, streamType);
43 | Writer.PrimitiveMappings.Add(objType, streamType);
44 | }
45 |
46 | // Add an object mapping to the reader
47 | public void AddObjectMapping(Type objType, Type streamType) {
48 | Reader.ObjectMappings.Add(objType, streamType);
49 | }
50 |
51 | // Create reader/writer
52 | public BinaryObjectStream(byte[] bytes, Endianness endianness = Endianness.Little, bool leaveOpen = false) : base(bytes) {
53 | Reader = new BinaryObjectReader(this, endianness, leaveOpen);
54 | Writer = new BinaryObjectWriter(this, endianness, leaveOpen);
55 | }
56 | public BinaryObjectStream(Endianness endianness = Endianness.Little, bool leaveOpen = false) {
57 | Reader = new BinaryObjectReader(this, endianness, leaveOpen);
58 | Writer = new BinaryObjectWriter(this, endianness, leaveOpen);
59 | }
60 |
61 | // Surrogate methods
62 | public byte[] ReadBytes(int count) => Reader.ReadBytes(count);
63 | public long ReadInt64() => Reader.ReadInt64();
64 | public ulong ReadUInt64() => Reader.ReadUInt64();
65 | public int ReadInt32() => Reader.ReadInt32();
66 | public uint ReadUInt32() => Reader.ReadUInt32();
67 | public short ReadInt16() => Reader.ReadInt16();
68 | public ushort ReadUInt16() => Reader.ReadUInt16();
69 | public bool ReadBoolean() => Reader.ReadBoolean();
70 | public float ReadSingle() => Reader.ReadSingle();
71 | public double ReadDouble() => Reader.ReadDouble();
72 | public new byte ReadByte() => Reader.ReadByte();
73 |
74 | public byte[] ReadBytes(long addr, int count) => Reader.ReadBytes(addr, count);
75 | public long ReadInt64(long addr) => Reader.ReadInt64(addr);
76 | public ulong ReadUInt64(long addr) => Reader.ReadUInt64(addr);
77 | public int ReadInt32(long addr) => Reader.ReadInt32(addr);
78 | public uint ReadUInt32(long addr) => Reader.ReadUInt32(addr);
79 | public short ReadInt16(long addr) => Reader.ReadInt16(addr);
80 | public ushort ReadUInt16(long addr) => Reader.ReadUInt16(addr);
81 | public byte ReadByte(long addr) => Reader.ReadByte(addr);
82 | public bool ReadBoolean(long addr) => Reader.ReadBoolean(addr);
83 |
84 | public T ReadObject(long addr) where T : new() => Reader.ReadObject(addr);
85 | public T ReadObject() where T : new() => Reader.ReadObject();
86 |
87 | public T[] ReadArray(long addr, int count) where T : new() => Reader.ReadArray(addr, count);
88 | public T[] ReadArray(int count) where T : new() => Reader.ReadArray(count);
89 |
90 | public string ReadNullTerminatedString(long addr, Encoding encoding = null) => Reader.ReadNullTerminatedString(addr, encoding);
91 | public string ReadNullTerminatedString(Encoding encoding = null) => Reader.ReadNullTerminatedString(encoding);
92 | public string ReadFixedLengthString(long addr, int length, Encoding encoding = null) => Reader.ReadFixedLengthString(addr, length, encoding);
93 | public string ReadFixedLengthString(int length, Encoding encoding = null) => Reader.ReadFixedLengthString(length, encoding);
94 |
95 | public void WriteEndianBytes(byte[] bytes) => Writer.WriteEndianBytes(bytes);
96 | public void Write(long int64) => Writer.Write(int64);
97 | public void Write(ulong uint64) => Writer.Write(uint64);
98 | public void Write(int int32) => Writer.Write(int32);
99 | public void Write(uint uint32) => Writer.Write(uint32);
100 | public void Write(short int16) => Writer.Write(int16);
101 | public void Write(ushort uint16) => Writer.Write(uint16);
102 |
103 | public void Write(long addr, byte[] bytes) => Writer.Write(addr, bytes);
104 | public void Write(long addr, long int64) => Writer.Write(addr, int64);
105 | public void Write(long addr, ulong uint64) => Writer.Write(addr, uint64);
106 | public void Write(long addr, int int32) => Writer.Write(addr, int32);
107 | public void Write(long addr, uint uint32) => Writer.Write(addr, uint32);
108 | public void Write(long addr, short int16) => Writer.Write(addr, int16);
109 | public void Write(long addr, ushort uint16) => Writer.Write(addr, uint16);
110 | public void Write(long addr, byte value) => Writer.Write(addr, value);
111 | public void Write(long addr, bool value) => Writer.Write(addr, value);
112 |
113 | public void WriteObject(long addr, T obj) => Writer.WriteObject(addr, obj);
114 | public void WriteObject(T obj) => Writer.WriteObject(obj);
115 |
116 | public void WriteArray(long addr, T[] array) => Writer.WriteArray(addr, array);
117 | public void WriteArray(T[] array) => Writer.WriteArray(array);
118 |
119 | public void WriteNullTerminatedString(long addr, string str, Encoding encoding = null) => Writer.WriteNullTerminatedString(addr, str, encoding);
120 | public void WriteNullTerminatedString(string str, Encoding encoding = null) => Writer.WriteNullTerminatedString(str, encoding);
121 | public void WriteFixedLengthString(long addr, string str, int size = -1, Encoding encoding = null) => Writer.WriteFixedLengthString(addr, str, size, encoding);
122 | public void WriteFixedLengthString(string str, int size = -1, Encoding encoding = null) => Writer.WriteFixedLengthString(str, size, encoding);
123 | }
124 | }
125 |
126 |
--------------------------------------------------------------------------------
/Bin2Object/BinaryObjectWriter.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Katy Coe - http://www.djkaty.com - https://github.com/djkaty/Bin2Object/
2 |
3 | using System;
4 | using System.Collections.Generic;
5 | using System.IO;
6 | using System.Linq;
7 | using System.Reflection;
8 | using System.Text;
9 |
10 | namespace NoisyCowStudios.Bin2Object
11 | {
12 | public class BinaryObjectWriter : BinaryWriter
13 | {
14 | // Generic method cache to dramatically speed up repeated calls to WriteObject with the same T
15 | private Dictionary writeObjectGenericCache = new Dictionary();
16 |
17 | // VersionAttribute cache to dramatically speed up repeated calls to ReadObject with the same T
18 | private Dictionary>> writeObjectVersionCache = new Dictionary>>();
19 |
20 | // Thread synchronization objects (for thread safety)
21 | private object writeLock = new object();
22 |
23 | // Initialization
24 | public BinaryObjectWriter(Stream stream, Endianness endianness = Endianness.Little, bool leaveOpen = false) : base(stream, Encoding.Default, leaveOpen) {
25 | Endianness = endianness;
26 | }
27 |
28 | // Position in the stream
29 | public long Position {
30 | get => BaseStream.Position;
31 | set => BaseStream.Position = value;
32 | }
33 |
34 | // Allows you to specify types which should be written as different types to the stream
35 | // Key: type in object; Value: type in stream
36 | public Dictionary PrimitiveMappings { get; } = new Dictionary();
37 |
38 | public Endianness Endianness { get; set; }
39 |
40 | public double Version { get; set; } = 1;
41 |
42 | public Encoding Encoding { get; set; } = Encoding.UTF8;
43 |
44 | public void WriteEndianBytes(byte[] bytes) {
45 | Write(Endianness == Endianness.Little ? bytes : bytes.Reverse().ToArray());
46 | }
47 |
48 | public override void Write(long int64) => WriteEndianBytes(BitConverter.GetBytes(int64));
49 |
50 | public override void Write(ulong uint64) => WriteEndianBytes(BitConverter.GetBytes(uint64));
51 |
52 | public override void Write(int int32) => WriteEndianBytes(BitConverter.GetBytes(int32));
53 |
54 | public override void Write(uint uint32) => WriteEndianBytes(BitConverter.GetBytes(uint32));
55 |
56 | public override void Write(short int16) => WriteEndianBytes(BitConverter.GetBytes(int16));
57 |
58 | public override void Write(ushort uint16) => WriteEndianBytes(BitConverter.GetBytes(uint16));
59 |
60 | public void Write(long addr, byte[] bytes) {
61 | lock (writeLock) {
62 | Position = addr;
63 | WriteEndianBytes(bytes);
64 | }
65 | }
66 |
67 | public void Write(long addr, long int64) {
68 | lock (writeLock) {
69 | Position = addr;
70 | Write(int64);
71 | }
72 | }
73 |
74 | public void Write(long addr, ulong uint64) {
75 | lock (writeLock) {
76 | Position = addr;
77 | Write(uint64);
78 | }
79 | }
80 |
81 | public void Write(long addr, int int32) {
82 | lock (writeLock) {
83 | Position = addr;
84 | Write(int32);
85 | }
86 | }
87 |
88 | public void Write(long addr, uint uint32) {
89 | lock (writeLock) {
90 | Position = addr;
91 | Write(uint32);
92 | }
93 | }
94 |
95 | public void Write(long addr, short int16) {
96 | lock (writeLock) {
97 | Position = addr;
98 | Write(int16);
99 | }
100 | }
101 |
102 | public void Write(long addr, ushort uint16) {
103 | lock (writeLock) {
104 | Position = addr;
105 | Write(uint16);
106 | }
107 | }
108 |
109 | public void Write(long addr, byte value) {
110 | lock (writeLock) {
111 | Position = addr;
112 | Write(value);
113 | }
114 | }
115 |
116 | public void Write(long addr, bool value) {
117 | lock (writeLock) {
118 | Position = addr;
119 | Write(value);
120 | }
121 | }
122 |
123 | public void WriteObject(long addr, T obj) {
124 | lock (writeLock) {
125 | Position = addr;
126 | WriteObject(obj);
127 | }
128 | }
129 |
130 | public void WriteObject(T obj) {
131 | var type = typeof(T);
132 | var ti = type.GetTypeInfo();
133 |
134 | if (ti.IsPrimitive) {
135 | // Checked for mapped primitive types
136 | if ((from m in PrimitiveMappings where m.Key.GetTypeInfo().Name == type.Name select m.Value).FirstOrDefault() is Type mapping) {
137 | var mappedWriter = (from m in GetType().GetMethods() where m.Name == "Write" && m.GetParameters()[0].ParameterType == mapping && m.ReturnType == typeof(void) select m).FirstOrDefault();
138 | mappedWriter?.Invoke(this, new object[] { obj });
139 | return;
140 | }
141 |
142 | // Unmapped primitive
143 | switch (obj) {
144 | case long v:
145 | Write(v);
146 | break;
147 | case ulong v:
148 | Write(v);
149 | break;
150 | case int v:
151 | Write(v);
152 | break;
153 | case uint v:
154 | Write(v);
155 | break;
156 | case short v:
157 | Write(v);
158 | break;
159 | case ushort v:
160 | Write(v);
161 | break;
162 | case byte v:
163 | Write(v);
164 | break;
165 | case bool v:
166 | Write(v);
167 | break;
168 | default:
169 | throw new ArgumentException("Unsupported primitive type specified: " + type.FullName);
170 | }
171 | return;
172 | }
173 |
174 | // First time caching
175 | if (!writeObjectVersionCache.ContainsKey(type)) {
176 | var fields = new Dictionary>();
177 | foreach (var i in type.GetFields())
178 | fields.Add(i, i.GetCustomAttributes(false).Select(v => (v.Min, v.Max)).ToList());
179 |
180 | writeObjectVersionCache.Add(type, fields);
181 | }
182 |
183 | foreach (var (i, versions) in writeObjectVersionCache[type]) {
184 | // Only process fields for our selected object versioning (always process if none supplied)
185 | if (versions.Any() && !versions.Any(v => (v.Min <= Version || v.Min == -1) && (v.Max >= Version || v.Max == -1)))
186 | continue;
187 |
188 | // String
189 | if (i.FieldType == typeof(string)) {
190 | var attr = i.GetCustomAttribute(false);
191 |
192 | // No String attribute? Use a null-terminated string by default
193 | if (attr == null || attr.IsNullTerminated)
194 | WriteNullTerminatedString((string) i.GetValue(obj));
195 | else {
196 | if (attr.FixedSize <= 0)
197 | throw new ArgumentException("String attribute for array field " + i.Name + " configuration invalid");
198 | WriteFixedLengthString((string) i.GetValue(obj), attr.FixedSize);
199 | }
200 | }
201 |
202 | // Array
203 | else if (i.FieldType.IsArray) {
204 | var attr = i.GetCustomAttribute(false) ??
205 | throw new InvalidOperationException("Array field " + i.Name + " must have ArrayLength attribute");
206 |
207 | int lengthPrimitive;
208 |
209 | if (attr.FieldName != null) {
210 | var field = type.GetField(attr.FieldName) ??
211 | throw new ArgumentException("Array field " + i.Name + " has invalid FieldName in ArrayLength attribute");
212 | lengthPrimitive = Convert.ToInt32(field.GetValue(obj));
213 | } else if (attr.FixedSize > 0) {
214 | lengthPrimitive = attr.FixedSize;
215 | } else {
216 | throw new ArgumentException("ArrayLength attribute for array field " + i.Name + " configuration invalid");
217 | }
218 |
219 | var arr = i.GetValue(obj);
220 | var us = GetType().GetMethods().Where(m => m.Name == "WriteArray" && m.GetParameters().Length == 1 && m.IsGenericMethodDefinition).Single();
221 | var mi2 = us.MakeGenericMethod(i.FieldType.GetElementType());
222 | mi2.Invoke(this, new object[] { arr });
223 | }
224 |
225 | // Primitive type
226 | // This is unnecessary but saves on many generic Invoke calls which are really slow
227 | else if (i.FieldType.IsPrimitive) {
228 | // Checked for mapped primitive types
229 | if ((from m in PrimitiveMappings where m.Key.GetTypeInfo().Name == i.FieldType.Name select m.Value).FirstOrDefault() is Type mapping) {
230 | var mappedWriter = (from m in GetType().GetMethods() where m.Name == "Write" && m.GetParameters()[0].ParameterType == mapping && m.ReturnType == typeof(void) select m).FirstOrDefault();
231 | mappedWriter?.Invoke(this, new object[] { Convert.ChangeType(i.GetValue(obj), mapping) });
232 | } else {
233 | // Unmapped primitive type
234 | switch (i.GetValue(obj)) {
235 | case long v:
236 | Write(v);
237 | break;
238 | case ulong v:
239 | Write(v);
240 | break;
241 | case int v:
242 | Write(v);
243 | break;
244 | case uint v:
245 | Write(v);
246 | break;
247 | case short v:
248 | Write(v);
249 | break;
250 | case ushort v:
251 | Write(v);
252 | break;
253 | case byte v:
254 | Write(v);
255 | break;
256 | case bool v:
257 | Write(v);
258 | break;
259 | default:
260 | throw new ArgumentException("Unsupported primitive type specified: " + type.FullName);
261 | }
262 | }
263 | }
264 |
265 | // Object
266 | else {
267 | if (!writeObjectGenericCache.TryGetValue(i.FieldType.FullName, out MethodInfo mi2)) {
268 | var us = GetType().GetMethods().Where(m => m.Name == "WriteObject" && m.IsGenericMethodDefinition).Single();
269 | mi2 = us.MakeGenericMethod(i.FieldType);
270 | writeObjectGenericCache.Add(i.FieldType.FullName, mi2);
271 | }
272 | mi2.Invoke(this, new[] { i.GetValue(obj) });
273 | }
274 | }
275 | }
276 |
277 | public void WriteArray(long addr, T[] array) {
278 | lock (writeLock) {
279 | Position = addr;
280 | WriteArray(array);
281 | }
282 | }
283 |
284 | public void WriteArray(T[] array) {
285 | for (int i = 0; i < array.Length; i++) {
286 | WriteObject(array[i]);
287 | }
288 | }
289 |
290 | public void WriteNullTerminatedString(long addr, string str, Encoding encoding = null) {
291 | lock (writeLock) {
292 | Position = addr;
293 | WriteNullTerminatedString(str, encoding);
294 | }
295 | }
296 |
297 | public void WriteNullTerminatedString(string str, Encoding encoding = null) {
298 | WriteFixedLengthString(str, str.Length + 1, encoding);
299 | }
300 |
301 | // The difference between this and BinaryWriter.Write(string) is that the latter adds a length prefix before the string
302 | public void WriteFixedLengthString(long addr, string str, int size = -1, Encoding encoding = null) {
303 | lock (writeLock) {
304 | Position = addr;
305 | WriteFixedLengthString(str, size, encoding);
306 | }
307 | }
308 |
309 | public void WriteFixedLengthString(string str, int size = -1, Encoding encoding = null) {
310 | var bytes = encoding?.GetBytes(str) ?? Encoding.GetBytes(str);
311 | Write(bytes);
312 |
313 | if (size != -1)
314 | for (var padding = str.Length; padding < size; padding++)
315 | Write((byte) 0);
316 | }
317 | }
318 | }
319 |
--------------------------------------------------------------------------------
/Bin2Object/Endianness.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017-2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty/Bin2Object/
2 |
3 | namespace NoisyCowStudios.Bin2Object
4 | {
5 | public enum Endianness
6 | {
7 | Little,
8 | Big
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Perfare - https://github.com/Perfare/Il2CppDumper/
4 | Copyright (c) 2016 Alican Cubukcuoglu - https://github.com/AlicanC/AlicanC-s-Modern-Warfare-2-Tool/
5 | Copyright (c) 2017-2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty/Bin2Object/
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Bin2Object
2 | A BinaryReader and BinaryWriter which can serialize and deserialize arbitrary binary formats into objects and vice versa.
3 |
4 | Built against .NET Standard 2.1 (.NET Core 3.1).
5 |
6 | See Tests\Tests.cs for usage examples.
7 |
8 | www.djkaty.com
9 |
10 | Copyright © 2016 Perfare - https://github.com/Perfare/Il2CppDumper/
11 |
12 | Copyright © 2016 Alican Cubukcuoglu - https://github.com/AlicanC/AlicanC-s-Modern-Warfare-2-Tool/
13 |
14 | Copyright © 2017-2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty/Bin2Object/
15 |
--------------------------------------------------------------------------------
/Tests/Tests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017-2019 Katy Coe - http://www.djkaty.com - https://github.com/djkaty/Bin2Object/
2 |
3 | using System;
4 | using System.IO;
5 | using NUnit.Framework;
6 | using NoisyCowStudios.Bin2Object;
7 | using System.Linq;
8 |
9 | namespace Tests
10 | {
11 | // Stop the "this item is never used" compiler spam
12 | #pragma warning disable CS0649
13 | class TestObject
14 | {
15 | public int a;
16 | public short b;
17 | // No String attribute specified - use null-terminated by default
18 | public string c;
19 | public byte d;
20 | }
21 |
22 | class TestMappedObject
23 | {
24 | [SkipWhenReading]
25 | public string c = "XYZ";
26 |
27 | public int e; // Field does not exist in target
28 |
29 | public short a; // Map from a shorter primitive type
30 | public ulong d; // Map from a longer primitive type
31 | public short b;
32 | }
33 |
34 | class TestObjectWithArrays
35 | {
36 | public int numberOfItems;
37 | [ArrayLength(FieldName = "numberOfItems")]
38 | public ushort[] itemArray;
39 | [ArrayLength(FixedSize = 5)]
40 | public byte[] fiveItems;
41 | }
42 |
43 | class TestObjectWithStrings
44 | {
45 | [String(FixedSize = 8)]
46 | public string eightCharString;
47 | [String(FixedSize = 4)]
48 | public string fourCharString;
49 | [String(IsNullTerminated = true)]
50 | public string nullTerminatedString;
51 | }
52 |
53 | class TestObjectWithVersioning
54 | {
55 | public int allVersionsItem;
56 | [Version(Min = 2)]
57 | public string version2AndHigherItem;
58 | [Version(Max = 1)]
59 | public string version1Item;
60 | [Version(Min = 1, Max = 2)]
61 | [String(IsNullTerminated = true)] // adding a 2nd attribute to prove it works
62 | public string version1And2Item;
63 | }
64 |
65 | class TestObjectWithMultiVersioning
66 | {
67 | public byte a;
68 | [Version(Min = 1, Max = 2)]
69 | [Version(Min = 4)]
70 | public byte b;
71 | public byte c;
72 | }
73 |
74 | class TestObjectWithPrimitiveMapping
75 | {
76 | public int int1;
77 | public int int2;
78 | public bool bool1;
79 | public long long1;
80 | }
81 | #pragma warning restore CS0649
82 |
83 | [TestFixture]
84 | class Tests
85 | {
86 | [Test]
87 | public void TestPrimitives([Values(Endianness.Little, Endianness.Big)] Endianness endianness) {
88 | var testData = new byte[] {0x04, 0x03, 0x02, 0x01, 0xFF, 0xFF, 0xFF, 0xFF};
89 | bool bigEndian = endianness == Endianness.Big;
90 |
91 | using (var stream = new MemoryStream(testData))
92 | using (var reader = new BinaryObjectReader(stream, endianness)) {
93 | // Raw bytes
94 | reader.Position = 0;
95 |
96 | var bytes = reader.ReadBytes(8);
97 | Assert.AreEqual(8, bytes.Length);
98 | for (var i = 0; i < bytes.Length; i++)
99 | if (bigEndian)
100 | Assert.AreEqual(testData[i], bytes[bytes.Length - i - 1]);
101 | else
102 | Assert.AreEqual(testData[i], bytes[i]);
103 |
104 | // Primitives with endianness
105 | reader.Position = 0;
106 |
107 | try {
108 | Assert.AreEqual(bigEndian ? 0x04030201FFFFFFFF : 0xFFFFFFFF01020304, reader.ReadInt64());
109 | }
110 | catch (OverflowException) { }
111 | reader.Position -= 8;
112 | try {
113 | Assert.AreEqual(bigEndian ? 0x04030201FFFFFFFF : 0xFFFFFFFF01020304, reader.ReadUInt64());
114 | }
115 | catch (OverflowException) { }
116 | reader.Position -= 8;
117 | Assert.AreEqual(bigEndian ? 0x04030201 : 0x01020304, reader.ReadInt32());
118 | reader.Position -= 4;
119 | Assert.AreEqual(bigEndian ? 0x04030201 : 0x01020304, reader.ReadUInt32());
120 | reader.Position -= 4;
121 | Assert.AreEqual(bigEndian ? 0x0403 : 0x0304, reader.ReadInt16());
122 | reader.Position -= 2;
123 | Assert.AreEqual(bigEndian ? 0x0403 : 0x0304, reader.ReadUInt16());
124 |
125 | // Signedness
126 | reader.Position = 4;
127 |
128 | Assert.AreEqual(-1, reader.ReadInt32());
129 | reader.Position -= 4;
130 | Assert.AreEqual(0xFFFFFFFF, reader.ReadUInt32());
131 |
132 | reader.Position = 6;
133 |
134 | Assert.AreEqual(-1, reader.ReadInt16());
135 | reader.Position -= 2;
136 | Assert.AreEqual(0xFFFF, reader.ReadUInt16());
137 | }
138 | }
139 |
140 | [Test]
141 | public void TestNullTerminatedString() {
142 | var testData = new byte[] {0x41, 0x42, 0x43, 0x44, 0x00}; // ABCD\0
143 |
144 | using (var stream = new MemoryStream(testData))
145 | using (var reader = new BinaryObjectReader(stream)) {
146 | var str = reader.ReadNullTerminatedString();
147 | Assert.AreEqual("ABCD", str);
148 | }
149 | }
150 |
151 | [Test]
152 | public void TestObject([Values(Endianness.Little, Endianness.Big)] Endianness endianness) {
153 | byte[] testData;
154 |
155 | if (endianness == Endianness.Little)
156 | testData = new byte[] {0x04, 0x03, 0x02, 0x01, 0x34, 0x12, 0x41, 0x42, 0x43, 0x44, 0x00, 0x99};
157 | else {
158 | testData = new byte[] {0x01, 0x02, 0x03, 0x04, 0x12, 0x34, 0x41, 0x42, 0x43, 0x44, 0x00, 0x99};
159 | }
160 |
161 | using (var stream = new MemoryStream(testData))
162 | using (var reader = new BinaryObjectReader(stream, endianness)) {
163 | var obj = reader.ReadObject();
164 |
165 | Assert.AreEqual(0x01020304, obj.a);
166 | Assert.AreEqual(0x1234, obj.b);
167 | Assert.That("ABCD" == obj.c);
168 | Assert.AreEqual(0x99, obj.d);
169 | }
170 | }
171 |
172 | [Test]
173 | public void TestArray() {
174 | var testData = new byte[] {0x34, 0x12, 0x78, 0x56, 0xFF, 0xFF};
175 |
176 | using (var stream = new MemoryStream(testData))
177 | using (var reader = new BinaryObjectReader(stream)) {
178 | var arr = reader.ReadArray(3);
179 |
180 | Assert.AreEqual(3, arr.Length);
181 | Assert.AreEqual(0x1234, arr[0]);
182 | Assert.AreEqual(0x5678, arr[1]);
183 | Assert.AreEqual(-1, arr[2]);
184 | }
185 | }
186 |
187 | [Test]
188 | public void TestArrayOfObject([Values(Endianness.Little, Endianness.Big)] Endianness endianness) {
189 | byte[] testData;
190 |
191 | if (endianness == Endianness.Little)
192 | testData = new byte[] {
193 | 0x04, 0x03, 0x02, 0x01, 0x34, 0x12, 0x41, 0x42, 0x43, 0x44, 0x00, 0x99,
194 | 0x05, 0x03, 0x02, 0x01, 0x35, 0x12, 0x41, 0x42, 0x43, 0x45, 0x00, 0x9A
195 | };
196 | else {
197 | testData = new byte[] {
198 | 0x01, 0x02, 0x03, 0x04, 0x12, 0x34, 0x41, 0x42, 0x43, 0x44, 0x00, 0x99,
199 | 0x01, 0x02, 0x03, 0x05, 0x12, 0x35, 0x41, 0x42, 0x43, 0x45, 0x00, 0x9A,
200 | };
201 | }
202 |
203 | using (var stream = new MemoryStream(testData))
204 | using (var reader = new BinaryObjectReader(stream, endianness)) {
205 | var objArr = reader.ReadArray(2);
206 |
207 | Assert.AreEqual(2, objArr.Length);
208 |
209 | Assert.AreEqual(0x01020304, objArr[0].a);
210 | Assert.AreEqual(0x1234, objArr[0].b);
211 | Assert.That("ABCD" == objArr[0].c);
212 | Assert.AreEqual(0x99, objArr[0].d);
213 |
214 | Assert.AreEqual(0x01020305, objArr[1].a);
215 | Assert.AreEqual(0x1235, objArr[1].b);
216 | Assert.That("ABCE" == objArr[1].c);
217 | Assert.AreEqual(0x9A, objArr[1].d);
218 | }
219 | }
220 |
221 | [Test]
222 | public void TestObjectWithArrays() {
223 | var testData = new byte[]
224 | {0x03, 0x00, 0x00, 0x00, 0x34, 0x12, 0x78, 0x56, 0xBC, 0x9A, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E};
225 |
226 | using (var stream = new MemoryStream(testData))
227 | using (var reader = new BinaryObjectReader(stream)) {
228 | var obj = reader.ReadObject();
229 |
230 | Assert.AreEqual(3, obj.numberOfItems);
231 | Assert.AreEqual(3, obj.itemArray.Length);
232 | Assert.AreEqual(0x1234, obj.itemArray[0]);
233 | Assert.AreEqual(0x5678, obj.itemArray[1]);
234 | Assert.AreEqual(0x9ABC, obj.itemArray[2]);
235 | for (int i = 0; i < 5; i++)
236 | Assert.AreEqual(i + 0x0A, obj.fiveItems[i]);
237 | }
238 | }
239 |
240 | [Test]
241 | public void TestObjectWithStrings() {
242 | var testData = new byte[]
243 | {0x41, 0x42, 0x43, 0x44, 0x45, 0x00, 0x00, 0x00, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x00};
244 |
245 | using (var stream = new MemoryStream(testData))
246 | using (var reader = new BinaryObjectReader(stream)) {
247 | var obj = reader.ReadObject();
248 |
249 | Assert.AreEqual("ABCDE", obj.eightCharString);
250 | Assert.AreEqual("FGHI", obj.fourCharString);
251 | Assert.AreEqual("JK", obj.nullTerminatedString);
252 | }
253 | }
254 |
255 | [Test]
256 | public void TestObjectWithVersioning() {
257 | var testData = new byte[]
258 | {0x34, 0x12, 0x00, 0x00, 0x41, 0x00, 0x42, 0x00, 0x43, 0x00};
259 |
260 | for (int v = 1; v <= 3; v++)
261 | using (var stream = new MemoryStream(testData))
262 | using (var reader = new BinaryObjectReader(stream)) {
263 | reader.Version = v;
264 | var obj = reader.ReadObject();
265 |
266 | Assert.AreEqual(0x1234, obj.allVersionsItem);
267 | Assert.AreEqual((v == 1 ? "A" : null), obj.version1Item);
268 | Assert.AreEqual((v >= 2 ? "A" : null), obj.version2AndHigherItem);
269 | Assert.AreEqual((v < 3 ? "B" : null), obj.version1And2Item);
270 | }
271 | }
272 |
273 | [Test]
274 | public void TestObjectWithMultiVersioning() {
275 | var testData = new byte[]
276 | {0x01, 0x02, 0x03};
277 |
278 | for (int v = 1; v <= 5; v++)
279 | using (var stream = new MemoryStream(testData))
280 | using (var reader = new BinaryObjectReader(stream)) {
281 | reader.Version = v;
282 | var obj = reader.ReadObject();
283 |
284 | Assert.AreEqual(0x01, obj.a);
285 | Assert.AreEqual(v != 3 ? 0x02 : 0x00, obj.b);
286 | Assert.AreEqual(v != 3 ? 0x03 : 0x02, obj.c);
287 | }
288 | }
289 |
290 | [Test]
291 | public void TestMappedPrimitives() {
292 | var testData = new byte[]
293 | {0x01, 0x02, 0x01, 0x87, 0x65, 0x43, 0x21};
294 |
295 | using (var stream = new MemoryStream(testData))
296 | using (var reader = new BinaryObjectReader(stream)) {
297 | reader.PrimitiveMappings.Add(typeof(int).Name, typeof(byte));
298 | reader.PrimitiveMappings.Add(typeof(bool).Name, typeof(byte));
299 | reader.PrimitiveMappings.Add(typeof(long).Name, typeof(int));
300 |
301 | var obj = reader.ReadObject();
302 |
303 | Assert.AreEqual(obj.int1, 1);
304 | Assert.AreEqual(obj.int2, 2);
305 | Assert.AreEqual(obj.bool1, true);
306 | Assert.AreEqual(obj.long1, 0x21436587);
307 | }
308 | }
309 |
310 | [Test]
311 | public void TestMappedObject() {
312 |
313 | var testData = new byte[]
314 | { 0xCC, 0xCC, 0xCC, 0xCC, 0x02, 0x01, 0x99, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12 };
315 |
316 | using var stream = new MemoryStream(testData);
317 | using var reader = new BinaryObjectReader(stream);
318 |
319 | reader.ObjectMappings.Add(typeof(TestObject), typeof(TestMappedObject));
320 |
321 | var obj = reader.ReadObject();
322 |
323 | Assert.AreEqual(0x0102, obj.a);
324 | Assert.AreEqual(0x1234, obj.b);
325 | Assert.That("XYZ" == obj.c);
326 | Assert.AreEqual(0x99, obj.d);
327 | }
328 |
329 | [Test]
330 | public void TestWriterPrimitives() {
331 | var expected = new byte[]
332 | { 0x01, 0x02, 0x03,
333 | 0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0x01,
334 | 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff,
335 | 0x78, 0x56, 0x34, 0x12, 0x98, 0xba, 0xdc, 0xfe,
336 | 0x34, 0x12, 0xdc, 0xfe, 0xcc, 0x01 };
337 |
338 | using var stream = new MemoryStream();
339 | using (var writer = new BinaryObjectWriter(stream)) {
340 | writer.WriteEndianBytes(new byte[] { 0x01, 0x02, 0x03 });
341 | writer.Write((long) 0x0123456789abcdef);
342 | writer.Write((ulong) 0xffeeddccbbaa9988);
343 | writer.Write((int) 0x12345678);
344 | writer.Write((uint) 0xfedcba98);
345 | writer.Write((short) 0x1234);
346 | writer.Write((ushort) 0xfedc);
347 | writer.Write((byte) 0xcc);
348 | writer.Write(true);
349 | }
350 | var bytes = stream.ToArray();
351 | Assert.That(Enumerable.SequenceEqual(bytes, expected));
352 | }
353 |
354 | [Test]
355 | public void TestWriterStrings() {
356 | var expected = new byte[]
357 | { 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00,
358 | 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x00,
359 | 0x74, 0x65, 0x73, 0x74,
360 | 0x74, 0x65, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
361 |
362 | using var stream = new MemoryStream();
363 | using (var writer = new BinaryObjectWriter(stream)) {
364 | writer.WriteNullTerminatedString("hello");
365 | writer.WriteNullTerminatedString("world");
366 | writer.WriteFixedLengthString("test");
367 | writer.WriteFixedLengthString("test", 10);
368 | }
369 | var bytes = stream.ToArray();
370 | Assert.That(Enumerable.SequenceEqual(bytes, expected));
371 | }
372 |
373 | [Test]
374 | public void TestWriterObject([Values(Endianness.Little, Endianness.Big)] Endianness endianness) {
375 | var expected =
376 | endianness == Endianness.Little? new byte[]
377 | { 0x78, 0x56, 0x34, 0x12, 0x34, 0x12, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0xff }
378 |
379 | : new byte[]
380 | { 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0xff };
381 |
382 | var obj = new TestObject {
383 | a = 0x12345678,
384 | b = 0x1234,
385 | c = "hello",
386 | d = 0xff
387 | };
388 |
389 | using var stream = new MemoryStream();
390 | using (var writer = new BinaryObjectWriter(stream, endianness)) {
391 | writer.WriteObject(obj);
392 | }
393 | var bytes = stream.ToArray();
394 | Assert.That(Enumerable.SequenceEqual(bytes, expected));
395 | }
396 |
397 | [Test]
398 | public void TestWriterArray() {
399 | var expected = new byte[] { 0x34, 0x12, 0x78, 0x56, 0xff, 0xff };
400 |
401 | var testData = new short[] { 0x1234, 0x5678, -1 };
402 |
403 | using var stream = new MemoryStream();
404 | using (var writer = new BinaryObjectWriter(stream)) {
405 | writer.WriteArray(testData);
406 | }
407 | var bytes = stream.ToArray();
408 | Assert.That(Enumerable.SequenceEqual(bytes, expected));
409 | }
410 |
411 | [Test]
412 | public void TestWriterArrayOfObject([Values(Endianness.Little, Endianness.Big)] Endianness endianness) {
413 | byte[] expected;
414 |
415 | if (endianness == Endianness.Little)
416 | expected = new byte[] {
417 | 0x04, 0x03, 0x02, 0x01, 0x34, 0x12, 0x41, 0x42, 0x43, 0x44, 0x00, 0x99,
418 | 0x05, 0x03, 0x02, 0x01, 0x35, 0x12, 0x41, 0x42, 0x43, 0x45, 0x00, 0x9A
419 | };
420 | else {
421 | expected = new byte[] {
422 | 0x01, 0x02, 0x03, 0x04, 0x12, 0x34, 0x41, 0x42, 0x43, 0x44, 0x00, 0x99,
423 | 0x01, 0x02, 0x03, 0x05, 0x12, 0x35, 0x41, 0x42, 0x43, 0x45, 0x00, 0x9A,
424 | };
425 | }
426 |
427 | var testData = new TestObject[] {
428 | new TestObject { a = 0x01020304, b = 0x1234, c = "ABCD", d = 0x99 },
429 | new TestObject { a = 0x01020305, b = 0x1235, c = "ABCE", d = 0x9A }
430 | };
431 |
432 | using var stream = new MemoryStream();
433 | using (var writer = new BinaryObjectWriter(stream, endianness)) {
434 | writer.WriteArray(testData);
435 | }
436 | var bytes = stream.ToArray();
437 | Assert.That(Enumerable.SequenceEqual(bytes, expected));
438 | }
439 |
440 | [Test]
441 | public void TestWriterObjectWithArrays() {
442 | var expected = new byte[]
443 | {0x03, 0x00, 0x00, 0x00, 0x34, 0x12, 0x78, 0x56, 0xBC, 0x9A, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E};
444 |
445 | var testData = new TestObjectWithArrays {
446 | numberOfItems = 3,
447 | itemArray = new ushort[] { 0x1234, 0x5678, 0x9ABC },
448 | fiveItems = new byte[] { 0x0A, 0x0B, 0x0C, 0x0D, 0x0E }
449 | };
450 |
451 | using var stream = new MemoryStream();
452 | using (var writer = new BinaryObjectWriter(stream)) {
453 | writer.WriteObject(testData);
454 | }
455 | var bytes = stream.ToArray();
456 | Assert.That(Enumerable.SequenceEqual(bytes, expected));
457 | }
458 |
459 | [Test]
460 | public void TestWriterObjectWithStrings() {
461 | var expected = new byte[]
462 | {0x41, 0x42, 0x43, 0x44, 0x45, 0x00, 0x00, 0x00, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x00};
463 |
464 | var testData = new TestObjectWithStrings {
465 | eightCharString = "ABCDE",
466 | fourCharString = "FGHI",
467 | nullTerminatedString = "JK"
468 | };
469 |
470 | using var stream = new MemoryStream();
471 | using (var writer = new BinaryObjectWriter(stream)) {
472 | writer.WriteObject(testData);
473 | }
474 | var bytes = stream.ToArray();
475 | Assert.That(Enumerable.SequenceEqual(bytes, expected));
476 | }
477 |
478 | [Test]
479 | public void TestWriterObjectWithVersioning() {
480 | var expected = new byte[][] {
481 | new byte[] {0x34, 0x12, 0x00, 0x00, 0x41, 0x00, 0x43, 0x00 },
482 | new byte[] {0x34, 0x12, 0x00, 0x00, 0x42, 0x00, 0x43, 0x00 },
483 | new byte[] {0x34, 0x12, 0x00, 0x00, 0x42, 0x00 }
484 | };
485 |
486 | var testData = new TestObjectWithVersioning {
487 | allVersionsItem = 0x1234,
488 | version1Item = "A",
489 | version2AndHigherItem = "B",
490 | version1And2Item = "C"
491 | };
492 |
493 | for (int v = 1; v <= 3; v++)
494 | using (var stream = new MemoryStream()) {
495 | using (var writer = new BinaryObjectWriter(stream)) {
496 | writer.Version = v;
497 | writer.WriteObject(testData);
498 | }
499 | var bytes = stream.ToArray();
500 | Assert.That(Enumerable.SequenceEqual(bytes, expected[v - 1]));
501 | }
502 | }
503 |
504 | [Test]
505 | public void TestWriterObjectWithMultiVersioning() {
506 | var expected = new byte[][] {
507 | new byte[] {0x01, 0x02, 0x03},
508 | new byte[] {0x01, 0x02, 0x03},
509 | new byte[] {0x01, 0x03},
510 | new byte[] {0x01, 0x02, 0x03},
511 | new byte[] {0x01, 0x02, 0x03},
512 | };
513 |
514 | var testData = new TestObjectWithMultiVersioning {
515 | a = 0x01,
516 | b = 0x02,
517 | c = 0x03
518 | };
519 |
520 | for (int v = 1; v <= 5; v++)
521 | using (var stream = new MemoryStream()) {
522 | using (var writer = new BinaryObjectWriter(stream)) {
523 | writer.Version = v;
524 | writer.WriteObject(testData);
525 | }
526 | var bytes = stream.ToArray();
527 | Assert.That(Enumerable.SequenceEqual(bytes, expected[v - 1]));
528 | }
529 | }
530 |
531 | [Test]
532 | public void TestWriterMappedPrimitives() {
533 | var expected = new byte[]
534 | {0x01, 0x02, 0x01, 0x87, 0x65, 0x43, 0x21};
535 |
536 | var testData = new TestObjectWithPrimitiveMapping {
537 | int1 = 1,
538 | int2 = 2,
539 | bool1 = true,
540 | long1 = 0x21436587
541 | };
542 |
543 | using var stream = new MemoryStream();
544 | using (var writer = new BinaryObjectWriter(stream)) {
545 | writer.PrimitiveMappings.Add(typeof(int), typeof(byte));
546 | writer.PrimitiveMappings.Add(typeof(bool), typeof(byte));
547 | writer.PrimitiveMappings.Add(typeof(long), typeof(int));
548 |
549 | writer.WriteObject(testData);
550 | }
551 | var bytes = stream.ToArray();
552 | Assert.That(Enumerable.SequenceEqual(bytes, expected));
553 | }
554 | }
555 | }
556 |
--------------------------------------------------------------------------------
/Tests/Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.1
5 | any
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------