mirror of https://github.com/icsharpcode/ILSpy.git
				
				
			
			You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							572 lines
						
					
					
						
							16 KiB
						
					
					
				
			
		
		
	
	
							572 lines
						
					
					
						
							16 KiB
						
					
					
				#nullable enable | 
						|
// Copyright (c) 2018 Daniel Grunwald | 
						|
// Based on the .NET Core ResourceReader; make available under the MIT license | 
						|
// by the .NET Foundation. | 
						|
//  | 
						|
// Permission is hereby granted, free of charge, to any person obtaining a copy of this | 
						|
// software and associated documentation files (the "Software"), to deal in the Software | 
						|
// without restriction, including without limitation the rights to use, copy, modify, merge, | 
						|
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons | 
						|
// to whom the Software is furnished to do so, subject to the following conditions: | 
						|
//  | 
						|
// The above copyright notice and this permission notice shall be included in all copies or | 
						|
// substantial portions of the Software. | 
						|
//  | 
						|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, | 
						|
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR | 
						|
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE | 
						|
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR | 
						|
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | 
						|
// DEALINGS IN THE SOFTWARE. | 
						|
 | 
						|
using System; | 
						|
using System.Collections; | 
						|
using System.Collections.Generic; | 
						|
using System.Diagnostics; | 
						|
using System.IO; | 
						|
using System.Text; | 
						|
 | 
						|
namespace ICSharpCode.Decompiler.Util | 
						|
{ | 
						|
	/// <summary> | 
						|
	/// .resources file. | 
						|
	/// </summary> | 
						|
	public class ResourcesFile : IEnumerable<KeyValuePair<string, object?>>, IDisposable | 
						|
	{ | 
						|
		sealed class MyBinaryReader : BinaryReader | 
						|
		{ | 
						|
			public MyBinaryReader(Stream input, bool leaveOpen) : base(input, Encoding.UTF8, leaveOpen) | 
						|
			{ | 
						|
			} | 
						|
 | 
						|
			// upgrade from protected to public visibility | 
						|
			public new int Read7BitEncodedInt() | 
						|
			{ | 
						|
				return base.Read7BitEncodedInt(); | 
						|
			} | 
						|
 | 
						|
			public void Seek(long pos, SeekOrigin origin) | 
						|
			{ | 
						|
				BaseStream.Seek(pos, origin); | 
						|
			} | 
						|
		} | 
						|
 | 
						|
		enum ResourceTypeCode | 
						|
		{ | 
						|
			Null = 0, | 
						|
			String = 1, | 
						|
			Boolean = 2, | 
						|
			Char = 3, | 
						|
			Byte = 4, | 
						|
			SByte = 5, | 
						|
			Int16 = 6, | 
						|
			UInt16 = 7, | 
						|
			Int32 = 8, | 
						|
			UInt32 = 9, | 
						|
			Int64 = 10, | 
						|
			UInt64 = 11, | 
						|
			Single = 12, | 
						|
			Double = 13, | 
						|
			Decimal = 14, | 
						|
			DateTime = 0xF, | 
						|
			TimeSpan = 0x10, | 
						|
			LastPrimitive = 0x10, | 
						|
			ByteArray = 0x20, | 
						|
			Stream = 33, | 
						|
			StartOfUserTypes = 0x40 | 
						|
		} | 
						|
 | 
						|
		/// <summary>Holds the number used to identify resource files.</summary> | 
						|
		public const int MagicNumber = unchecked((int)0xBEEFCACE); | 
						|
		const int ResourceSetVersion = 2; | 
						|
 | 
						|
		readonly MyBinaryReader reader; | 
						|
		readonly int version; | 
						|
		readonly int numResources; | 
						|
		readonly string[] typeTable; | 
						|
		readonly int[] namePositions; | 
						|
		readonly long fileStartPosition; | 
						|
		readonly long nameSectionPosition; | 
						|
		readonly long dataSectionPosition; | 
						|
		long[]? startPositions; | 
						|
 | 
						|
		/// <summary> | 
						|
		/// Creates a new ResourcesFile. | 
						|
		/// </summary> | 
						|
		/// <param name="stream">Input stream.</param> | 
						|
		/// <param name="leaveOpen">Whether the stream should be help open when the ResourcesFile is disposed.</param> | 
						|
		/// <remarks> | 
						|
		/// The stream is must be held open while the ResourcesFile is in use. | 
						|
		/// The stream must be seekable; any operation using the ResourcesFile will end up seeking the stream. | 
						|
		/// </remarks> | 
						|
		public ResourcesFile(Stream stream, bool leaveOpen = true) | 
						|
		{ | 
						|
			fileStartPosition = stream.Position; | 
						|
			reader = new MyBinaryReader(stream, leaveOpen); | 
						|
 | 
						|
			const string ResourcesHeaderCorrupted = "Resources header corrupted."; | 
						|
 | 
						|
			// Read ResourceManager header | 
						|
			// Check for magic number | 
						|
			int magicNum = reader.ReadInt32(); | 
						|
			if (magicNum != MagicNumber) | 
						|
				throw new BadImageFormatException("Not a .resources file - invalid magic number"); | 
						|
			// Assuming this is ResourceManager header V1 or greater, hopefully | 
						|
			// after the version number there is a number of bytes to skip | 
						|
			// to bypass the rest of the ResMgr header. For V2 or greater, we | 
						|
			// use this to skip to the end of the header | 
						|
			int resMgrHeaderVersion = reader.ReadInt32(); | 
						|
			int numBytesToSkip = reader.ReadInt32(); | 
						|
			if (numBytesToSkip < 0 || resMgrHeaderVersion < 0) | 
						|
			{ | 
						|
				throw new BadImageFormatException(ResourcesHeaderCorrupted); | 
						|
			} | 
						|
			if (resMgrHeaderVersion > 1) | 
						|
			{ | 
						|
				reader.BaseStream.Seek(numBytesToSkip, SeekOrigin.Current); | 
						|
			} | 
						|
			else | 
						|
			{ | 
						|
				// We don't care about numBytesToSkip; read the rest of the header | 
						|
 | 
						|
				// readerType: | 
						|
				reader.ReadString(); | 
						|
				// resourceSetType: | 
						|
				reader.ReadString(); | 
						|
			} | 
						|
 | 
						|
			// Read RuntimeResourceSet header | 
						|
			// Do file version check | 
						|
			version = reader.ReadInt32(); | 
						|
			if (version != ResourceSetVersion && version != 1) | 
						|
				throw new BadImageFormatException($"Unsupported resource set version: {version}"); | 
						|
 | 
						|
			numResources = reader.ReadInt32(); | 
						|
			if (numResources < 0) | 
						|
			{ | 
						|
				throw new BadImageFormatException(ResourcesHeaderCorrupted); | 
						|
			} | 
						|
 | 
						|
			// Read type positions into type positions array. | 
						|
			// But delay initialize the type table. | 
						|
			int numTypes = reader.ReadInt32(); | 
						|
			if (numTypes < 0) | 
						|
			{ | 
						|
				throw new BadImageFormatException(ResourcesHeaderCorrupted); | 
						|
			} | 
						|
			typeTable = new string[numTypes]; | 
						|
			for (int i = 0; i < numTypes; i++) | 
						|
			{ | 
						|
				typeTable[i] = reader.ReadString(); | 
						|
			} | 
						|
 | 
						|
			// Prepare to read in the array of name hashes | 
						|
			//  Note that the name hashes array is aligned to 8 bytes so  | 
						|
			//  we can use pointers into it on 64 bit machines. (4 bytes  | 
						|
			//  may be sufficient, but let's plan for the future) | 
						|
			//  Skip over alignment stuff.  All public .resources files | 
						|
			//  should be aligned   No need to verify the byte values. | 
						|
			long pos = reader.BaseStream.Position - fileStartPosition; | 
						|
			int alignBytes = unchecked((int)pos) & 7; | 
						|
			if (alignBytes != 0) | 
						|
			{ | 
						|
				for (int i = 0; i < 8 - alignBytes; i++) | 
						|
				{ | 
						|
					reader.ReadByte(); | 
						|
				} | 
						|
			} | 
						|
 | 
						|
			// Skip over the array of name hashes | 
						|
			try | 
						|
			{ | 
						|
				reader.Seek(checked(4 * numResources), SeekOrigin.Current); | 
						|
			} | 
						|
			catch (OverflowException) | 
						|
			{ | 
						|
				throw new BadImageFormatException(ResourcesHeaderCorrupted); | 
						|
			} | 
						|
 | 
						|
			// Read in the array of relative positions for all the names. | 
						|
			namePositions = new int[numResources]; | 
						|
			for (int i = 0; i < numResources; i++) | 
						|
			{ | 
						|
				int namePosition = reader.ReadInt32(); | 
						|
				if (namePosition < 0) | 
						|
				{ | 
						|
					throw new BadImageFormatException(ResourcesHeaderCorrupted); | 
						|
				} | 
						|
				namePositions[i] = namePosition; | 
						|
			} | 
						|
 | 
						|
			// Read location of data section. | 
						|
			int dataSectionOffset = reader.ReadInt32(); | 
						|
			if (dataSectionOffset < 0) | 
						|
			{ | 
						|
				throw new BadImageFormatException(ResourcesHeaderCorrupted); | 
						|
			} | 
						|
 | 
						|
			// Store current location as start of name section | 
						|
			nameSectionPosition = reader.BaseStream.Position; | 
						|
			dataSectionPosition = fileStartPosition + dataSectionOffset; | 
						|
 | 
						|
			// _nameSectionOffset should be <= _dataSectionOffset; if not, it's corrupt | 
						|
			if (dataSectionPosition < nameSectionPosition) | 
						|
			{ | 
						|
				throw new BadImageFormatException(ResourcesHeaderCorrupted); | 
						|
			} | 
						|
		} | 
						|
 | 
						|
		public void Dispose() | 
						|
		{ | 
						|
			reader.Dispose(); | 
						|
		} | 
						|
 | 
						|
		public int ResourceCount => numResources; | 
						|
 | 
						|
		public string GetResourceName(int index) | 
						|
		{ | 
						|
			return GetResourceName(index, out _); | 
						|
		} | 
						|
 | 
						|
		int GetResourceDataOffset(int index) | 
						|
		{ | 
						|
			GetResourceName(index, out int dataOffset); | 
						|
			return dataOffset; | 
						|
		} | 
						|
 | 
						|
		string GetResourceName(int index, out int dataOffset) | 
						|
		{ | 
						|
			long pos = nameSectionPosition + namePositions[index]; | 
						|
			byte[] bytes; | 
						|
			lock (reader) | 
						|
			{ | 
						|
				reader.Seek(pos, SeekOrigin.Begin); | 
						|
				// Can't use reader.ReadString, since it's using UTF-8! | 
						|
				int byteLen = reader.Read7BitEncodedInt(); | 
						|
				if (byteLen < 0) | 
						|
				{ | 
						|
					throw new BadImageFormatException("Resource name has negative length"); | 
						|
				} | 
						|
				bytes = new byte[byteLen]; | 
						|
				// We must read byteLen bytes, or we have a corrupted file. | 
						|
				// Use a blocking read in case the stream doesn't give us back | 
						|
				// everything immediately. | 
						|
				int count = byteLen; | 
						|
				while (count > 0) | 
						|
				{ | 
						|
					int n = reader.Read(bytes, byteLen - count, count); | 
						|
					if (n == 0) | 
						|
						throw new BadImageFormatException("End of stream within a resource name"); | 
						|
					count -= n; | 
						|
				} | 
						|
				dataOffset = reader.ReadInt32(); | 
						|
				if (dataOffset < 0) | 
						|
				{ | 
						|
					throw new BadImageFormatException("Negative data offset"); | 
						|
				} | 
						|
			} | 
						|
			return Encoding.Unicode.GetString(bytes); | 
						|
		} | 
						|
 | 
						|
		internal bool AllEntriesAreStreams() | 
						|
		{ | 
						|
			if (version != 2) | 
						|
				return false; | 
						|
			lock (reader) | 
						|
			{ | 
						|
				for (int i = 0; i < numResources; i++) | 
						|
				{ | 
						|
					int dataOffset = GetResourceDataOffset(i); | 
						|
					reader.Seek(dataSectionPosition + dataOffset, SeekOrigin.Begin); | 
						|
					var typeCode = (ResourceTypeCode)reader.Read7BitEncodedInt(); | 
						|
					if (typeCode != ResourceTypeCode.Stream) | 
						|
						return false; | 
						|
				} | 
						|
			} | 
						|
			return true; | 
						|
		} | 
						|
 | 
						|
		object? LoadObject(int dataOffset) | 
						|
		{ | 
						|
			try | 
						|
			{ | 
						|
				lock (reader) | 
						|
				{ | 
						|
					if (version == 1) | 
						|
					{ | 
						|
						return LoadObjectV1(dataOffset); | 
						|
					} | 
						|
					else | 
						|
					{ | 
						|
						return LoadObjectV2(dataOffset); | 
						|
					} | 
						|
				} | 
						|
			} | 
						|
			catch (EndOfStreamException e) | 
						|
			{ | 
						|
				throw new BadImageFormatException("Invalid resource file", e); | 
						|
			} | 
						|
		} | 
						|
 | 
						|
		string FindType(int typeIndex) | 
						|
		{ | 
						|
			if (typeIndex < 0 || typeIndex >= typeTable.Length) | 
						|
				throw new BadImageFormatException("Type index out of bounds"); | 
						|
			return typeTable[typeIndex]; | 
						|
		} | 
						|
 | 
						|
		// This takes a virtual offset into the data section and reads an Object | 
						|
		// from that location. | 
						|
		// Anyone who calls LoadObject should make sure they take a lock so  | 
						|
		// no one can cause us to do a seek in here. | 
						|
		private object? LoadObjectV1(int dataOffset) | 
						|
		{ | 
						|
			Debug.Assert(System.Threading.Monitor.IsEntered(reader)); | 
						|
			reader.Seek(dataSectionPosition + dataOffset, SeekOrigin.Begin); | 
						|
			int typeIndex = reader.Read7BitEncodedInt(); | 
						|
			if (typeIndex == -1) | 
						|
				return null; | 
						|
			string typeName = FindType(typeIndex); | 
						|
			int comma = typeName.IndexOf(','); | 
						|
			if (comma > 0) | 
						|
			{ | 
						|
				// strip assembly name | 
						|
				typeName = typeName.Substring(0, comma); | 
						|
			} | 
						|
			switch (typeName) | 
						|
			{ | 
						|
				case "System.String": | 
						|
					return reader.ReadString(); | 
						|
				case "System.Byte": | 
						|
					return reader.ReadByte(); | 
						|
				case "System.SByte": | 
						|
					return reader.ReadSByte(); | 
						|
				case "System.Int16": | 
						|
					return reader.ReadInt16(); | 
						|
				case "System.UInt16": | 
						|
					return reader.ReadUInt16(); | 
						|
				case "System.Int32": | 
						|
					return reader.ReadInt32(); | 
						|
				case "System.UInt32": | 
						|
					return reader.ReadUInt32(); | 
						|
				case "System.Int64": | 
						|
					return reader.ReadInt64(); | 
						|
				case "System.UInt64": | 
						|
					return reader.ReadUInt64(); | 
						|
				case "System.Single": | 
						|
					return reader.ReadSingle(); | 
						|
				case "System.Double": | 
						|
					return reader.ReadDouble(); | 
						|
				case "System.DateTime": | 
						|
					// Ideally we should use DateTime's ToBinary & FromBinary, | 
						|
					// but we can't for compatibility reasons. | 
						|
					return new DateTime(reader.ReadInt64()); | 
						|
				case "System.TimeSpan": | 
						|
					return new TimeSpan(reader.ReadInt64()); | 
						|
				case "System.Decimal": | 
						|
					int[] bits = new int[4]; | 
						|
					for (int i = 0; i < bits.Length; i++) | 
						|
						bits[i] = reader.ReadInt32(); | 
						|
					return new decimal(bits); | 
						|
				default: | 
						|
					return new ResourceSerializedObject(FindType(typeIndex), this, reader.BaseStream.Position); | 
						|
			} | 
						|
		} | 
						|
 | 
						|
		private object? LoadObjectV2(int dataOffset) | 
						|
		{ | 
						|
			Debug.Assert(System.Threading.Monitor.IsEntered(reader)); | 
						|
			reader.Seek(dataSectionPosition + dataOffset, SeekOrigin.Begin); | 
						|
			var typeCode = (ResourceTypeCode)reader.Read7BitEncodedInt(); | 
						|
			switch (typeCode) | 
						|
			{ | 
						|
				case ResourceTypeCode.Null: | 
						|
					return null; | 
						|
 | 
						|
				case ResourceTypeCode.String: | 
						|
					return reader.ReadString(); | 
						|
 | 
						|
				case ResourceTypeCode.Boolean: | 
						|
					return reader.ReadBoolean(); | 
						|
 | 
						|
				case ResourceTypeCode.Char: | 
						|
					return (char)reader.ReadUInt16(); | 
						|
 | 
						|
				case ResourceTypeCode.Byte: | 
						|
					return reader.ReadByte(); | 
						|
 | 
						|
				case ResourceTypeCode.SByte: | 
						|
					return reader.ReadSByte(); | 
						|
 | 
						|
				case ResourceTypeCode.Int16: | 
						|
					return reader.ReadInt16(); | 
						|
 | 
						|
				case ResourceTypeCode.UInt16: | 
						|
					return reader.ReadUInt16(); | 
						|
 | 
						|
				case ResourceTypeCode.Int32: | 
						|
					return reader.ReadInt32(); | 
						|
 | 
						|
				case ResourceTypeCode.UInt32: | 
						|
					return reader.ReadUInt32(); | 
						|
 | 
						|
				case ResourceTypeCode.Int64: | 
						|
					return reader.ReadInt64(); | 
						|
 | 
						|
				case ResourceTypeCode.UInt64: | 
						|
					return reader.ReadUInt64(); | 
						|
 | 
						|
				case ResourceTypeCode.Single: | 
						|
					return reader.ReadSingle(); | 
						|
 | 
						|
				case ResourceTypeCode.Double: | 
						|
					return reader.ReadDouble(); | 
						|
 | 
						|
				case ResourceTypeCode.Decimal: | 
						|
					return reader.ReadDecimal(); | 
						|
 | 
						|
				case ResourceTypeCode.DateTime: | 
						|
					// Use DateTime's ToBinary & FromBinary. | 
						|
					long data = reader.ReadInt64(); | 
						|
					return DateTime.FromBinary(data); | 
						|
 | 
						|
				case ResourceTypeCode.TimeSpan: | 
						|
					long ticks = reader.ReadInt64(); | 
						|
					return new TimeSpan(ticks); | 
						|
 | 
						|
				// Special types | 
						|
				case ResourceTypeCode.ByteArray: | 
						|
				{ | 
						|
					int len = reader.ReadInt32(); | 
						|
					if (len < 0) | 
						|
					{ | 
						|
						throw new BadImageFormatException("Resource with negative length"); | 
						|
					} | 
						|
					return reader.ReadBytes(len); | 
						|
				} | 
						|
 | 
						|
				case ResourceTypeCode.Stream: | 
						|
				{ | 
						|
					int len = reader.ReadInt32(); | 
						|
					if (len < 0) | 
						|
					{ | 
						|
						throw new BadImageFormatException("Resource with negative length"); | 
						|
					} | 
						|
					byte[] bytes = reader.ReadBytes(len); | 
						|
					return new MemoryStream(bytes, writable: false); | 
						|
				} | 
						|
 | 
						|
				default: | 
						|
					if (typeCode < ResourceTypeCode.StartOfUserTypes) | 
						|
					{ | 
						|
						throw new BadImageFormatException("Invalid typeCode"); | 
						|
					} | 
						|
					return new ResourceSerializedObject(FindType(typeCode - ResourceTypeCode.StartOfUserTypes), this, reader.BaseStream.Position); | 
						|
			} | 
						|
		} | 
						|
 | 
						|
		public object? GetResourceValue(int index) | 
						|
		{ | 
						|
			GetResourceName(index, out int dataOffset); | 
						|
			return LoadObject(dataOffset); | 
						|
		} | 
						|
 | 
						|
		public IEnumerator<KeyValuePair<string, object?>> GetEnumerator() | 
						|
		{ | 
						|
			for (int i = 0; i < numResources; i++) | 
						|
			{ | 
						|
				string name = GetResourceName(i, out int dataOffset); | 
						|
				object? val = LoadObject(dataOffset); | 
						|
				yield return new KeyValuePair<string, object?>(name, val); | 
						|
			} | 
						|
		} | 
						|
 | 
						|
		IEnumerator IEnumerable.GetEnumerator() | 
						|
		{ | 
						|
			return GetEnumerator(); | 
						|
		} | 
						|
 | 
						|
		long[] GetStartPositions() | 
						|
		{ | 
						|
			long[]? positions = LazyInit.VolatileRead(ref startPositions); | 
						|
			if (positions != null) | 
						|
				return positions; | 
						|
			lock (reader) | 
						|
			{ | 
						|
				// double-checked locking | 
						|
				positions = LazyInit.VolatileRead(ref startPositions); | 
						|
				if (positions != null) | 
						|
					return positions; | 
						|
				positions = new long[numResources * 2]; | 
						|
				int outPos = 0; | 
						|
				for (int i = 0; i < numResources; i++) | 
						|
				{ | 
						|
					positions[outPos++] = nameSectionPosition + namePositions[i]; | 
						|
					positions[outPos++] = dataSectionPosition + GetResourceDataOffset(i); | 
						|
				} | 
						|
				Array.Sort(positions); | 
						|
				return LazyInit.GetOrSet(ref startPositions, positions); | 
						|
			} | 
						|
		} | 
						|
 | 
						|
		internal byte[] GetBytesForSerializedObject(long pos) | 
						|
		{ | 
						|
			long[] positions = GetStartPositions(); | 
						|
			int i = Array.BinarySearch(positions, pos); | 
						|
			if (i < 0) | 
						|
			{ | 
						|
				// 'pos' the the start position of the serialized object data | 
						|
				// This is the position after the type code, so it should not appear in the 'positions' array. | 
						|
				// Set i to the index of the next position after 'pos'. | 
						|
				i = ~i; | 
						|
				// Note: if 'pos' does exist in the array, that means the stream has length 0, | 
						|
				// so we keep the i that we found. | 
						|
			} | 
						|
			lock (reader) | 
						|
			{ | 
						|
				long endPos; | 
						|
				if (i == positions.Length) | 
						|
				{ | 
						|
					endPos = reader.BaseStream.Length; | 
						|
				} | 
						|
				else | 
						|
				{ | 
						|
					endPos = positions[i]; | 
						|
				} | 
						|
				int len = (int)(endPos - pos); | 
						|
				reader.Seek(pos, SeekOrigin.Begin); | 
						|
				return reader.ReadBytes(len); | 
						|
			} | 
						|
		} | 
						|
	} | 
						|
 | 
						|
	public class ResourceSerializedObject | 
						|
	{ | 
						|
		public string? TypeName { get; } | 
						|
		readonly ResourcesFile file; | 
						|
		readonly long position; | 
						|
 | 
						|
		internal ResourceSerializedObject(string? typeName, ResourcesFile file, long position) | 
						|
		{ | 
						|
			this.TypeName = typeName; | 
						|
			this.file = file; | 
						|
			this.position = position; | 
						|
		} | 
						|
 | 
						|
		/// <summary> | 
						|
		/// Gets a stream that starts with the serialized object data. | 
						|
		/// </summary> | 
						|
		public Stream GetStream() | 
						|
		{ | 
						|
			return new MemoryStream(file.GetBytesForSerializedObject(position), writable: false); | 
						|
		} | 
						|
 | 
						|
		/// <summary> | 
						|
		/// Gets the serialized object data. | 
						|
		/// </summary> | 
						|
		public byte[] GetBytes() | 
						|
		{ | 
						|
			return file.GetBytesForSerializedObject(position); | 
						|
		} | 
						|
	} | 
						|
}
 | 
						|
 |