Repository: jhy/jsoup
Pull request #1584 (2021-07-12 23:02:23)

Bump junit-jupiter from 5.6.0 to 5.7.2
Bumps [junit-jupiter](https://github.com/junit-team/junit5) from 5.6.0 to 5.7.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/junit-team/junit5/releases">junit-jupiter's releases</a>.</em></p>
<blockquote>
<p>JUnit 5.7.2 = Platform 1.7.2 + Jupiter 5.7.2 + Vintage 5.7.2</p>
<p>See <a href="http://junit.org/junit5/docs/5.7.2/release-notes/">Release Notes</a>.</p>
<p>JUnit 5.7.1 = Platform 1.7.1 + Jupiter 5.7.1 + Vintage 5.7.1</p>
<p>See <a href="http://junit.org/junit5/docs/5.7.1/release-notes/">Release Notes</a>.</p>
<p>JUnit 5.7.0 = Platform 1.7.0 + Jupiter 5.7.0 + Vintage 5.7.0</p>
<p>See <a href="http://junit.org/junit5/docs/5.7.0/release-notes/">Release Notes</a>.</p>
<p>JUnit 5.7.0-RC1 = Platform 1.7.0-RC1 + Jupiter 5.7.0-RC1 + Vintage 5.7.0-RC1</p>
<p>See <a href="http://junit.org/junit5/docs/5.7.0-RC1/release-notes/">Release Notes</a>.</p>
<p>JUnit 5.7.0-M1 = Platform 1.7.0-M1 + Jupiter 5.7.0-M1 + Vintage 5.7.0-M1</p>
<p>See <a href="http://junit.org/junit5/docs/5.7.0-M1/release-notes/">Release Notes</a>.</p>
<p>JUnit 5.6.3 = Platform 1.6.3 + Jupiter 5.6.3 + Vintage 5.6.3</p>
<p>See <a href="http://junit.org/junit5/docs/5.6.3/release-notes/">Release Notes</a>.</p>
<p>JUnit 5.6.2 = Platform 1.6.2 + Jupiter 5.6.2 + Vintage 5.6.2</p>
<p>See <a href="http://junit.org/junit5/docs/5.6.2/release-notes/">Release Notes</a>.</p>
<p>JUnit 5.6.1 = Platform 1.6.1 + Jupiter 5.6.1 + Vintage 5.6.1</p>
<p>See <a href="http://junit.org/junit5/docs/5.6.1/release-notes/">Release Notes</a>.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/junit-team/junit5/commit/0ba600e478c728cdb80c83dcbf554c08f7a0755c"><code>0ba600e</code></a> Move <a href="https://github-redirect.dependabot.com/junit-team/junit5/issues/2423">#2423</a> to 5.7.2</li>
<li><a href="https://github.com/junit-team/junit5/commit/e6d74ab2f2d17c88c166a30fbcd8bb97f3fc8f31"><code>e6d74ab</code></a> Acquire global read lock in presence of other exclusive resources</li>
<li><a href="https://github.com/junit-team/junit5/commit/267ce626491b02a86af7fb94191313adb8959f8e"><code>267ce62</code></a> Add Stalebot configuration</li>
<li><a href="https://github.com/junit-team/junit5/commit/05414dc722cee139718c8de7202e3f8a79a1f680"><code>05414dc</code></a> Upgradle to 7.0.1</li>
<li><a href="https://github.com/junit-team/junit5/commit/8d9ef8045bc28645953baace63999cc994e1d4e1"><code>8d9ef80</code></a> Update spring-nohttp plugin</li>
<li><a href="https://github.com/junit-team/junit5/commit/4e98f5530b6d731f9167cbe4e43cf8d0fcc2f525"><code>4e98f55</code></a> Hard-wrap at 90 chars and add indentation</li>
<li><a href="https://github.com/junit-team/junit5/commit/6359e512436a642ad1efc986f754105344b94ff3"><code>6359e51</code></a> Add Provide-Capability OSGi header to engine bundles</li>
<li><a href="https://github.com/junit-team/junit5/commit/a02c62ac53a9d4d271e4bfd8ee28f438d4dbe4f8"><code>a02c62a</code></a> Move entry to 5.7.2 release notes</li>
<li><a href="https://github.com/junit-team/junit5/commit/4f206d347fda5c84b1a46245b0c57d427f24039b"><code>4f206d3</code></a> Update shadow plugin</li>
<li><a href="https://github.com/junit-team/junit5/commit/81e6b008f742a5d2a4e1aaa23e409199f1c55b86"><code>81e6b00</code></a> Update Kotlin plugin</li>
<li>Additional commits viewable in <a href="https://github.com/junit-team/junit5/compare/r5.6.0...r5.7.2">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.junit.jupiter:junit-jupiter&package-manager=maven&previous-version=5.6.0&new-version=5.7.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/CHANGES b/CHANGES
index b86ecbf7..d60b728b 100644
--- a/CHANGES
+++ b/CHANGES
@@ -23,6 +23,10 @@ jsoup changelog
   * Bugfix [Fuzz]: malformed deeply nested table elements could create a stack overflow.
     <https://github.com/jhy/jsoup/issues/1577>
 
+  * Bugfix [Fuzz]: Speed optimized malformed HTML creating elements with thousands of elements - limit the attribute
+    count per element when parsing to 512 (in real-world HTML, P99 is ~ 8).
+    <https://github.com/jhy/jsoup/issues/1578>
+
 *** Release 1.14.1 [2021-Jul-10]
   * Change: updated the minimum supported Java version from Java 7 to Java 8.
 
diff --git a/pom.xml b/pom.xml
index ab146c3c..9fefc2e5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -83,7 +83,7 @@
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-javadoc-plugin</artifactId>
-        <version>3.1.0</version>
+        <version>3.3.0</version>
         <configuration>
             <doclint>none</doclint>
             <source>7</source>
@@ -156,7 +156,7 @@
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-resources-plugin</artifactId>
-        <version>3.0.1</version>
+        <version>3.2.0</version>
       </plugin>
       <plugin>
         <artifactId>maven-release-plugin</artifactId>
@@ -308,7 +308,7 @@
     <dependency>
       <groupId>org.junit.jupiter</groupId>
       <artifactId>junit-jupiter</artifactId>
-      <version>5.6.0</version>
+      <version>5.7.2</version>
       <scope>test</scope>
     </dependency>
 
diff --git a/src/main/java/org/jsoup/nodes/Attributes.java b/src/main/java/org/jsoup/nodes/Attributes.java
index b2dc18ef..6e0d7898 100644
--- a/src/main/java/org/jsoup/nodes/Attributes.java
+++ b/src/main/java/org/jsoup/nodes/Attributes.java
@@ -46,6 +46,7 @@ public class Attributes implements Iterable<Attribute>, Cloneable {
     static final int NotFound = -1;
     private static final String EmptyString = "";
 
+    // the number of instance fields is kept as low as possible giving an object size of 24 bytes
     private int size = 0; // number of slots used (not total capacity, which is keys.length)
     String[] keys = new String[InitialCapacity];
     String[] vals = new String[InitialCapacity];
@@ -246,16 +247,12 @@ public class Attributes implements Iterable<Attribute>, Cloneable {
     }
 
     /**
-     Get the number of attributes in this set.
+     Get the number of attributes in this set, including any jsoup internal-only attributes. Internal attributes are
+     excluded from the {@link #html()}, {@link #asList()}, and {@link #iterator()} methods.
      @return size
      */
     public int size() {
-        int s = 0;
-        for (int i = 0; i < size; i++) {
-            if (!isInternalKey(keys[i]))
-                s++;
-        }
-        return s;
+        return size;
     }
 
     /**
diff --git a/src/main/java/org/jsoup/parser/CharacterReader.java b/src/main/java/org/jsoup/parser/CharacterReader.java
index 35f52fa7..e2297494 100644
--- a/src/main/java/org/jsoup/parser/CharacterReader.java
+++ b/src/main/java/org/jsoup/parser/CharacterReader.java
@@ -555,7 +555,7 @@ public final class CharacterReader {
     }
 
     /**
-     * Caches short strings, as a flywheel pattern, to reduce GC load. Just for this doc, to prevent leaks.
+     * Caches short strings, as a flyweight pattern, to reduce GC load. Just for this doc, to prevent leaks.
      * <p />
      * Simplistic, and on hash collisions just falls back to creating a new string, vs a full HashMap with Entry list.
      * That saves both having to create objects as hash keys, and running through the entry list, at the expense of
@@ -569,27 +569,22 @@ public final class CharacterReader {
             return "";
 
         // calculate hash:
-        int hash = 31 * count;
-        int offset = start;
+        int hash = 0;
         for (int i = 0; i < count; i++) {
-            hash = 31 * hash + charBuf[offset++];
+            hash = 31 * hash + charBuf[start + i];
         }
 
         // get from cache
         final int index = hash & stringCacheSize - 1;
         String cached = stringCache[index];
 
-        if (cached == null) { // miss, add
+        if (cached != null && rangeEquals(charBuf, start, count, cached)) // positive hit
+            return cached;
+        else {
             cached = new String(charBuf, start, count);
-            stringCache[index] = cached;
-        } else { // hashcode hit, check equality
-            if (rangeEquals(charBuf, start, count, cached)) { // hit
-                return cached;
-            } else { // hashcode conflict
-                cached = new String(charBuf, start, count);
-                stringCache[index] = cached; // update the cache, as recently used strings are more likely to show up again
-            }
+            stringCache[index] = cached; // add or replace, assuming most recently used are most likely to recur next
         }
+
         return cached;
     }
 
diff --git a/src/main/java/org/jsoup/parser/Token.java b/src/main/java/org/jsoup/parser/Token.java
index 9ade8e0d..0b3cb6c5 100644
--- a/src/main/java/org/jsoup/parser/Token.java
+++ b/src/main/java/org/jsoup/parser/Token.java
@@ -97,11 +97,16 @@ abstract class Token {
             return this;
         }
 
+        /* Limits runaway crafted HTML from spewing attributes and getting a little sluggish in ensureCapacity.
+        Real-world HTML will P99 around 8 attributes, so plenty of headroom. Implemented here and not in the Attributes
+        object so that API users can add more if ever required. */
+        private static final int MaxAttributes = 512;
+
         final void newAttribute() {
             if (attributes == null)
                 attributes = new Attributes();
 
-            if (pendingAttributeName != null) {
+            if (pendingAttributeName != null && attributes.size() < MaxAttributes) {
                 // the tokeniser has skipped whitespace control chars, but trimming could collapse to empty for other control codes, so verify here
                 pendingAttributeName = pendingAttributeName.trim();
                 if (pendingAttributeName.length() > 0) {


Test patch:
diff --git a/src/test/java/org/jsoup/nodes/AttributesTest.java b/src/test/java/org/jsoup/nodes/AttributesTest.java
index 84d9ed78..d2f986b0 100644
--- a/src/test/java/org/jsoup/nodes/AttributesTest.java
+++ b/src/test/java/org/jsoup/nodes/AttributesTest.java
@@ -225,6 +225,10 @@ public class AttributesTest {
 
         a.put(Attributes.internalKey("baseUri"), "example.com");
         a.put(Attributes.internalKey("another"), "example.com");
-        assertEquals(2, a.size());
+        a.put(Attributes.internalKey("last"), "example.com");
+        a.remove(Attributes.internalKey("last"));
+
+        assertEquals(4, a.size());
+        assertEquals(2, a.asList().size()); // excluded from lists
     }
 }