1 /*
  2  * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.  Oracle designates this
  8  * particular file as subject to the "Classpath" exception as provided
  9  * by Oracle in the LICENSE file that accompanied this code.
 10  *
 11  * This code is distributed in the hope that it will be useful, but WITHOUT
 12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 14  * version 2 for more details (a copy is included in the LICENSE file that
 15  * accompanied this code).
 16  *
 17  * You should have received a copy of the GNU General Public License version
 18  * 2 along with this work; if not, write to the Free Software Foundation,
 19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 20  *
 21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 22  * or visit www.oracle.com if you need additional information or have any
 23  * questions.
 24  */
 25 package jdk.tools.jlink.internal;
 26 
 27 import java.lang.module.ModuleDescriptor;
 28 import java.nio.ByteBuffer;
 29 import java.nio.ByteOrder;
 30 import java.util.HashSet;
 31 import java.util.LinkedHashMap;
 32 import java.util.Map;
 33 import java.util.Objects;
 34 import java.util.Optional;
 35 import java.util.Set;
 36 import java.util.stream.Stream;
 37 import jdk.internal.jimage.decompressor.CompressedResourceHeader;
 38 import jdk.internal.module.Checks;
 39 import jdk.internal.module.ModuleInfo;
 40 import jdk.internal.module.ModuleInfo.Attributes;
 41 import jdk.internal.module.ModuleTarget;
 42 import jdk.tools.jlink.plugin.ResourcePool;
 43 import jdk.tools.jlink.plugin.ResourcePoolBuilder;
 44 import jdk.tools.jlink.plugin.ResourcePoolEntry;
 45 import jdk.tools.jlink.plugin.ResourcePoolModule;
 46 import jdk.tools.jlink.plugin.ResourcePoolModuleView;
 47 import jdk.tools.jlink.plugin.PluginException;
 48 
 49 /**
 50  * A manager for pool of resources.
 51  */
 52 public class ResourcePoolManager {
 53     // utility to read Module Attributes of the given ResourcePoolModule
 54     static Attributes readModuleAttributes(ResourcePoolModule mod) {
 55         String p = "/" + mod.name() + "/module-info.class";
 56         Optional<ResourcePoolEntry> content = mod.findEntry(p);
 57         if (content.isEmpty()) {
 58               throw new PluginException("module-info.class not found for " +
 59                   mod.name() + " module");
 60         }
 61         ByteBuffer bb = ByteBuffer.wrap(content.get().contentBytes());
 62         try {
 63             return ModuleInfo.read(bb, null);
 64         } catch (RuntimeException re) {
 65             throw new RuntimeException("module info cannot be read for " + mod.name(), re);
 66         }
 67     }
 68 
 69     static class ResourcePoolModuleImpl implements ResourcePoolModule {
 70         private static final String PREVIEW_PREFIX = "META-INF/preview/";
 71 
 72         final Map<String, ResourcePoolEntry> moduleContent = new LinkedHashMap<>();
 73         // lazily initialized
 74         private ModuleDescriptor descriptor;
 75         private ModuleTarget target;
 76 
 77         final String name;
 78 
 79         private ResourcePoolModuleImpl(String name) {
 80             this.name = name;
 81         }
 82 
 83         @Override
 84         public String name() {
 85             return name;
 86         }
 87 
 88         @Override
 89         public Optional<ResourcePoolEntry> findEntry(String path) {
 90             if (!path.startsWith("/")) {
 91                 path = "/" + path;
 92             }
 93             if (!path.startsWith("/" + name + "/")) {
 94                 path = "/" + name + path; // path already starts with '/'
 95             }
 96             return Optional.ofNullable(moduleContent.get(path));
 97         }
 98 
 99         @Override
100         public ModuleDescriptor descriptor() {
101             initModuleAttributes();
102             return descriptor;
103         }
104 
105         @Override
106         public String targetPlatform() {
107             initModuleAttributes();
108             return target != null? target.targetPlatform() : null;
109         }
110 
111         private void initModuleAttributes() {
112             if (this.descriptor == null) {
113                 Attributes attr = readModuleAttributes(this);
114                 this.descriptor = attr.descriptor();
115                 this.target = attr.target();
116             }
117         }
118 
119         @Override
120         public Set<String> packages() {
121             Set<String> pkgs = new HashSet<>();
122             moduleContent.values().stream()
123                     .filter(m -> m.type() == ResourcePoolEntry.Type.CLASS_OR_RESOURCE)
124                     .forEach(res -> inferPackageName(res).ifPresent(pkgs::add));
125             return pkgs;
126         }
127 
128         @Override
129         public String toString() {
130             return name();
131         }
132 
133         @Override
134         public Stream<ResourcePoolEntry> entries() {
135             return moduleContent.values().stream();
136         }
137 
138         @Override
139         public int entryCount() {
140             return moduleContent.values().size();
141         }
142 
143         /**
144          * Returns a valid non-empty package name, inferred from a resource pool
145          * entry's path.
146          *
147          * <p>If the resource pool entry is for a preview resource (i.e. with
148          * path {@code "/mod-name/META-INF/preview/pkg-path/resource-name"})
149          * the package name is the non-preview name based on {@code "pkg-path"}.
150          *
151          * @return the inferred package name, or {@link Optional#empty() empty}
152          *     if no name could be inferred.
153          */
154         private static Optional<String> inferPackageName(ResourcePoolEntry res) {
155             // Expect entry paths to be "/mod-name/pkg-path/resource-name", but
156             // may also get "/mod-name/META-INF/preview/pkg-path/resource-name"
157             String name = res.path();
158             if (name.charAt(0) == '/') {
159                 int pkgStart = name.indexOf('/', 1) + 1;
160                 int pkgEnd = name.lastIndexOf('/');
161                 if (pkgStart > 0 && pkgEnd > pkgStart) {
162                     String pkgPath = name.substring(pkgStart, pkgEnd);
163                     // Handle preview paths by removing the prefix.
164                     if (pkgPath.startsWith(PREVIEW_PREFIX)) {
165                         pkgPath = pkgPath.substring(PREVIEW_PREFIX.length());
166                     }
167                     String pkgName = pkgPath.replace('/', '.');
168                     if (Checks.isPackageName(pkgName)) {
169                         return Optional.of(pkgName);
170                     }
171                 }
172             }
173             return Optional.empty();
174         }
175     }
176 
177     public class ResourcePoolImpl implements ResourcePool {
178         @Override
179         public ResourcePoolModuleView moduleView() {
180             return ResourcePoolManager.this.moduleView();
181         }
182 
183         @Override
184         public Stream<ResourcePoolEntry> entries() {
185             return ResourcePoolManager.this.entries();
186         }
187 
188         @Override
189         public int entryCount() {
190             return ResourcePoolManager.this.entryCount();
191         }
192 
193         @Override
194         public Optional<ResourcePoolEntry> findEntry(String path) {
195             return ResourcePoolManager.this.findEntry(path);
196         }
197 
198         @Override
199         public Optional<ResourcePoolEntry> findEntryInContext(String path, ResourcePoolEntry context) {
200             return ResourcePoolManager.this.findEntryInContext(path, context);
201         }
202 
203         @Override
204         public boolean contains(ResourcePoolEntry data) {
205             return ResourcePoolManager.this.contains(data);
206         }
207 
208         @Override
209         public boolean isEmpty() {
210             return ResourcePoolManager.this.isEmpty();
211         }
212 
213         @Override
214         public ByteOrder byteOrder() {
215             return ResourcePoolManager.this.byteOrder();
216         }
217 
218         public StringTable getStringTable() {
219             return ResourcePoolManager.this.getStringTable();
220         }
221     }
222 
223     class ResourcePoolBuilderImpl implements ResourcePoolBuilder {
224         private boolean built;
225 
226         @Override
227         public void add(ResourcePoolEntry data) {
228             if (built) {
229                 throw new IllegalStateException("resource pool already built!");
230             }
231             ResourcePoolManager.this.add(data);
232         }
233 
234         @Override
235         public ResourcePool build() {
236             built = true;
237             return ResourcePoolManager.this.resourcePool();
238         }
239     }
240 
241     class ResourcePoolModuleViewImpl implements ResourcePoolModuleView {
242         @Override
243         public Optional<ResourcePoolModule> findModule(String name) {
244             return ResourcePoolManager.this.findModule(name);
245         }
246 
247         @Override
248         public Stream<ResourcePoolModule> modules() {
249             return ResourcePoolManager.this.modules();
250         }
251 
252         @Override
253         public int moduleCount() {
254             return ResourcePoolManager.this.moduleCount();
255         }
256     }
257 
258     private final Map<String, ResourcePoolEntry> resources = new LinkedHashMap<>();
259     private final Map<String, ResourcePoolModule> modules = new LinkedHashMap<>();
260     private final ByteOrder order;
261     private final StringTable table;
262     private final ResourcePool poolImpl;
263     private final ResourcePoolBuilder poolBuilderImpl;
264     private final ResourcePoolModuleView moduleViewImpl;
265 
266     public ResourcePoolManager() {
267         this(ByteOrder.nativeOrder());
268     }
269 
270     public ResourcePoolManager(ByteOrder order) {
271         this(order, new StringTable() {
272 
273             @Override
274             public int addString(String str) {
275                 return -1;
276             }
277 
278             @Override
279             public String getString(int id) {
280                 return null;
281             }
282         });
283     }
284 
285     public ResourcePoolManager(ByteOrder order, StringTable table) {
286         this.order = Objects.requireNonNull(order);
287         this.table = Objects.requireNonNull(table);
288         this.poolImpl = new ResourcePoolImpl();
289         this.poolBuilderImpl = new ResourcePoolBuilderImpl();
290         this.moduleViewImpl = new ResourcePoolModuleViewImpl();
291     }
292 
293     public ResourcePool resourcePool() {
294         return poolImpl;
295     }
296 
297     public ResourcePoolBuilder resourcePoolBuilder() {
298         return poolBuilderImpl;
299     }
300 
301     public ResourcePoolModuleView moduleView() {
302         return moduleViewImpl;
303     }
304 
305     /**
306      * Add a ResourcePoolEntry.
307      *
308      * @param data The ResourcePoolEntry to add.
309      */
310     public void add(ResourcePoolEntry data) {
311         Objects.requireNonNull(data);
312         if (resources.get(data.path()) != null) {
313             throw new PluginException("Resource " + data.path()
314                     + " already present");
315         }
316         String modulename = data.moduleName();
317         ResourcePoolModuleImpl m = (ResourcePoolModuleImpl)modules.get(modulename);
318         if (m == null) {
319             m = new ResourcePoolModuleImpl(modulename);
320             modules.put(modulename, m);
321         }
322         resources.put(data.path(), data);
323         m.moduleContent.put(data.path(), data);
324     }
325 
326     /**
327      * Retrieves the module for the provided name.
328      *
329      * @param name The module name
330      * @return the module of matching name, if found
331      */
332     public Optional<ResourcePoolModule> findModule(String name) {
333         Objects.requireNonNull(name);
334         return Optional.ofNullable(modules.get(name));
335     }
336 
337     /**
338      * The stream of modules contained in this ResourcePool.
339      *
340      * @return The stream of modules.
341      */
342     public Stream<ResourcePoolModule> modules() {
343         return modules.values().stream();
344     }
345 
346     /**
347      * Return the number of ResourcePoolModule count in this ResourcePool.
348      *
349      * @return the module count.
350      */
351     public int moduleCount() {
352         return modules.size();
353     }
354 
355     /**
356      * Get all ResourcePoolEntry contained in this ResourcePool instance.
357      *
358      * @return The stream of ResourcePoolModuleEntries.
359      */
360     public Stream<ResourcePoolEntry> entries() {
361         return resources.values().stream();
362     }
363 
364     /**
365      * Return the number of ResourcePoolEntry count in this ResourcePool.
366      *
367      * @return the entry count.
368      */
369     public int entryCount() {
370         return resources.values().size();
371     }
372 
373     /**
374      * Get the ResourcePoolEntry for the passed path.
375      *
376      * @param path A data path
377      * @return A ResourcePoolEntry instance or null if the data is not found
378      */
379     public Optional<ResourcePoolEntry> findEntry(String path) {
380         Objects.requireNonNull(path);
381         return Optional.ofNullable(resources.get(path));
382     }
383 
384     /**
385      * Get the ResourcePoolEntry for the passed path restricted to supplied context.
386      *
387      * @param path A data path
388      * @param context A context of the search
389      * @return A ResourcePoolEntry instance or null if the data is not found
390      */
391     public Optional<ResourcePoolEntry> findEntryInContext(String path, ResourcePoolEntry context) {
392         Objects.requireNonNull(path);
393         Objects.requireNonNull(context);
394         ResourcePoolModule module = modules.get(context.moduleName());
395         Objects.requireNonNull(module);
396         Optional<ResourcePoolEntry> entry = module.findEntry(path);
397         // Navigating other modules via requires and exports is problematic
398         // since we cannot construct the runtime model of loaders and layers.
399         return entry;
400      }
401 
402     /**
403      * Check if the ResourcePool contains the given ResourcePoolEntry.
404      *
405      * @param data The module data to check existence for.
406      * @return The module data or null if not found.
407      */
408     public boolean contains(ResourcePoolEntry data) {
409         Objects.requireNonNull(data);
410         return findEntry(data.path()).isPresent();
411     }
412 
413     /**
414      * Check if the ResourcePool contains some content at all.
415      *
416      * @return True, no content, false otherwise.
417      */
418     public boolean isEmpty() {
419         return resources.isEmpty();
420     }
421 
422     /**
423      * The ByteOrder currently in use when generating the jimage file.
424      *
425      * @return The ByteOrder.
426      */
427     public ByteOrder byteOrder() {
428         return order;
429     }
430 
431     public StringTable getStringTable() {
432         return table;
433     }
434 
435     /**
436      * A resource that has been compressed.
437      */
438     public static final class CompressedModuleData extends ByteArrayResourcePoolEntry {
439 
440         final long uncompressed_size;
441 
442         private CompressedModuleData(String module, String path,
443                 byte[] content, long uncompressed_size) {
444             super(module, path, ResourcePoolEntry.Type.CLASS_OR_RESOURCE, content);
445             this.uncompressed_size = uncompressed_size;
446         }
447 
448         public long getUncompressedSize() {
449             return uncompressed_size;
450         }
451 
452         @Override
453         public boolean equals(Object other) {
454             if (!(other instanceof CompressedModuleData)) {
455                 return false;
456             }
457             CompressedModuleData f = (CompressedModuleData) other;
458             return f.path().equals(path());
459         }
460 
461         @Override
462         public int hashCode() {
463             return super.hashCode();
464         }
465     }
466 
467     public static CompressedModuleData newCompressedResource(ResourcePoolEntry original,
468             ByteBuffer compressed,
469             String plugin,
470             StringTable strings,
471             ByteOrder order) {
472         Objects.requireNonNull(original);
473         Objects.requireNonNull(compressed);
474         Objects.requireNonNull(plugin);
475 
476         boolean isTerminal = !(original instanceof CompressedModuleData);
477         long uncompressed_size = original.contentLength();
478         if (original instanceof CompressedModuleData) {
479             CompressedModuleData comp = (CompressedModuleData) original;
480             uncompressed_size = comp.getUncompressedSize();
481         }
482         int nameOffset = strings.addString(plugin);
483         CompressedResourceHeader rh
484                 = new CompressedResourceHeader(compressed.limit(), original.contentLength(),
485                         nameOffset, isTerminal);
486         // Merge header with content;
487         byte[] h = rh.getBytes(order);
488         ByteBuffer bb = ByteBuffer.allocate(compressed.limit() + h.length);
489         bb.order(order);
490         bb.put(h);
491         bb.put(compressed);
492         byte[] contentWithHeader = bb.array();
493 
494         CompressedModuleData compressedResource
495                 = new CompressedModuleData(original.moduleName(), original.path(),
496                         contentWithHeader, uncompressed_size);
497         return compressedResource;
498     }
499 }