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 }