1 /*
  2  * Copyright (c) 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 
 26 package jdk.internal.jimage;
 27 
 28 import java.nio.IntBuffer;
 29 import java.util.Comparator;
 30 import java.util.Iterator;
 31 import java.util.List;
 32 import java.util.NoSuchElementException;
 33 import java.util.Objects;
 34 import java.util.function.Function;
 35 
 36 /**
 37  * Represents the module entries stored in the buffer of {@code "/packages/xxx"}
 38  * image locations (package subdirectories). These entries use flags which are
 39  * similar to, but distinct from, the {@link ImageLocation} flags, so
 40  * encapsulating them here helps avoid confusion.
 41  *
 42  * @implNote This class needs to maintain JDK 8 source compatibility.
 43  *
 44  * It is used internally in the JDK to implement jimage/jrtfs access,
 45  * but also compiled and delivered as part of the jrtfs.jar to support access
 46  * to the jimage file provided by the shipped JDK by tools running on JDK 8.
 47  */
 48 public final class ModuleReference implements Comparable<ModuleReference> {
 49     // These flags are additive (hence "has-content" rather than "is-empty").
 50 
 51     /** If set, this package exists in preview mode. */
 52     private static final int FLAGS_PKG_HAS_PREVIEW_VERSION = 0x1;
 53     /** If set, this package exists in non-preview mode. */
 54     private static final int FLAGS_PKG_HAS_NORMAL_VERSION = 0x2;
 55     /** If set, the associated module has resources (in normal or preview mode). */
 56     private static final int FLAGS_PKG_HAS_RESOURCES = 0x4;
 57 
 58     /**
 59      * References are ordered with preview versions first which permits early
 60      * exit when processing preview entries (it's reversed because the default
 61      * order for a boolean is {@code false < true}).
 62      */
 63     private static final Comparator<ModuleReference> PREVIEW_FIRST =
 64             Comparator.comparing(ModuleReference::hasPreviewVersion).reversed()
 65                     .thenComparing(ModuleReference::name);
 66 
 67     /**
 68      * Returns a reference for non-empty packages (those with resources) in a
 69      * given module.
 70      *
 71      * <p>The same reference can be used for multiple packages in the same module.
 72      */
 73     public static ModuleReference forPackage(String moduleName, boolean isPreview) {
 74         return new ModuleReference(moduleName, FLAGS_PKG_HAS_RESOURCES | previewFlag(isPreview));
 75     }
 76 
 77     /**
 78      * Returns a reference for empty packages in a given module.
 79      *
 80      * <p>The same reference can be used for multiple packages in the same module.
 81      */
 82     public static ModuleReference forEmptyPackage(String moduleName, boolean isPreview) {
 83         return new ModuleReference(moduleName, previewFlag(isPreview));
 84     }
 85 
 86     private static int previewFlag(boolean isPreview) {
 87         return isPreview ? FLAGS_PKG_HAS_PREVIEW_VERSION : FLAGS_PKG_HAS_NORMAL_VERSION;
 88     }
 89 
 90     /** Merges two references for the same module (combining their flags). */
 91     public ModuleReference merge(ModuleReference other) {
 92         if (!name.equals(other.name)) {
 93             throw new IllegalArgumentException("Cannot merge " + other + " with " + this);
 94         }
 95         // Because flags are additive, we can just OR them here.
 96         return new ModuleReference(name, flags | other.flags);
 97     }
 98 
 99     private final String name;
100     private final int flags;
101 
102     private ModuleReference(String moduleName, int flags) {
103         this.name = Objects.requireNonNull(moduleName);
104         this.flags = flags;
105     }
106 
107     /** Returns the module name of this reference. */
108     public String name() {
109         return name;
110     }
111 
112     /**
113      * Returns whether the package associated with this reference contains
114      * resources in this reference's module.
115      *
116      * <p>An invariant of the module system is that while a package may exist
117      * under many modules, it only has resources in one.
118      */
119     public boolean hasResources() {
120         return (flags & FLAGS_PKG_HAS_RESOURCES) != 0;
121     }
122 
123     /**
124      * Returns whether the package associated with this reference has a preview
125      * version (empty or otherwise) in this reference's module.
126      */
127     public boolean hasPreviewVersion() {
128         return (flags & FLAGS_PKG_HAS_PREVIEW_VERSION) != 0;
129     }
130 
131     /** Returns whether this reference exists only in preview mode. */
132     public boolean isPreviewOnly() {
133         return (flags & FLAGS_PKG_HAS_NORMAL_VERSION) == 0;
134     }
135 
136     @Override
137     public int compareTo(ModuleReference rhs) {
138         return PREVIEW_FIRST.compare(this, rhs);
139     }
140 
141     @Override
142     public String toString() {
143         return "ModuleReference{ module=" + name + ", flags=" + flags + " }";
144     }
145 
146     @Override
147     public boolean equals(Object obj) {
148         if (!(obj instanceof ModuleReference)) {
149             return false;
150         }
151         ModuleReference other = (ModuleReference) obj;
152         return name.equals(other.name) && flags == other.flags;
153     }
154 
155     @Override
156     public int hashCode() {
157         return Objects.hash(name, flags);
158     }
159 
160     /**
161      * Reads the content buffer of a package subdirectory to return a sequence
162      * of module name offsets in the jimage.
163      *
164      * @param buffer the content buffer of an {@link ImageLocation} with type
165      *     {@link ImageLocation.LocationType#PACKAGES_DIR PACKAGES_DIR}.
166      * @param includeNormal whether to include name offsets for modules present
167      *     in normal (non-preview) mode.
168      * @param includePreview whether to include name offsets for modules present
169      *     in preview mode.
170      * @return an iterator of module name offsets.
171      */
172     public static Iterator<Integer> readNameOffsets(
173             IntBuffer buffer, boolean includeNormal, boolean includePreview) {
174         int bufferSize = buffer.capacity();
175         if (bufferSize == 0 || (bufferSize & 0x1) != 0) {
176             throw new IllegalArgumentException("Invalid buffer size");
177         }
178         int includeMask = (includeNormal ? FLAGS_PKG_HAS_NORMAL_VERSION : 0)
179                 + (includePreview ? FLAGS_PKG_HAS_PREVIEW_VERSION : 0);
180         if (includeMask == 0) {
181             throw new IllegalArgumentException("Invalid flags");
182         }
183 
184         return new Iterator<Integer>() {
185             private int idx = nextIdx(0);
186 
187             int nextIdx(int idx) {
188                 for (; idx < bufferSize; idx += 2) {
189                     // If any of the test flags are set, include this entry.
190                     if ((buffer.get(idx) & includeMask) != 0) {
191                         return idx;
192                     } else if (!includeNormal) {
193                         // Preview entries are first in the offset buffer, so we
194                         // can exit early (by returning the end index) if we are
195                         // only iterating preview entries, and have run out.
196                         break;
197                     }
198                 }
199                 return bufferSize;
200             }
201 
202             @Override
203             public boolean hasNext() {
204                 return idx < bufferSize;
205             }
206 
207             @Override
208             public Integer next() {
209                 if (idx < bufferSize) {
210                     int nameOffset = buffer.get(idx + 1);
211                     idx = nextIdx(idx + 2);
212                     return nameOffset;
213                 }
214                 throw new NoSuchElementException();
215             }
216         };
217     }
218 
219     /**
220      * Writes a list of module references to a given buffer. The given references
221      * list is checked carefully to ensure the written buffer will be valid.
222      *
223      * <p>Entries are written in order, taking two integer slots per entry as
224      * {@code [<flags>, <encoded-name>]}.
225      *
226      * @param refs the references to write, correctly ordered.
227      * @param buffer destination buffer.
228      * @param nameEncoder encoder for module names.
229      * @throws IllegalArgumentException in the references are invalid in any way.
230      */
231     public static void write(
232             List<ModuleReference> refs, IntBuffer buffer, Function<String, Integer> nameEncoder) {
233         if (refs.isEmpty()) {
234             throw new IllegalArgumentException("References list must be non-empty");
235         }
236         int expectedCapacity = 2 * refs.size();
237         if (buffer.capacity() != expectedCapacity) {
238             throw new IllegalArgumentException(
239                     "Invalid buffer capacity: expected " + expectedCapacity + ", got " + buffer.capacity());
240         }
241         // This catches exact duplicates in the list.
242         refs.stream().reduce((lhs, rhs) -> {
243             if (lhs.compareTo(rhs) >= 0) {
244                 throw new IllegalArgumentException("References must be strictly ordered: " + refs);
245             }
246             return rhs;
247         });
248         // Distinct references can have the same name (but we don't allow this).
249         if (refs.stream().map(ModuleReference::name).distinct().count() != refs.size()) {
250             throw new IllegalArgumentException("Reference names must be unique: " + refs);
251         }
252         if (refs.stream().filter(ModuleReference::hasResources).count() > 1) {
253             throw new IllegalArgumentException("At most one reference can have resources: " + refs);
254         }
255         for (ModuleReference modRef : refs) {
256             buffer.put(modRef.flags);
257             buffer.put(nameEncoder.apply(modRef.name));
258         }
259     }
260 }