diff --git a/MetadataExtractor/Formats/Apple/BplistReader.cs b/MetadataExtractor/Formats/Apple/BplistReader.cs index e38221228..f5a2fe595 100644 --- a/MetadataExtractor/Formats/Apple/BplistReader.cs +++ b/MetadataExtractor/Formats/Apple/BplistReader.cs @@ -45,19 +45,20 @@ public static PropertyListResults Parse(byte[] bplist) Trailer trailer = ReadTrailer(); - SequentialByteArrayReader reader = new(bplist, baseIndex: checked((int)(trailer.OffsetTableOffset + trailer.TopObject))); + int offset = checked((int)(trailer.OffsetTableOffset + trailer.TopObject)); + var reader = new BufferReader(bplist.AsSpan(offset), isBigEndian: true); int[] offsets = new int[(int)trailer.NumObjects]; - for (long i = 0; i < trailer.NumObjects; i++) + for (int i = 0; i < (int)trailer.NumObjects; i++) { if (trailer.OffsetIntSize == 1) { - offsets[(int)i] = reader.GetByte(); + offsets[i] = reader.GetByte(); } else if (trailer.OffsetIntSize == 2) { - offsets[(int)i] = reader.GetUInt16(); + offsets[i] = reader.GetUInt16(); } } @@ -65,7 +66,7 @@ public static PropertyListResults Parse(byte[] bplist) for (int i = 0; i < offsets.Length; i++) { - reader = new SequentialByteArrayReader(bplist, offsets[i]); + reader = new BufferReader(bplist.AsSpan(offsets[i]), isBigEndian: true); byte b = reader.GetByte(); @@ -75,13 +76,13 @@ public static PropertyListResults Parse(byte[] bplist) object obj = objectFormat switch { // dict - 0x0D => HandleDict(marker), + 0x0D => HandleDict(ref reader, marker), // string (ASCII) 0x05 => reader.GetString(bytesRequested: marker & 0x0F, Encoding.ASCII), // data - 0x04 => HandleData(marker), + 0x04 => HandleData(ref reader, marker), // int - 0x01 => HandleInt(marker), + 0x01 => HandleInt(ref reader, marker), // unknown _ => throw new NotSupportedException($"Unsupported object format {objectFormat:X2}.") }; @@ -93,10 +94,10 @@ public static PropertyListResults Parse(byte[] bplist) Trailer ReadTrailer() { - SequentialByteArrayReader reader = new(bplist, bplist.Length - Trailer.SizeBytes); + var reader = new BufferReader(bplist.AsSpan(bplist.Length - Trailer.SizeBytes), isBigEndian: true); // Skip 5-byte unused values, 1-byte sort version. - reader.Skip(6); + reader.Skip(5 + 1); return new Trailer { @@ -108,7 +109,7 @@ Trailer ReadTrailer() }; } - object HandleInt(byte marker) + static object HandleInt(ref BufferReader reader, byte marker) { return marker switch { @@ -120,7 +121,7 @@ object HandleInt(byte marker) }; } - Dictionary HandleDict(byte count) + static Dictionary HandleDict(ref BufferReader reader, byte count) { var keyRefs = ArrayPool.Shared.Rent(count); @@ -141,7 +142,7 @@ Dictionary HandleDict(byte count) return map; } - object HandleData(byte marker) + object HandleData(ref BufferReader reader, byte marker) { int byteCount = marker; diff --git a/MetadataExtractor/Formats/Exif/makernotes/OlympusCameraSettingsMakernoteDescriptor.cs b/MetadataExtractor/Formats/Exif/makernotes/OlympusCameraSettingsMakernoteDescriptor.cs index 4feef3f25..a4e7ef5cd 100644 --- a/MetadataExtractor/Formats/Exif/makernotes/OlympusCameraSettingsMakernoteDescriptor.cs +++ b/MetadataExtractor/Formats/Exif/makernotes/OlympusCameraSettingsMakernoteDescriptor.cs @@ -160,30 +160,16 @@ public sealed class OlympusCameraSettingsMakernoteDescriptor(OlympusCameraSettin return null; var sb = new StringBuilder(); - switch (values[0]) + sb.Append(values[0] switch { - case 0: - sb.Append("Single AF"); - break; - case 1: - sb.Append("Sequential shooting AF"); - break; - case 2: - sb.Append("Continuous AF"); - break; - case 3: - sb.Append("Multi AF"); - break; - case 4: - sb.Append("Face detect"); - break; - case 10: - sb.Append("MF"); - break; - default: - sb.Append("Unknown (" + values[0] + ")"); - break; - } + 0 => "Single AF", + 1 => "Sequential shooting AF", + 2 => "Continuous AF", + 3 => "Multi AF", + 4 => "Face detect", + 10 => "MF", + _ => $"Unknown ({values[0]})" + }); if (values.Length > 1) { @@ -231,18 +217,12 @@ public sealed class OlympusCameraSettingsMakernoteDescriptor(OlympusCameraSettin var sb = new StringBuilder(); - switch (values[0]) + sb.Append(values[0] switch { - case 0: - sb.Append("AF not used"); - break; - case 1: - sb.Append("AF used"); - break; - default: - sb.Append("Unknown (" + values[0] + ")"); - break; - } + 0 => "AF not used", + 1 => "AF used", + _ => $"Unknown ({values[0]})" + }); if (values.Length > 1) sb.Append("; " + values[1]); @@ -385,24 +365,14 @@ public sealed class OlympusCameraSettingsMakernoteDescriptor(OlympusCameraSettin var sb = new StringBuilder(); - switch (values[0]) + sb.Append(values[0] switch { - case 0: - sb.Append("Off"); - break; - case 3: - sb.Append("TTL"); - break; - case 4: - sb.Append("Auto"); - break; - case 5: - sb.Append("Manual"); - break; - default: - sb.Append("Unknown (" + values[0] + ")"); - break; - } + 0 => "Off", + 3 => "TTL", + 4 => "Auto", + 5 => "Manual", + _ => $"Unknown ({values[0]})" + }); for (var i = 1; i < values.Length; i++) sb.Append("; ").Append(values[i]); @@ -692,33 +662,17 @@ public sealed class OlympusCameraSettingsMakernoteDescriptor(OlympusCameraSettin return null; var sb = new StringBuilder(); - switch (values[0]) + sb.Append(values[0] switch { - case 1: - sb.Append("Vivid"); - break; - case 2: - sb.Append("Natural"); - break; - case 3: - sb.Append("Muted"); - break; - case 4: - sb.Append("Portrait"); - break; - case 5: - sb.Append("i-Enhance"); - break; - case 256: - sb.Append("Monotone"); - break; - case 512: - sb.Append("Sepia"); - break; - default: - sb.Append("Unknown (").Append(values[0]).Append(')'); - break; - } + 1 => "Vivid", + 2 => "Natural", + 3 => "Muted", + 4 => "Portrait", + 5 => "i-Enhance", + 256 => "Monotone", + 512 => "Sepia", + _ => $"Unknown ({values[0]})" + }); if (values.Length > 1) sb.Append("; ").Append(values[1]); @@ -822,33 +776,18 @@ public sealed class OlympusCameraSettingsMakernoteDescriptor(OlympusCameraSettin sb.Append("Partial Color " + values[i] + "; "); else if (i == 4) { - switch (values[i]) + sb.Append(values[i] switch { - case 0x0000: - sb.Append("No Effect"); - break; - case 0x8010: - sb.Append("Star Light"); - break; - case 0x8020: - sb.Append("Pin Hole"); - break; - case 0x8030: - sb.Append("Frame"); - break; - case 0x8040: - sb.Append("Soft Focus"); - break; - case 0x8050: - sb.Append("White Edge"); - break; - case 0x8060: - sb.Append("B&W"); - break; - default: - sb.Append("Unknown (").Append(values[i]).Append(')'); - break; - } + 0x0000 => "No Effect", + 0x8010 => "Star Light", + 0x8020 => "Pin Hole", + 0x8030 => "Frame", + 0x8040 => "Soft Focus", + 0x8050 => "White Edge", + 0x8060 => "B&W", + _ => $"Unknown ({values[i]})" + }); + sb.Append("; "); } else if (i == 6) diff --git a/MetadataExtractor/Formats/Exif/makernotes/OlympusMakernoteDirectory.cs b/MetadataExtractor/Formats/Exif/makernotes/OlympusMakernoteDirectory.cs index 20de3f2a5..9afa7668a 100644 --- a/MetadataExtractor/Formats/Exif/makernotes/OlympusMakernoteDirectory.cs +++ b/MetadataExtractor/Formats/Exif/makernotes/OlympusMakernoteDirectory.cs @@ -448,9 +448,9 @@ public override void Set(int tagType, object value) base.Set(tagType, value); } - private void ProcessCameraSettings(byte[] bytes) + private void ProcessCameraSettings(ReadOnlySpan bytes) { - var reader = new SequentialByteArrayReader(bytes); + var reader = new BufferReader(bytes, isBigEndian: true); var count = bytes.Length / 4; for (var i = 0; i < count; i++) diff --git a/MetadataExtractor/Formats/Flir/FlirReader.cs b/MetadataExtractor/Formats/Flir/FlirReader.cs index bcd961461..4ac2d3e29 100644 --- a/MetadataExtractor/Formats/Flir/FlirReader.cs +++ b/MetadataExtractor/Formats/Flir/FlirReader.cs @@ -163,8 +163,9 @@ public IEnumerable Extract(IndexedReader reader) directory.Set(TagRawValueMedian, reader2.GetUInt16(TagRawValueMedian)); directory.Set(TagRawValueRange, reader2.GetUInt16(TagRawValueRange)); - var dateTimeBytes = reader2.GetBytes(TagDateTimeOriginal, 10); - var dateTimeReader = new SequentialByteArrayReader(dateTimeBytes, isMotorolaByteOrder: false); + Span dateTimeBytes = stackalloc byte[10]; + reader2.GetBytes(TagDateTimeOriginal, dateTimeBytes); + var dateTimeReader = new BufferReader(dateTimeBytes, isBigEndian: false); var tm = dateTimeReader.GetUInt32(); var ss = dateTimeReader.GetUInt32() & 0xffff; var tz = dateTimeReader.GetInt16(); diff --git a/MetadataExtractor/Formats/Jpeg/JpegReader.cs b/MetadataExtractor/Formats/Jpeg/JpegReader.cs index c9bd4bfae..1ea210b36 100644 --- a/MetadataExtractor/Formats/Jpeg/JpegReader.cs +++ b/MetadataExtractor/Formats/Jpeg/JpegReader.cs @@ -29,35 +29,45 @@ public JpegDirectory Extract(JpegSegment segment) // The value of TagCompressionType is determined by the segment type found directory.Set(JpegDirectory.TagCompressionType, (int)segment.Type - (int)JpegSegmentType.Sof0); - SequentialReader reader = new SequentialByteArrayReader(segment.Bytes); + const int JpegHeaderSize = 1 + 2 + 2 + 1; - try + if (segment.Span.Length < JpegHeaderSize) { - directory.Set(JpegDirectory.TagDataPrecision, reader.GetByte()); - directory.Set(JpegDirectory.TagImageHeight, reader.GetUInt16()); - directory.Set(JpegDirectory.TagImageWidth, reader.GetUInt16()); - - var componentCount = reader.GetByte(); - - directory.Set(JpegDirectory.TagNumberOfComponents, componentCount); - - // For each component, there are three bytes of data: - // 1 - Component ID: 1 = Y, 2 = Cb, 3 = Cr, 4 = I, 5 = Q - // 2 - Sampling factors: bit 0-3 vertical, 4-7 horizontal - // 3 - Quantization table number - - for (var i = 0; i < componentCount; i++) - { - var componentId = reader.GetByte(); - var samplingFactorByte = reader.GetByte(); - var quantizationTableNumber = reader.GetByte(); - var component = new JpegComponent(componentId, samplingFactorByte, quantizationTableNumber); - directory.Set(JpegDirectory.TagComponentData1 + i, component); - } + directory.AddError("Insufficient bytes for JPEG segment header."); + + return directory; } - catch (IOException ex) + + var reader = new BufferReader(segment.Span, isBigEndian: true); + + directory.Set(JpegDirectory.TagDataPrecision, reader.GetByte()); + directory.Set(JpegDirectory.TagImageHeight, reader.GetUInt16()); + directory.Set(JpegDirectory.TagImageWidth, reader.GetUInt16()); + + var componentCount = reader.GetByte(); + + directory.Set(JpegDirectory.TagNumberOfComponents, componentCount); + + const int JpegComponentSize = 1 + 1 + 1; + + if (reader.Available < componentCount * JpegComponentSize) + { + directory.AddError("Insufficient bytes for JPEG the requested number of JPEG components."); + return directory; + } + + // For each component, there are three bytes of data: + // 1 - Component ID: 1 = Y, 2 = Cb, 3 = Cr, 4 = I, 5 = Q + // 2 - Sampling factors: bit 0-3 vertical, 4-7 horizontal + // 3 - Quantization table number + + for (var i = 0; i < componentCount; i++) { - directory.AddError(ex.Message); + var componentId = reader.GetByte(); + var samplingFactorByte = reader.GetByte(); + var quantizationTableNumber = reader.GetByte(); + var component = new JpegComponent(componentId, samplingFactorByte, quantizationTableNumber); + directory.Set(JpegDirectory.TagComponentData1 + i, component); } return directory; diff --git a/MetadataExtractor/Formats/Photoshop/PhotoshopReader.cs b/MetadataExtractor/Formats/Photoshop/PhotoshopReader.cs index 198d3e746..c410f84d7 100644 --- a/MetadataExtractor/Formats/Photoshop/PhotoshopReader.cs +++ b/MetadataExtractor/Formats/Photoshop/PhotoshopReader.cs @@ -53,12 +53,15 @@ public IReadOnlyList Extract(SequentialReader reader, int length) // http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037504 var pos = 0; int clippingPathCount = 0; + + Span signature = stackalloc byte[4]; + while (pos < length) { try { // 4 bytes for the signature ("8BIM", "PHUT", etc.) - var signature = reader.GetString(4, Encoding.UTF8); + reader.GetBytes(signature); pos += 4; // 2 bytes for the resource identifier (tag type). @@ -106,7 +109,7 @@ public IReadOnlyList Extract(SequentialReader reader, int length) } // Skip any unsupported IRBs - if (signature != "8BIM") + if (!signature.SequenceEqual("8BIM"u8)) continue; switch (tagType) diff --git a/MetadataExtractor/Formats/Png/PngChromaticities.cs b/MetadataExtractor/Formats/Png/PngChromaticities.cs index 9d6ab6897..16f6cb75b 100644 --- a/MetadataExtractor/Formats/Png/PngChromaticities.cs +++ b/MetadataExtractor/Formats/Png/PngChromaticities.cs @@ -20,23 +20,16 @@ public PngChromaticities(byte[] bytes) if (bytes.Length != 8 * 4) throw new PngProcessingException("Invalid number of bytes"); - var reader = new SequentialByteArrayReader(bytes); + var reader = new BufferReader(bytes, isBigEndian: true); - try - { - WhitePointX = reader.GetInt32(); - WhitePointY = reader.GetInt32(); - RedX = reader.GetInt32(); - RedY = reader.GetInt32(); - GreenX = reader.GetInt32(); - GreenY = reader.GetInt32(); - BlueX = reader.GetInt32(); - BlueY = reader.GetInt32(); - } - catch (IOException ex) - { - throw new PngProcessingException(ex); - } + WhitePointX = reader.GetInt32(); + WhitePointY = reader.GetInt32(); + RedX = reader.GetInt32(); + RedY = reader.GetInt32(); + GreenX = reader.GetInt32(); + GreenY = reader.GetInt32(); + BlueX = reader.GetInt32(); + BlueY = reader.GetInt32(); } } } diff --git a/MetadataExtractor/Formats/Png/PngDescriptor.cs b/MetadataExtractor/Formats/Png/PngDescriptor.cs index 1f019323e..9894e5bcd 100644 --- a/MetadataExtractor/Formats/Png/PngDescriptor.cs +++ b/MetadataExtractor/Formats/Png/PngDescriptor.cs @@ -82,10 +82,10 @@ public sealed class PngDescriptor(PngDirectory directory) if (bytes is null) return null; - var reader = new SequentialByteArrayReader(bytes); + var reader = new BufferReader(bytes, isBigEndian: true); try { - // TODO do we need to normalise these based upon the bit depth? + // TODO do we need to normalize these based upon the bit depth? switch (bytes.Length) { case 1: diff --git a/MetadataExtractor/Formats/Png/PngHeader.cs b/MetadataExtractor/Formats/Png/PngHeader.cs index 4dd9db709..4cac12ccc 100644 --- a/MetadataExtractor/Formats/Png/PngHeader.cs +++ b/MetadataExtractor/Formats/Png/PngHeader.cs @@ -11,7 +11,7 @@ public PngHeader(byte[] bytes) if (bytes.Length != 13) throw new PngProcessingException("PNG header chunk must have exactly 13 data bytes"); - var reader = new SequentialByteArrayReader(bytes); + var reader = new BufferReader(bytes, isBigEndian: true); ImageWidth = reader.GetInt32(); ImageHeight = reader.GetInt32(); diff --git a/MetadataExtractor/Formats/Png/PngMetadataReader.cs b/MetadataExtractor/Formats/Png/PngMetadataReader.cs index 73f2802d3..85f31beb4 100644 --- a/MetadataExtractor/Formats/Png/PngMetadataReader.cs +++ b/MetadataExtractor/Formats/Png/PngMetadataReader.cs @@ -295,36 +295,56 @@ private static IEnumerable ProcessChunk(PngChunk chunk) } else if (chunkType == PngChunkType.tIME) { - var reader = new SequentialByteArrayReader(bytes); - var year = reader.GetUInt16(); - var month = reader.GetByte(); - int day = reader.GetByte(); - int hour = reader.GetByte(); - int minute = reader.GetByte(); - int second = reader.GetByte(); var directory = new PngDirectory(PngChunkType.tIME); - if (DateUtil.IsValidDate(year, month, day) && DateUtil.IsValidTime(hour, minute, second)) + + if (bytes.Length < 2 + 1 + 1 + 1 + 1 + 1) { - var time = new DateTime(year, month, day, hour, minute, second, DateTimeKind.Unspecified); - directory.Set(PngDirectory.TagLastModificationTime, time); + directory.AddError("Insufficient bytes for PNG tIME chunk."); } else { - directory.AddError($"PNG tIME data describes an invalid date/time: year={year} month={month} day={day} hour={hour} minute={minute} second={second}"); + var reader = new BufferReader(bytes, isBigEndian: true); + + var year = reader.GetUInt16(); + var month = reader.GetByte(); + int day = reader.GetByte(); + int hour = reader.GetByte(); + int minute = reader.GetByte(); + int second = reader.GetByte(); + + if (DateUtil.IsValidDate(year, month, day) && DateUtil.IsValidTime(hour, minute, second)) + { + var time = new DateTime(year, month, day, hour, minute, second, DateTimeKind.Unspecified); + directory.Set(PngDirectory.TagLastModificationTime, time); + } + else + { + directory.AddError($"PNG tIME data describes an invalid date/time: year={year} month={month} day={day} hour={hour} minute={minute} second={second}"); + } + yield return directory; } - yield return directory; } else if (chunkType == PngChunkType.pHYs) { - var reader = new SequentialByteArrayReader(bytes); - var pixelsPerUnitX = reader.GetInt32(); - var pixelsPerUnitY = reader.GetInt32(); - var unitSpecifier = reader.GetSByte(); var directory = new PngDirectory(PngChunkType.pHYs); - directory.Set(PngDirectory.TagPixelsPerUnitX, pixelsPerUnitX); - directory.Set(PngDirectory.TagPixelsPerUnitY, pixelsPerUnitY); - directory.Set(PngDirectory.TagUnitSpecifier, unitSpecifier); - yield return directory; + + if (bytes.Length < 4 + 4 + 1) + { + directory.AddError("Insufficient bytes for PNG pHYs chunk."); + } + else + { + var reader = new BufferReader(bytes, isBigEndian: true); + + var pixelsPerUnitX = reader.GetInt32(); + var pixelsPerUnitY = reader.GetInt32(); + var unitSpecifier = reader.GetSByte(); + + directory.Set(PngDirectory.TagPixelsPerUnitX, pixelsPerUnitX); + directory.Set(PngDirectory.TagPixelsPerUnitY, pixelsPerUnitY); + directory.Set(PngDirectory.TagUnitSpecifier, unitSpecifier); + yield return directory; + } } else if (chunkType.Equals(PngChunkType.sBIT)) { diff --git a/MetadataExtractor/Formats/Wav/WavFactHandler.cs b/MetadataExtractor/Formats/Wav/WavFactHandler.cs index dac863e59..825b155cb 100644 --- a/MetadataExtractor/Formats/Wav/WavFactHandler.cs +++ b/MetadataExtractor/Formats/Wav/WavFactHandler.cs @@ -25,7 +25,7 @@ public WavFactHandler(List directories) protected override void Populate(WavFactDirectory directory, byte[] payload) { - var reader = new SequentialByteArrayReader(payload, isMotorolaByteOrder: false); + var reader = new BufferReader(payload, isBigEndian: false); directory.Set(TagSampleLength, reader.GetUInt32()); } } diff --git a/MetadataExtractor/IO/BufferReader.cs b/MetadataExtractor/IO/BufferReader.cs new file mode 100644 index 000000000..0c237b67f --- /dev/null +++ b/MetadataExtractor/IO/BufferReader.cs @@ -0,0 +1,139 @@ +// Copyright (c) Drew Noakes and contributors. All Rights Reserved. Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Buffers.Binary; + +namespace MetadataExtractor.IO; + +internal ref struct BufferReader(ReadOnlySpan bytes, bool isBigEndian) +{ + private readonly ReadOnlySpan _bytes = bytes; + private readonly bool _isBigEndian = isBigEndian; + + private int _position = 0; + + public readonly int Available => _bytes.Length - _position; + + public readonly int Position => _position; + + public byte GetByte() + { + if (_position >= _bytes.Length) + throw new IOException("End of data reached."); + + return _bytes[_position++]; + } + + public void GetBytes(scoped Span bytes) + { + var buffer = Advance(bytes.Length); + buffer.CopyTo(bytes); + } + + public byte[] GetBytes(int count) + { + var buffer = Advance(count); + var bytes = new byte[count]; + + buffer.CopyTo(bytes); + return bytes; + } + + private ReadOnlySpan Advance(int count) + { + Debug.Assert(count >= 0, "count must be zero or greater"); + + if (_position + count > _bytes.Length) + throw new IOException("End of data reached."); + + var span = _bytes.Slice(_position, count); + + _position += count; + + return span; + } + + public void Skip(int count) + { + Debug.Assert(count >= 0, "count must be zero or greater"); + + if (_position + count > _bytes.Length) + throw new IOException("End of data reached."); + + _position += count; + } + + public sbyte GetSByte() + { + return unchecked((sbyte)_bytes[_position++]); + } + + public ushort GetUInt16() + { + var bytes = Advance(2); + + return _isBigEndian + ? BinaryPrimitives.ReadUInt16BigEndian(bytes) + : BinaryPrimitives.ReadUInt16LittleEndian(bytes); + } + + public short GetInt16() + { + var bytes = Advance(2); + + return _isBigEndian + ? BinaryPrimitives.ReadInt16BigEndian(bytes) + : BinaryPrimitives.ReadInt16LittleEndian(bytes); + } + + public uint GetUInt32() + { + var bytes = Advance(4); + + return _isBigEndian + ? BinaryPrimitives.ReadUInt32BigEndian(bytes) + : BinaryPrimitives.ReadUInt32LittleEndian(bytes); + } + + public int GetInt32() + { + var bytes = Advance(4); + + return _isBigEndian + ? BinaryPrimitives.ReadInt32BigEndian(bytes) + : BinaryPrimitives.ReadInt32LittleEndian(bytes); + } + + public long GetInt64() + { + var bytes = Advance(8); + + return _isBigEndian + ? BinaryPrimitives.ReadInt64BigEndian(bytes) + : BinaryPrimitives.ReadInt64LittleEndian(bytes); + } + + public ulong GetUInt64() + { + var bytes = _bytes.Slice(_position, 8); + Advance(8); + + return _isBigEndian + ? BinaryPrimitives.ReadUInt64BigEndian(bytes) + : BinaryPrimitives.ReadUInt64LittleEndian(bytes); + } + + public string GetString(int bytesRequested, Encoding encoding) + { + // This check is important on .NET Framework + if (bytesRequested is 0) + return ""; + + Span bytes = bytesRequested <= 256 + ? stackalloc byte[bytesRequested] + : new byte[bytesRequested]; + + GetBytes(bytes); + + return encoding.GetString(bytes); + } +}