[java-support] branch dev/JSPT-111 updated: Deserialization and tests.

Scott Cantor cantor.2 at osu.edu
Thu May 20 19:02:35 UTC 2021


This is an automated email from the git hooks/post-receive script.

scantor pushed a commit to branch dev/JSPT-111
in repository java-support.

View the commit online:
http://git.shibboleth.net/view/?p=java-support.git;a=commit;h=bca370686e22568b33859a4b172bc2b426c19ac6

The following commit(s) were added to refs/heads/dev/JSPT-111 by this push:
       new  bca3706   Deserialization and tests.
bca3706 is described below

commit bca370686e22568b33859a4b172bc2b426c19ac6
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Thu May 20 15:02:33 2021 -0400

    Deserialization and tests.
---
 .../shibboleth/utilities/java/support/ddf/DDF.java | 215 ++++++++++++++++++++-
 .../utilities/java/support/ddf/DDFTest.java        |  67 +++++++
 2 files changed, 280 insertions(+), 2 deletions(-)

diff --git a/src/main/java/net/shibboleth/utilities/java/support/ddf/DDF.java b/src/main/java/net/shibboleth/utilities/java/support/ddf/DDF.java
index 25724fc..5b66a92 100644
--- a/src/main/java/net/shibboleth/utilities/java/support/ddf/DDF.java
+++ b/src/main/java/net/shibboleth/utilities/java/support/ddf/DDF.java
@@ -18,7 +18,9 @@
 package net.shibboleth.utilities.java.support.ddf;
 
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.OutputStream;
+import java.net.URLDecoder;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -102,7 +104,7 @@ public class DDF implements Iterable<DDF> {
          * 
          * @param val value of the type enum
          */
-        private DDFType(@NonNegative final int val) {
+        private DDFType(final int val) {
             value = val;
         }
         
@@ -115,6 +117,60 @@ public class DDF implements Iterable<DDF> {
             return value;
         }
         
+        /**
+         * Convert an integer into the corresponding enum value.
+         * 
+         * @param val input type
+         * 
+         * @return enum constant
+         * 
+         * @throws IllegalArgumentException if the type is out of range
+         */
+        public static DDFType valueOf(final int val) throws IllegalArgumentException {
+            final DDFType type;
+            switch (val) {
+                case -1:
+                    type = DDF_NULL;
+                    break;
+                        
+                case 0:
+                    type = DDF_EMPTY;
+                    break;
+                        
+                case 1:
+                    type = DDF_STRING;
+                    break;
+                        
+                case 2:
+                    type = DDF_INT;
+                    break;
+
+                case 3:
+                    type = DDF_FLOAT;
+                    break;
+                    
+                case 4:
+                    type = DDF_STRUCT;
+                    break;
+                    
+                case 5:
+                    type = DDF_LIST;
+                    break;
+
+                case 6:
+                    type = DDF_POINTER;
+                    break;
+                    
+                case 7:
+                    type = DDF_STRING_UNSAFE;
+                    break;
+                    
+                default:
+                    throw new IllegalArgumentException("Unrecognized DDF type");
+            }
+            return type;
+        }
+        
     };
     
     /** Node type. */
@@ -211,6 +267,7 @@ public class DDF implements Iterable<DDF> {
      * 
      * @return the copy
      */
+// Checkstyle: CyclomaticComplexity OFF
     @SuppressWarnings("unchecked")
     @Nonnull DDF copy() {
         final DDF dup = new DDF(name);
@@ -258,6 +315,7 @@ public class DDF implements Iterable<DDF> {
         
         return dup;
     }
+// Checkstyle: CyclomaticComplexity ON
     
     /**
      * Get the node name.
@@ -835,7 +893,7 @@ public class DDF implements Iterable<DDF> {
                 } catch(final NumberFormatException e) {
                     index = 0;
                 }
-                if (islist() && index < ((List<DDF>) current.value).size()) {
+                if (current.islist() && index < ((List<DDF>) current.value).size()) {
                     current = ((List<DDF>) current.value).get(index);
                 } else {
                     return new DDF();
@@ -1153,6 +1211,159 @@ public class DDF implements Iterable<DDF> {
         
         return os;
     }
+    
+    /**
+     * Parses a seralized DDF from an input stream.
+     * 
+     * @param is input stream
+     * 
+     * @return the parsed object
+     * 
+     * @throws IOException if an error occurs
+     */
+    @Nonnull public static DDF deserialize(@Nonnull final InputStream is) throws IOException {
+        
+        int ch;
+        final StringBuilder nameBuilder = new StringBuilder();
+
+        // First field is the name.
+        while ((ch = is.read()) != -1 && !Character.isWhitespace(ch)) {
+            if (ch >= 0 && ch <= 127) {
+                // The int is a code point from 0..255, but our grammar constrains this to 0..127 so
+                // this is a safe append, to promote the ASCII into Unicode.
+                nameBuilder.appendCodePoint(ch);
+            } else {
+                throw new IOException("Invalid code point outside US-ASCII range");
+            }
+        }
+        
+        if (ch != 0x20) {
+            // Name has to be followed by a space.
+            // This will also cover an early line or stream termination.
+            throw new IOException("Name not followed by space character");
+        }
+        
+        final String name = nameBuilder.toString();
+        if (name.isEmpty()) {
+            // No name field.
+            throw new IOException("Name field missing");
+        }
+        
+        final DDF obj = new DDF(null);
+        if (!".".equals(name)) {
+            // The name is stipulated to be UTF-8 safe so any high order ASCII characters are
+            // assumed to be part of a multi-byte sequence.
+            try {
+                obj.name(URLDecoder.decode(name, "UTF-8"));
+            } catch (final IllegalArgumentException e) {
+                throw new IOException(e);
+            }
+        }
+        
+        // Next field is the numeric type designation.
+        final StringBuilder typeBuilder = new StringBuilder();
+        while ((ch = is.read()) != -1 && Character.isDigit(ch)) {
+            // This is safe because the byte contract of the stream disallows
+            // any non-ASCII digit from satisfying the isDigit check.
+            typeBuilder.appendCodePoint(ch);
+        }
+        
+        // Before continuing, we convert the string into a DDF type.
+        final DDFType type;
+        try {
+            type = DDFType.valueOf(Integer.valueOf(typeBuilder.toString()));
+        } catch (final IllegalArgumentException e) {
+            throw new IOException("Invalid DDF type");
+        }
+        
+        // Required last byte read will vary by type.
+        if (type == DDFType.DDF_EMPTY || type == DDFType.DDF_POINTER) {
+            if (ch != 0x0A) {
+                throw new IOException("Empty/pointer record not terminated by linefeed");
+            }
+            // Nothing else to do, it's already empty.
+            return obj;
+        }
+        
+        // All others should be followed by a space.
+        if (ch != 0x20) {
+            throw new IOException("Type field not followed by space character");
+        }
+
+        // Process typical value types.
+        final StringBuilder valueBuilder = new StringBuilder();
+        switch (type) {
+            case DDF_STRING:
+            case DDF_STRING_UNSAFE:
+            case DDF_INT:
+            case DDF_FLOAT:
+                while ((ch = is.read()) != -1 && !Character.isWhitespace(ch)) {
+                    if (ch >= 0 && ch <= 127) {
+                        // The int is a code point from 0..255, but our grammar constrains this to 0..127 so
+                        // this is a safe append, to promote the ASCII into Unicode.
+                        valueBuilder.appendCodePoint(ch);
+                    } else {
+                        throw new IOException("Invalid code point outside US-ASCII range");
+                    }
+                }
+                
+                if (ch != 0x0A) {
+                    throw new IOException("String value not followed by linefeed");
+                }
+                
+                try {
+                    if (type == DDFType.DDF_STRING) {
+                        // String values are handled as UTF-8.
+                        obj.string(URLDecoder.decode(valueBuilder.toString(), "UTF-8"));
+                    } else if (type == DDFType.DDF_STRING_UNSAFE) {
+                        // Unsafe string values are processed as ISO-8859-1.
+                        // They may be anything, but it will guarantee a single byte encoding.
+                        obj.unsafe_string(URLDecoder.decode(valueBuilder.toString(), "ISO-8859-1"));
+                    } else if (type == DDFType.DDF_INT) {
+                        obj.integer(valueBuilder.toString());
+                    } else if (type == DDFType.DDF_FLOAT) {
+                        obj.floating(valueBuilder.toString());
+                    }
+                } catch (final IllegalArgumentException e) {
+                    throw new IOException(e);
+                }
+                return obj;
+                
+            case DDF_STRUCT:
+            case DDF_LIST:
+                while ((ch = is.read()) != -1 && Character.isDigit(ch)) {
+                    // This is safe because the byte contract of the stream disallows
+                    // any non-ASCII digit from satisfying the isDigit check.
+                    valueBuilder.appendCodePoint(ch);
+                }
+                
+                if (ch != 0x0A) {
+                    throw new IOException("Record count not followed by linefeed");
+                }
+                
+                int count;
+                try {
+                    count = Integer.valueOf(valueBuilder.toString());
+                } catch (final NumberFormatException e) {
+                    throw new IOException("Invalid record count");
+                }
+                
+                if (type == DDFType.DDF_STRUCT) {
+                    obj.structure();
+                } else {
+                    obj.list();
+                }
+                
+                for (; count > 0; --count) {
+                    obj.add(deserialize(is));
+                }
+                return obj;
+                
+
+            default:
+                throw new IOException("Unexpected record type");
+        }
+    }
 // Checkstyle: MethodLength|CyclomaticComplexity ON
 
     /**
diff --git a/src/test/java/net/shibboleth/utilities/java/support/ddf/DDFTest.java b/src/test/java/net/shibboleth/utilities/java/support/ddf/DDFTest.java
index 7ed2a1e..1889c58 100644
--- a/src/test/java/net/shibboleth/utilities/java/support/ddf/DDFTest.java
+++ b/src/test/java/net/shibboleth/utilities/java/support/ddf/DDFTest.java
@@ -19,8 +19,10 @@ package net.shibboleth.utilities.java.support.ddf;
 
 import static org.testng.Assert.*;
 
+import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
 
 import javax.annotation.Nonnull;
 
@@ -247,6 +249,71 @@ public class DDFTest {
         }
     }
     
+    @Test
+    public void testDeserialize() throws IOException {
+        try (final InputStream is = getClass().getResourceAsStream("empty-noname.ddf")) {
+            final DDF obj = DDF.deserialize(is);
+            assertTrue(obj.isempty());
+            assertNull(obj.name());
+        }
+        
+        try (final InputStream is = getClass().getResourceAsStream("empty-name.ddf")) {
+            final DDF obj = DDF.deserialize(is);
+            assertTrue(obj.isempty());
+            assertEquals(obj.name(), "foo bar");
+        }
+        
+        try (final InputStream is = getClass().getResourceAsStream("string-name.ddf")) {
+            final DDF obj = DDF.deserialize(is);
+            assertTrue(obj.isstring());
+            assertEquals(obj.name(), "foo bar");
+            assertEquals(obj.string(), "zorkmid☯️");
+        }
+
+        try (final InputStream is = getClass().getResourceAsStream("unsafestring-name.ddf")) {
+            final DDF obj = DDF.deserialize(is);
+            assertTrue(obj.isstring());
+            assertEquals(obj.name(), "foo bar");
+            final byte[] unsafe = {102, 111, 111, -128, 98, 97, 114};
+            assertEquals(obj.string(), new String(unsafe, "ISO-8859-1"));
+        }
+
+        try (final InputStream is = getClass().getResourceAsStream("int-name.ddf")) {
+            final DDF obj = DDF.deserialize(is);
+            assertTrue(obj.isint());
+            assertEquals(obj.name(), "foo bar");
+            assertEquals(obj.integer(), Integer.valueOf(42));
+        }
+
+        try (final InputStream is = getClass().getResourceAsStream("float-name.ddf")) {
+            final DDF obj = DDF.deserialize(is);
+            assertTrue(obj.isfloat());
+            assertEquals(obj.name(), "foo bar");
+            assertEquals(obj.floating(), Double.valueOf(42.1315927));
+        }
+
+        try (final InputStream is = getClass().getResourceAsStream("struct-empty.ddf")) {
+            final DDF obj = DDF.deserialize(is);
+            assertTrue(obj.isstruct());
+            assertEquals(obj.name(), "foo bar");
+            assertEquals(obj.integer(), Integer.valueOf(0));
+        }
+
+        try (final InputStream is = getClass().getResourceAsStream("struct-complex.ddf")) {
+            final DDF obj = DDF.deserialize(is);
+            assertTrue(obj.isstruct());
+            assertEquals(obj.name(), "foo bar");
+            assertEquals(obj.integer(), Integer.valueOf(1));
+            assertTrue(obj.getmember("infocom").isstruct());
+            assertTrue(obj.getmember("infocom.zork").islist());
+            assertEquals(obj.getmember("infocom.zork").integer(), Integer.valueOf(3));
+            assertEquals(obj.getmember("infocom.zork.[0]").integer(), Integer.valueOf(1));
+            assertEquals(obj.getmember("infocom.zork.[1]").integer(), Integer.valueOf(2));
+            assertEquals(obj.getmember("infocom.zork.[2]").integer(), Integer.valueOf(3));
+        }
+    }
+    
+    
     /**
      * Convert test file contents to a byte array.
      * 

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list