View Javadoc
1   /*
2    * Copyright (c) 2002-2026 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit;
16  
17  import java.util.Map;
18  import java.util.concurrent.ConcurrentHashMap;
19  
20  import org.htmlunit.javascript.host.file.Blob;
21  
22  /**
23   * The user-agent-wide blob URL store defined by
24   * <a href="https://w3c.github.io/FileAPI/#BlobURLStore">Blob URL Store</a>.
25   *
26   * @author Lai Quang Duong
27   * @author Ronald Brill
28   */
29  public class BlobUrlStore {
30  
31      private static final class Entry {
32          private final Blob object_;
33          private final Page owningPage_;
34  
35          Entry(final Blob object, final Page owningPage) {
36              object_ = object;
37              owningPage_ = owningPage;
38          }
39      }
40  
41      private final Map<String, Entry> store_ = new ConcurrentHashMap<>();
42  
43      /**
44       * Adds an entry.
45       *
46       * <p>The {@code blobUrl} should not contain a fragment — fragments are
47       * stripped when resolving and removing entries but not on insertion.
48       * In practice this is always satisfied because blob URLs generated by
49       * {@code URL.createObjectURL()} never carry a fragment.
50       * </p>
51       *
52       * @param blobUrl the generated {@code blob:} URL; must not contain a fragment
53       * @param object the {@link Blob} (or {@code File}) the URL refers to
54       * @param owningPage the page that created the URL
55       */
56      public void put(final String blobUrl, final Blob object, final Page owningPage) {
57          store_.put(blobUrl, new Entry(object, owningPage));
58      }
59  
60      /**
61       * Resolves a blob URL to its stored {@link Blob} object per
62       * <a href="https://w3c.github.io/FileAPI/#blob-url-resolve">the Blob URL resolution algorithm</a>.
63       *
64       * <p>Note: this implementation resolves the URL regardless of which page or environment
65       * is requesting it, which is a deliberate simplification of the spec's origin checks.
66       * </p>
67       *
68       * <p>Any fragment component in {@code blobUrl} is ignored during lookup.</p>
69       *
70       * @param blobUrl the {@code blob:} URL to resolve
71       * @return the stored {@link Blob}, or {@code null} if no entry exists for this URL
72       */
73      public Blob resolve(final String blobUrl) {
74          final int fragmentIndex = blobUrl.indexOf('#');
75          final String url = fragmentIndex >= 0 ? blobUrl.substring(0, fragmentIndex) : blobUrl;
76  
77          final Entry entry = store_.get(url);
78          return entry == null ? null : entry.object_;
79      }
80  
81      /**
82       * Removes the entry for the given blob URL, revoking access to the associated {@link Blob}.
83       *
84       * @param blobUrl the {@code blob:} URL to revoke
85       */
86      public void remove(final String blobUrl) {
87          store_.remove(blobUrl);
88      }
89  
90      /**
91       * Removes all blob URL entries associated with the given page.
92       * Called when a page is unloaded to honour the
93       * <a href="https://w3c.github.io/FileAPI/#lifeTime">blob URL lifetime</a> rules.
94       *
95       * <p>Note: this operation is not atomic — entries added concurrently during
96       * page unload may survive. This is acceptable given HtmlUnit's single-threaded
97       * page lifecycle.
98       * </p>
99       *
100      * @param owningPage the page being unloaded
101      */
102     // package-private: called by the page lifecycle, not part of the public API
103     void removeForPage(final Page owningPage) {
104         store_.values().removeIf(entry -> entry.owningPage_ == owningPage);
105     }
106 
107     /**
108      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
109      *
110      * Removes all entries regardless of owning page.
111      * Prefer {@link #removeForPage(Page)} for normal page lifecycle handling.
112      */
113     public void clear() {
114         store_.clear();
115     }
116 }