package org.uic.barcode.asn1.uper; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.uic.barcode.asn1.datatypes.Asn1Default; import org.uic.barcode.asn1.datatypes.Asn1Optional; import org.uic.barcode.asn1.datatypes.FieldOrder; import org.uic.barcode.asn1.datatypes.HasExtensionMarker; import org.uic.barcode.asn1.datatypes.IntRange; import org.uic.barcode.asn1.datatypes.IsExtension; import org.uic.barcode.asn1.datatypes.NoAsn1Field; import org.uic.barcode.asn1.datatypes.SizeRange; import org.uic.barcode.logger.Logger; import org.uic.barcode.logger.LoggerFactory; /** A "quick-and-dirty" implementation of ASN.1 encoder for UPER (Unaligned Packed Encoding Rules). * * @see ITU-T Recommendation X.691 * * TODO: Cover the rest of (useful) ASN.1 datatypes and PER-visible constraints, * write unit tests for them. Clean-up, do more refactoring. **/ public final class UperEncoder { public final static Logger logger = LoggerFactory.getLogger("asnLogger"); private final static int NUM_16K = 16384; @SuppressWarnings("unused") private final static int NUM_32K = 32768; @SuppressWarnings("unused") private final static int NUM_48K = 49152; @SuppressWarnings("unused") private final static int NUM_64K = 65536; private UperEncoder(){} public static byte[] encode(T obj) throws IllegalArgumentException, UnsupportedOperationException { try { BitBuffer bitbuffer = ByteBitBuffer.createInfinite(); encode2(bitbuffer, obj, new Annotation[] {}); bitbuffer.flip(); byte[] result = Arrays.copyOf(bitbuffer.array(), (bitbuffer.limit() + 7) / 8); return result; } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Can't encode " + obj.getClass().getName() + ": " + e, e); } catch (Asn1EncodingException e) { throw new IllegalArgumentException("Can't encode " + obj.getClass().getName() + ":" + e.getMessage(), e); } } public static T decode(byte[] bytes, Class classOfT) throws IllegalArgumentException, UnsupportedOperationException { BitBuffer bitQueue = bitBufferFromBinaryString(binaryStringFromBytes(bytes)); T result = decodeAny(bitQueue, classOfT,null, new Annotation[] {}); if (bitQueue.remaining() > 7) { throw new IllegalArgumentException("Can't fully decode " + classOfT.getName() + ", got (" + result.getClass().getName() + "): " + result + "; remaining " + bitQueue.remaining() + " bits: " + bitQueue); } return result; } public static T decode(byte[] bytes, Class classOfT, Field f) throws IllegalArgumentException, UnsupportedOperationException { BitBuffer bitQueue = bitBufferFromBinaryString(binaryStringFromBytes(bytes)); T result = decodeAny(bitQueue, classOfT, f, new Annotation[] {}); if (bitQueue.remaining() > 7) { throw new IllegalArgumentException("Can't fully decode " + classOfT.getName() + ", got (" + result.getClass().getName() + "): " + result + "; remaining " + bitQueue.remaining() + " bits: " + bitQueue); } return result; } static void encode2(BitBuffer bitbuffer, T obj, Annotation[] extraAnnotations) throws Asn1EncodingException { for (Encoder e : encoders) { if (e.canEncode(obj, extraAnnotations)) { e.encode(bitbuffer, obj, extraAnnotations); return; } } logger.debug(String.format("Can't find encoder for %s",obj.getClass().getSimpleName())); throw new IllegalArgumentException("Can't find encoder for " + obj.getClass().getName() + " with extra annotations " + Arrays.asList(extraAnnotations)); } static T decodeAny(BitBuffer bitbuffer,Class classOfT, Field f, Annotation[] extraAnnotations) { logger.debug(String.format(String.format("Decoding classOfT : %s",classOfT.getCanonicalName()))); for (Decoder e : decoders) { if (e.canDecode(classOfT, extraAnnotations)) { return e.decode(bitbuffer, classOfT,f, extraAnnotations); } } logger.debug(String.format("Can't find decoder for %s",classOfT.getSimpleName())); throw new IllegalArgumentException("Can't find decoder for " + classOfT.getName() + " with extra annotations " + Arrays.asList(extraAnnotations)); } static T getDefault(Class classOfT, Annotation[] annotations) { AnnotationStore annots = new AnnotationStore(classOfT.getAnnotations(), annotations); Asn1Default defaultAnnotation = annots.getAnnotation(Asn1Default.class); if (defaultAnnotation == null){ return null; } Annotation[] defaultAnnots = new Annotation[] {defaultAnnotation}; for (Decoder e : decoders) { if (e.canDecode(classOfT, defaultAnnots)) { return e.getDefault(classOfT, defaultAnnots); } } logger.debug(String.format("Can't find decoder for %s",classOfT.getSimpleName())); throw new IllegalArgumentException("Can't find default for " + classOfT.getName() + " with extra annotations " + defaultAnnotation.toString()); } static IntRange newRange( final long minValue, final long maxValue, final boolean hasExtensionMarker) { return new IntRange() { @Override public Class annotationType() { return IntRange.class; } @Override public long minValue() { return minValue; } @Override public long maxValue() { return maxValue; } @Override public boolean hasExtensionMarker() { return hasExtensionMarker; } }; } static IntRange intRangeFromSizeRange(SizeRange sizeRange) { return newRange(sizeRange.minValue(), sizeRange.maxValue(), sizeRange.hasExtensionMarker()); } private static List encoders = new ArrayList<>(); private static List decoders = new ArrayList<>(); static { encoders.add(new IntCoder()); //encoders.add(new BigIntCoder()); encoders.add(new ByteCoder()); encoders.add(new BooleanCoder()); encoders.add(new SequenceCoder()); encoders.add(new ChoiceCoder()); encoders.add(new EnumCoder()); encoders.add(new BitStringCoder()); encoders.add(new SeqOfCoder()); encoders.add(new StringCoder()); decoders.add(new IntCoder()); //decoders.add(new BigIntCoder()); decoders.add(new ByteCoder()); decoders.add(new BooleanCoder()); decoders.add(new SequenceCoder()); decoders.add(new ChoiceCoder()); decoders.add(new EnumCoder()); decoders.add(new BitStringCoder()); decoders.add(new SeqOfCoder()); decoders.add(new StringCoder()); } static void encodeAsOpenType( BitBuffer bitbuffer, T obj, Annotation[] extraAnnotations) throws IllegalArgumentException, IllegalAccessException, Asn1EncodingException { logger.debug(String.format("OPEN TYPE for {%s}. Encoding preceedes length determinant" ,obj != null ? obj.getClass().getSimpleName() : "null")); BitBuffer tmpbuffer = ByteBitBuffer.createInfinite(); encode2(tmpbuffer, obj, extraAnnotations); int numBytes = (tmpbuffer.position() + 7) / 8; logger.debug(String.format("Encoding open type length determinant (%d) for %s (will be inserted before the open type content)", numBytes, obj != null ? obj.getClass().getName() : "null" )); try { encodeLengthDeterminant(bitbuffer, numBytes); } catch (Asn1EncodingException e) { throw new Asn1EncodingException(" length of open type ", e); } tmpbuffer.flip(); for (int i = 0; i < tmpbuffer.limit(); i++) { bitbuffer.put(tmpbuffer.get()); } //CG padding bits to fill the byte: Open Types are wrapped in an OCTET STRING int paddingBits = numBytes*8 - tmpbuffer.limit(); for (int i = 0; i < paddingBits; i++) { bitbuffer.put(false); } } static T decodeAsOpenType(BitBuffer bitbuffer, Class classOfT,Field f, Annotation[] extraAnnotations) { logger.debug(String.format("OPEN TYPE for %s. Encoding preceedes length determinant", classOfT != null ? classOfT.getName() : "null")); long numBytes = decodeLengthDeterminant(bitbuffer); BitBuffer openTypeBitBuffer = ByteBitBuffer.allocate((int)numBytes * 8); for (int i = 0; i < numBytes * 8; i++) { openTypeBitBuffer.put(bitbuffer.get()); } openTypeBitBuffer.flip(); if (classOfT != null) { T result = decodeAny(openTypeBitBuffer, classOfT, f, extraAnnotations); // Assert that padding bits are all 0. logger.debug(String.format("open type had padding bits")); for (int i = 0; i < openTypeBitBuffer.remaining(); i++) { boolean paddingBit = openTypeBitBuffer.get(); logger.debug(String.format("padding bit %d was <%s>", i, paddingBit ? "1" : "0")); if (paddingBit) { throw new IllegalArgumentException("non-zero padding bit " + i + " for open type " + classOfT.getSimpleName()); } } return result; } else { return null; } } /* * skip an unknown extension element * - decode length * - skip the bytes according to the length */ static void decodeSkipUnknownElement(BitBuffer bitbuffer, String name) { logger.debug(String.format("Skip unknown extension in %s. Encoding preceedes length determinant", name)); long numBytes = decodeLengthDeterminant(bitbuffer); for (int i = 0; i < numBytes * 8; i++) { bitbuffer.get(); } logger.debug(String.format(String.format("Skiped %d bytes", numBytes))); } static boolean hasNonNullExtensions( T obj, Asn1ContainerFieldSorter sorter) throws IllegalArgumentException, IllegalAccessException { for (Field f : sorter.extensionFields) { //CG elements with default value will not be not included if (f.get(obj) != null && !isDefault(f,f.get(obj)) ) { return true; } } return false; } private static Constructor findConsturctor(Class classOfT, Object... parameters) { @SuppressWarnings("unchecked") Constructor[] declaredConstructors = (Constructor[]) classOfT .getDeclaredConstructors(); for (Constructor c : declaredConstructors) { Class[] parameterTypes = c.getParameterTypes(); if (parameterTypes.length == parameters.length) { boolean constructorIsOk = true; for (int i = 0; i < parameters.length; i++) { if (!parameterTypes[i].isAssignableFrom(parameters[i].getClass())) { constructorIsOk = false; break; } } if (constructorIsOk) { return c; } } } Class[] parameterTypes = new Class[parameters.length]; for (int i = 0; i < parameters.length; i++) { parameterTypes[i] = parameters[i].getClass(); } throw new IllegalArgumentException("Can't get the " + parameters.length + "-argument constructor for parameter(s) " + Arrays.asList(parameters) + " of type(s) " + Arrays.asList(parameterTypes) + " for class " + classOfT.getName() + " (" + classOfT.getClass().getName() + " or " + Arrays.asList(classOfT.getClasses()) + ")" + ", all constructors: " + Arrays.asList(classOfT.getDeclaredConstructors())); } /** Instantiate a given class T using given parameters. */ static T instantiate(Class classOfT, Object... parameters) { Class[] parameterTypes = new Class[parameters.length]; for (int i = 0; i < parameters.length; i++) { parameterTypes[i] = parameters[i].getClass(); } Constructor constructor = findConsturctor(classOfT, parameters); boolean constructorIsAccessible = constructor.isAccessible(); constructor.setAccessible(true); T result; try { result = constructor.newInstance(parameters); } catch (IllegalArgumentException | InvocationTargetException | InstantiationException | IllegalAccessException e) { throw new IllegalArgumentException("Can't instantiate " + classOfT.getName(), e); } constructor.setAccessible(constructorIsAccessible); return result; } static long decodeConstrainedInt(BitBuffer bitqueue, IntRange intRange) { long lowerBound = intRange.minValue(); long upperBound = intRange.maxValue(); boolean hasExtensionMarker = intRange.hasExtensionMarker(); if (upperBound < lowerBound) { throw new IllegalArgumentException("Lower bound " + lowerBound + " is larger that upper bound " + upperBound); } if (hasExtensionMarker) { boolean extensionIsActive = bitqueue.get(); if (extensionIsActive) { //in extensions are encoded as uncontraint integers, thius an Asn1BigInteger type should be used(a lower range bound might be applied). throw new UnsupportedOperationException("int extension are not supported yet"); } } final Long range = upperBound - lowerBound + 1; if (range == 1) { return lowerBound; } int bitlength = BigInteger.valueOf(range - 1).bitLength(); logger.debug(String.format("This int will require %d bits, available %d" , bitlength, bitqueue.remaining())); BitBuffer relevantBits = ByteBitBuffer.allocate( ((bitlength + 7) / 8) * 8); // Full bytes. int numPaddingBits = (8 - (bitlength % 8)) % 8; // Leading padding 0-bits. for (int i = 0; i < numPaddingBits; i++) { relevantBits.put(false); } for (int i = 0; i < bitlength; i++) { relevantBits.put(bitqueue.get()); } relevantBits.flip(); final BigInteger big = new BigInteger(+1, relevantBits.array()); final Long result = lowerBound + big.longValue(); logger.debug(String.format("bits %s decoded as %d plus lower bound %d give %d", relevantBits.toBooleanStringFromPosition(0), big.longValue(), lowerBound, result)); if ((result < intRange.minValue() || intRange.maxValue() < result) && !intRange.hasExtensionMarker()) { throw new AssertionError("Decoded value " + result + " is outside of range (" + intRange.minValue() + ".." + intRange.maxValue() + ")"); } return result; } //CG Begin static boolean isDefault(Field f, Object obj) { if (f.getAnnotation(Asn1Default.class) != null) { String value = f.getAnnotation(Asn1Default.class).value(); if (obj.toString().equals(value)){ return true; } } return false; } public static boolean isNotAsn1(Field f) { return (f.getAnnotation(NoAsn1Field.class) != null); } //CG End static boolean hasExtensionMarker(AnnotationStore annotations) { return annotations.getAnnotation(HasExtensionMarker.class) != null; } private static boolean isExtension(Field f) { return f.getAnnotation(IsExtension.class) != null; } static boolean isMandatory(Field f) { return !isOptional(f); } static boolean isOptional(Field f) { //CG elements with default value are treated as optional as they are not encoded in case of the default value if (f.getAnnotation(Asn1Optional.class) != null || f.getAnnotation(Asn1Default.class) != null) { return true; } return false; } static class Asn1ContainerFieldSorter { /** "Outside extension root" */ List extensionFields = new ArrayList<>(); List optionalExtensionFields = new ArrayList<>(); List mandatoryExtensionField = new ArrayList<>(); /** "Within extension root" */ List ordinaryFields = new ArrayList<>(); List mandatoryOrdinaryFields = new ArrayList<>(); List optionalOrdinaryFields = new ArrayList<>(); List allFields = new ArrayList<>(); // Excluding test instrumentation. Map originalAccess = new HashMap<>(); Asn1ContainerFieldSorter(Class type) { /* * * sorting of the fields added to compensate the error * in the java SDK on android where getDeclaredFields does * not return the fields in the order of the class definition * */ List fields = Arrays.asList(type.getDeclaredFields()); Collections.sort(fields, new Comparator() { @Override public int compare(Field o1, Field o2) { FieldOrder ao1 = o1.getAnnotation(FieldOrder.class); FieldOrder ao2 = o2.getAnnotation(FieldOrder.class); int order1 = ao1 == null ? Integer.MAX_VALUE : ao1.order(); int order2 = ao2 == null ? Integer.MAX_VALUE : ao2.order(); if (order1 == Integer.MAX_VALUE || order2 == Integer.MAX_VALUE || order1 < 0 || order2 < 0 || order1 == order2) { logger.debug(String.format("field order error for %s",type.getSimpleName())); } return Integer.compare(order1, order2); } }); for (Field f : fields) { if (isTestInstrumentation(f) || isNonAsn1Field(f) ) { continue; } originalAccess.put(f, f.isAccessible()); f.setAccessible(true); if (isExtension(f)) { extensionFields.add(f); if (isOptional(f)) { optionalExtensionFields.add(f); } else { mandatoryExtensionField.add(f); } } else { ordinaryFields.add(f); } allFields.add(f); } for (Field f : ordinaryFields) { if (isMandatory(f)) { mandatoryOrdinaryFields.add(f); } else { optionalOrdinaryFields.add(f); } } } public void revertAccess() { for (Entry entry : originalAccess.entrySet()) { entry.getKey().setAccessible(entry.getValue()); } } } static boolean isTestInstrumentation(Field f) { return f.getName().startsWith("$"); } static boolean isNonAsn1Field(Field f) { if (f.getAnnotation(NoAsn1Field.class) != null) { return true; } return false; } static void encodeLengthOfBitmask(BitBuffer bitbuffer, int n) throws Asn1EncodingException { try { if (n <= 63) { logger.debug(String.format("normally small length of bitmask, length %d <= 63 indicated as bit <0>", n)); bitbuffer.put(false); encodeConstrainedInt(bitbuffer, n, 1, 63); return; } else { logger.debug(String.format("normally small length of bitmask, length %s > 63 indicated as bit <1>", n)); bitbuffer.put(true); encodeLengthDeterminant(bitbuffer, n); return; } } catch (Asn1EncodingException e) { throw new Asn1EncodingException(" length of bitmask ", e); } } static void encodeSmallInt(BitBuffer bitbuffer, int n) throws Asn1EncodingException { try { if (n <= 63) { logger.debug(String.format("normally small length of bitmask, length %d <= 63 indicated as bit <0>", n)); bitbuffer.put(false); encodeConstrainedInt(bitbuffer, n, 0, 63); return; } else { logger.debug(String.format("normally small length of bitmask, length %s > 63 indicated as bit <1>", n)); bitbuffer.put(true); encodeLengthDeterminant(bitbuffer, n); return; } } catch (Asn1EncodingException e) { throw new Asn1EncodingException(" length of bitmask ", e); } } static void encodeLengthDeterminant(BitBuffer bitbuffer, int n) throws Asn1EncodingException { try { int position = bitbuffer.position(); if (n < 128) { bitbuffer.put(false); encodeConstrainedInt(bitbuffer, n, 0, 127); logger.debug(String.format("Length determinant %d, encoded as <%s>", n, bitbuffer.toBooleanStringFromPosition(position))); if (bitbuffer.position() - position != 8) { throw new AssertionError("length determinant encoded not as 8 bits"); } return; } else if (n < NUM_16K) { bitbuffer.put(true); bitbuffer.put(false); encodeConstrainedInt(bitbuffer, n, 0, NUM_16K - 1); logger.debug(String.format("Length determinant %d, encoded as 2bits+14bits: <%s>", n,bitbuffer.toBooleanStringFromPosition(position))); if (bitbuffer.position() - position != 16) { throw new AssertionError("length determinant encoded not as 16 bits"); } return; } else { throw new UnsupportedOperationException("Length greater than 16K is not supported yet."); } } catch (Asn1EncodingException e) { throw new Asn1EncodingException(" length determinant ", e); } } static long decodeLengthOfBitmask(BitBuffer bitbuffer) { logger.debug("decoding length of bitmask"); boolean isGreaterThan63 = bitbuffer.get(); logger.debug(String.format("length determinant extension preamble size flag: preamble size > 63 is %s", isGreaterThan63)); if (!isGreaterThan63) { Long result = decodeConstrainedInt(bitbuffer, newRange(1, 63, false)); logger.debug(String.format("normally small length of bitmask, length <= 63, decoded as %d", result)); return result; } else { logger.debug(String.format("normally small length of bitmask, length > 63, decoding as ordinary length determinant...")); return decodeLengthDeterminant(bitbuffer); } } static long decodeSmallInt(BitBuffer bitbuffer) { logger.debug("decoding small int"); boolean isGreaterThan63 = bitbuffer.get(); logger.debug(String.format("length determinant extension preamble size flag: preamble size > 63 is %s", isGreaterThan63)); if (!isGreaterThan63) { Long result = decodeConstrainedInt(bitbuffer, newRange(0, 63, false)); logger.debug(String.format("normally small length of bitmask, length <= 63, decoded as %d", result)); return result; } else { logger.debug(String.format("normally small length of bitmask, length > 63, decoding as ordinary length determinant...")); return decodeLengthDeterminant(bitbuffer); } } static long decodeLengthDeterminant(BitBuffer bitbuffer) { boolean bit8 = bitbuffer.get(); if (!bit8) { // then value is less than 128 Long result = decodeConstrainedInt(bitbuffer, newRange(0, 127, false)); logger.debug(String.format("length determinant, decoded as %d", result)); return result; } else { boolean bit7 = bitbuffer.get(); if (!bit7) { // then value is less than 16K Long result = decodeConstrainedInt(bitbuffer, newRange(0, NUM_16K - 1, false)); logger.debug(String.format("length determinant, decoded as %d", result)); return result; } else { // "Large" n logger.debug("lengthes longer than 16K are not supported yet."); throw new UnsupportedOperationException("lengthes longer than 16K are not supported yet."); } } } static void encodeConstrainedInt( final BitBuffer bitbuffer, final long value, final long lowerBound, final long upperBound) throws Asn1EncodingException { encodeConstrainedInt(bitbuffer, value, lowerBound, upperBound, false); } static void encodeConstrainedInt( final BitBuffer bitbuffer, final long value, final long lowerBound, final long upperBound, final boolean hasExtensionMarker ) throws Asn1EncodingException { if (upperBound < lowerBound) { throw new IllegalArgumentException("Lower bound " + lowerBound + " is larger than upper bound " + upperBound); } if (!hasExtensionMarker && (value < lowerBound || value > upperBound)) { throw new Asn1EncodingException( " Value " + value + " is outside of fixed range " + lowerBound + ".." + upperBound); } final Long range = upperBound - lowerBound + 1; final int position = bitbuffer.position(); if (hasExtensionMarker) { boolean outsideOfRange = value < lowerBound || value > upperBound; logger.debug(String.format("constrained int with extension marker, %s extension range",outsideOfRange ? "outside" : "within", outsideOfRange ? "1" : "0")); bitbuffer.put(outsideOfRange); if (outsideOfRange) { throw new UnsupportedOperationException( "INT extensions are not supported yet"); } } if (range == 1) { logger.debug("constrained int of empty range, resulting in empty encoding <>"); return; } final BigInteger big = BigInteger.valueOf(value - lowerBound); final int numPaddingBits = BigInteger.valueOf(range - 1).bitLength() - big.bitLength(); for (int i = 0; i < numPaddingBits; i++) { bitbuffer.put(false); } for (int i = big.bitLength() - 1; i >= 0; i--) { bitbuffer.put(big.testBit(i)); } logger.debug(String.format("constrained int %d encoded as <%s>", value, bitbuffer.toBooleanStringFromPosition(position))); return; } public static byte[] bytesFromCollection(List bitlist) { int sizeBytes = (bitlist.size() + 7) / 8; byte[] result = new byte[sizeBytes]; int byteId = 0; byte bitId = 7; logger.debug(String.format("byte: < %s >", bitlist)); for (Boolean b : bitlist) { //logger.debug(String.format("bitId: %s, byteId: %s, value: %s", bitId, byteId, b)); result[byteId] |= (b ? 1 : 0) << bitId; bitId--; if (bitId < 0) { bitId = 7; byteId++; } } int nZeros = sizeBytes * 8 - bitlist.size(); String zeros = nZeros > 0 ? String.format("%0" + nZeros + "d", 0) : ""; logger.debug(String.format("Padding bits (%d): <%s>", nZeros, zeros)); return result; } final protected static char[] hexArray = "0123456789ABCDEF".toCharArray(); public static String hexStringFromBytes(byte[] bytes) { char[] hexChars = new char[bytes.length * 2]; for (int j = 0; j < bytes.length; j++) { int v = bytes[j] & 0xFF; hexChars[j * 2] = hexArray[v >>> 4]; hexChars[j * 2 + 1] = hexArray[v & 0x0F]; } return new String(hexChars); } public static byte[] bytesFromHexString(String s) { int len = s.length(); byte[] data = new byte[len / 2]; for (int i = 0; i < len; i += 2) { data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16)); } return data; } public static String binaryStringFromBytes(byte[] bytes) { StringBuilder sb = new StringBuilder(bytes.length * Byte.SIZE); for (int i = 0; i < Byte.SIZE * bytes.length; i++) sb.append((bytes[i / Byte.SIZE] << i % Byte.SIZE & 0x80) == 0 ? '0' : '1'); return sb.toString(); } private static BitBuffer bitBufferFromBinaryString(String s) { ByteBitBuffer result = ByteBitBuffer.allocate(s.length()); for (int i = 0; i < s.length(); i++) { if (s.charAt(i) != '1' && s.charAt(i) != '0') { throw new IllegalArgumentException("bad character in 'binary' string " + s.charAt(i)); } result.put(s.charAt(i) == '1'); } result.flip(); return result; } }