001/* 002 * Copyright 2017-2022 Product Mog LLC, 2022-2026 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.lokalized; 018 019import org.jspecify.annotations.NonNull; 020import org.jspecify.annotations.Nullable; 021 022import javax.annotation.concurrent.Immutable; 023import javax.annotation.concurrent.NotThreadSafe; 024import java.util.Objects; 025 026import static com.lokalized.Diagnostics.format; 027import static java.util.Objects.requireNonNull; 028 029/** 030 * Options applied while discovering and loading localized strings files. 031 * <p> 032 * The byte limit applies to {@link java.nio.file.Path} and {@link java.io.InputStream} inputs. The UTF-16 code-unit 033 * limit applies to {@link java.io.Reader} inputs, whose original byte representation is not available to Lokalized. The 034 * localized strings file-count, translation-node, and warning limits apply to every load, including single-resource 035 * {@code parse(...)} operations. The discovery-entry limit separately bounds resources, filesystem children, JAR 036 * entries, and classpath roots examined before candidate resources are parsed; it does not apply to explicit 037 * single-resource parsing or explicit locale-to-resource maps, which enumerate no candidates. The aggregate byte 038 * limit also applies to path and input-stream parsing; it cannot apply to a {@link java.io.Reader} because the original 039 * byte representation is unavailable. 040 * The translation-node limit bounds parsed model structure; the byte and character limits separately bound the 041 * localized strings file text containing that structure. 042 * <p> 043 * Exhaustive classpath searching is disabled by default because it inspects every filesystem and JAR root visible to 044 * a classloader. Enable it only when localized strings are packaged in a JAR that omits directory entries and ordinary 045 * {@link ClassLoader#getResources(String)} discovery therefore cannot find the requested package. 046 * 047 * @author <a href="https://revetkn.com">Mark Allen</a> 048 * @since 3.0.0 049 */ 050@Immutable 051public final class LocalizedStringLoadingOptions { 052 /** Default maximum read from one localized strings resource: 8,388,608 bytes (8 MiB). */ 053 @NonNull 054 public static final Integer DEFAULT_MAXIMUM_INPUT_BYTES = 8 * 1024 * 1024; 055 /** Default maximum read from one {@link java.io.Reader}: 8,388,608 UTF-16 code units. */ 056 @NonNull 057 public static final Integer DEFAULT_MAXIMUM_READER_CHARACTERS = 8 * 1024 * 1024; 058 /** Default maximum JSON object/array nesting depth: 64. */ 059 @NonNull 060 public static final Integer DEFAULT_MAXIMUM_JSON_NESTING_DEPTH = 64; 061 /** Whether exhaustive classpath-root searching is enabled by default: false. */ 062 @NonNull 063 public static final Boolean DEFAULT_EXHAUSTIVE_CLASSPATH_SEARCH = false; 064 /** Highest configurable JSON nesting depth supported by the loader: 128. */ 065 @NonNull 066 public static final Integer MAXIMUM_JSON_NESTING_DEPTH = 128; 067 /** Default maximum total read by one load: 33,554,432 bytes (32 MiB). */ 068 @NonNull 069 public static final Long DEFAULT_MAXIMUM_TOTAL_INPUT_BYTES = 32L * 1024L * 1024L; 070 /** Default maximum number of localized strings files accepted by one load: 256. */ 071 @NonNull 072 public static final Integer DEFAULT_MAXIMUM_LOCALIZED_STRINGS_FILES = 256; 073 /** 074 * Default maximum translation nodes accepted by one load: 100,000. Translation nodes comprise root entries, 075 * whole-message alternatives, placeholder definitions, and expression-fragment alternatives. 076 */ 077 @NonNull 078 public static final Integer DEFAULT_MAXIMUM_TRANSLATION_NODES = 100_000; 079 /** Default maximum number of warnings emitted by one load: 1,000. */ 080 @NonNull 081 public static final Integer DEFAULT_MAXIMUM_WARNINGS = 1_000; 082 /** Default maximum number of discovery entries examined by one filesystem or classpath load: 100,000. */ 083 @NonNull 084 public static final Integer DEFAULT_MAXIMUM_DISCOVERY_ENTRIES = 100_000; 085 /** Highest configurable discovery-entry limit supported by the loader: 1,000,000. */ 086 @NonNull 087 public static final Integer MAXIMUM_DISCOVERY_ENTRIES = 1_000_000; 088 089 @NonNull 090 private static final LocalizedStringLoadingOptions DEFAULTS = new Builder().build(); 091 092 private final int maximumInputBytes; 093 private final int maximumReaderCharacters; 094 private final int maximumJsonNestingDepth; 095 private final boolean exhaustiveClasspathSearch; 096 private final long maximumTotalInputBytes; 097 private final int maximumLocalizedStringsFiles; 098 private final int maximumTranslationNodes; 099 private final int maximumWarnings; 100 private final int maximumDiscoveryEntries; 101 102 private LocalizedStringLoadingOptions( 103 int maximumInputBytes, 104 int maximumReaderCharacters, 105 int maximumJsonNestingDepth, 106 boolean exhaustiveClasspathSearch, 107 long maximumTotalInputBytes, 108 int maximumLocalizedStringsFiles, 109 int maximumTranslationNodes, 110 int maximumWarnings, 111 int maximumDiscoveryEntries) { 112 this.maximumInputBytes = maximumInputBytes; 113 this.maximumReaderCharacters = maximumReaderCharacters; 114 this.maximumJsonNestingDepth = maximumJsonNestingDepth; 115 this.exhaustiveClasspathSearch = exhaustiveClasspathSearch; 116 this.maximumTotalInputBytes = maximumTotalInputBytes; 117 this.maximumLocalizedStringsFiles = maximumLocalizedStringsFiles; 118 this.maximumTranslationNodes = maximumTranslationNodes; 119 this.maximumWarnings = maximumWarnings; 120 this.maximumDiscoveryEntries = maximumDiscoveryEntries; 121 } 122 123 /** 124 * Gets the default loading options. 125 * 126 * @return the default immutable options, not null 127 */ 128 @NonNull 129 public static LocalizedStringLoadingOptions defaults() { 130 return DEFAULTS; 131 } 132 133 /** 134 * Creates a builder initialized with the default limits. 135 * 136 * @return a new builder, not null 137 */ 138 @NonNull 139 public static Builder builder() { 140 return new Builder(); 141 } 142 143 /** 144 * Creates a builder initialized from this instance. 145 * 146 * @return a builder initialized from this instance, not null 147 */ 148 @NonNull 149 public Builder toBuilder() { 150 return builder() 151 .maximumInputBytes(maximumInputBytes) 152 .maximumReaderCharacters(maximumReaderCharacters) 153 .maximumJsonNestingDepth(maximumJsonNestingDepth) 154 .exhaustiveClasspathSearch(exhaustiveClasspathSearch) 155 .maximumTotalInputBytes(maximumTotalInputBytes) 156 .maximumLocalizedStringsFiles(maximumLocalizedStringsFiles) 157 .maximumTranslationNodes(maximumTranslationNodes) 158 .maximumWarnings(maximumWarnings) 159 .maximumDiscoveryEntries(maximumDiscoveryEntries); 160 } 161 162 /** 163 * Gets the maximum bytes accepted from a path or input stream. 164 * 165 * @return the maximum bytes accepted from a path or input stream 166 */ 167 @NonNull 168 public Integer getMaximumInputBytes() { 169 return maximumInputBytes; 170 } 171 172 /** 173 * Gets the maximum UTF-16 code units accepted from a reader. 174 * 175 * @return the maximum UTF-16 code units accepted from a reader 176 */ 177 @NonNull 178 public Integer getMaximumReaderCharacters() { 179 return maximumReaderCharacters; 180 } 181 182 /** 183 * Gets the maximum JSON object/array nesting depth. 184 * 185 * @return the maximum JSON object/array nesting depth 186 */ 187 @NonNull 188 public Integer getMaximumJsonNestingDepth() { 189 return maximumJsonNestingDepth; 190 } 191 192 /** 193 * Reports whether classpath loading should inspect every filesystem and JAR root visible to the classloader after 194 * ordinary package-resource discovery. 195 * 196 * @return true when exhaustive classpath-root searching is enabled 197 */ 198 @NonNull 199 public Boolean isExhaustiveClasspathSearchEnabled() { 200 return exhaustiveClasspathSearch; 201 } 202 203 /** 204 * Gets the maximum total bytes accepted by one filesystem, classpath, path, or input-stream load. 205 * This limit does not apply to reader input because its original byte representation is unavailable. 206 * 207 * @return the aggregate byte limit, not null 208 */ 209 @NonNull 210 public Long getMaximumTotalInputBytes() { 211 return maximumTotalInputBytes; 212 } 213 214 /** 215 * Gets the maximum localized strings files accepted by one load. 216 * 217 * @return the maximum localized strings files accepted by one load, not null 218 */ 219 @NonNull 220 public Integer getMaximumLocalizedStringsFiles() { 221 return maximumLocalizedStringsFiles; 222 } 223 224 /** 225 * Gets the maximum translation nodes accepted by one load. Translation nodes comprise root entries, 226 * whole-message alternatives, placeholder definitions, and expression-fragment alternatives. 227 * 228 * @return the aggregate translation-node limit, not null 229 */ 230 @NonNull 231 public Integer getMaximumTranslationNodes() { 232 return maximumTranslationNodes; 233 } 234 235 /** 236 * Gets the maximum warnings emitted by one load. 237 * 238 * @return the maximum warnings emitted by one load, not null 239 */ 240 @NonNull 241 public Integer getMaximumWarnings() { 242 return maximumWarnings; 243 } 244 245 /** 246 * Gets the maximum number of entries examined while discovering filesystem or classpath resources. Entries include 247 * direct filesystem children, package-resource locations, physical JAR entries, classpath-root candidates, and 248 * manifest {@code Class-Path} entries. This budget is independent of parsed-resource and input-size limits. 249 * 250 * @return the discovery-entry limit, not null 251 */ 252 @NonNull 253 public Integer getMaximumDiscoveryEntries() { 254 return maximumDiscoveryEntries; 255 } 256 257 @Override 258 public boolean equals(@Nullable Object object) { 259 if (this == object) 260 return true; 261 if (!(object instanceof LocalizedStringLoadingOptions)) 262 return false; 263 LocalizedStringLoadingOptions that = (LocalizedStringLoadingOptions) object; 264 return maximumInputBytes == that.maximumInputBytes 265 && maximumReaderCharacters == that.maximumReaderCharacters 266 && maximumJsonNestingDepth == that.maximumJsonNestingDepth 267 && exhaustiveClasspathSearch == that.exhaustiveClasspathSearch 268 && maximumTotalInputBytes == that.maximumTotalInputBytes 269 && maximumLocalizedStringsFiles == that.maximumLocalizedStringsFiles 270 && maximumTranslationNodes == that.maximumTranslationNodes 271 && maximumWarnings == that.maximumWarnings 272 && maximumDiscoveryEntries == that.maximumDiscoveryEntries; 273 } 274 275 @Override 276 public int hashCode() { 277 return Objects.hash(maximumInputBytes, maximumReaderCharacters, maximumJsonNestingDepth, 278 exhaustiveClasspathSearch, maximumTotalInputBytes, maximumLocalizedStringsFiles, maximumTranslationNodes, 279 maximumWarnings, maximumDiscoveryEntries); 280 } 281 282 @Override 283 @NonNull 284 public String toString() { 285 return format("%s{maximumInputBytes=%d, maximumReaderCharacters=%d, maximumJsonNestingDepth=%d, " + 286 "exhaustiveClasspathSearch=%s, maximumTotalInputBytes=%d, maximumLocalizedStringsFiles=%d, " + 287 "maximumTranslationNodes=%d, maximumWarnings=%d, maximumDiscoveryEntries=%d}", 288 getClass().getSimpleName(), maximumInputBytes, 289 maximumReaderCharacters, maximumJsonNestingDepth, exhaustiveClasspathSearch, maximumTotalInputBytes, 290 maximumLocalizedStringsFiles, maximumTranslationNodes, maximumWarnings, maximumDiscoveryEntries); 291 } 292 293 /** 294 * Builder for {@link LocalizedStringLoadingOptions}. 295 * 296 * @since 3.0.0 297 */ 298 @NotThreadSafe 299 public static final class Builder { 300 private int maximumInputBytes = DEFAULT_MAXIMUM_INPUT_BYTES; 301 private int maximumReaderCharacters = DEFAULT_MAXIMUM_READER_CHARACTERS; 302 private int maximumJsonNestingDepth = DEFAULT_MAXIMUM_JSON_NESTING_DEPTH; 303 private boolean exhaustiveClasspathSearch = DEFAULT_EXHAUSTIVE_CLASSPATH_SEARCH; 304 private long maximumTotalInputBytes = DEFAULT_MAXIMUM_TOTAL_INPUT_BYTES; 305 private int maximumLocalizedStringsFiles = DEFAULT_MAXIMUM_LOCALIZED_STRINGS_FILES; 306 private int maximumTranslationNodes = DEFAULT_MAXIMUM_TRANSLATION_NODES; 307 private int maximumWarnings = DEFAULT_MAXIMUM_WARNINGS; 308 private int maximumDiscoveryEntries = DEFAULT_MAXIMUM_DISCOVERY_ENTRIES; 309 310 private Builder() { 311 } 312 313 /** 314 * Sets the maximum bytes accepted from a path or input stream. 315 * 316 * @param maximumInputBytes positive byte limit, at most {@link Integer#MAX_VALUE} - 1 317 * @return this builder, not null 318 */ 319 @NonNull 320 public Builder maximumInputBytes(@NonNull Integer maximumInputBytes) { 321 requireNonNull(maximumInputBytes); 322 if (maximumInputBytes <= 0 || maximumInputBytes == Integer.MAX_VALUE) 323 throw new IllegalArgumentException("maximumInputBytes must be between 1 and Integer.MAX_VALUE - 1"); 324 325 this.maximumInputBytes = maximumInputBytes; 326 return this; 327 } 328 329 /** 330 * Sets the maximum UTF-16 code units accepted from a reader. 331 * 332 * @param maximumReaderCharacters positive UTF-16 code-unit limit 333 * @return this builder, not null 334 */ 335 @NonNull 336 public Builder maximumReaderCharacters(@NonNull Integer maximumReaderCharacters) { 337 requireNonNull(maximumReaderCharacters); 338 if (maximumReaderCharacters <= 0) 339 throw new IllegalArgumentException("maximumReaderCharacters must be positive"); 340 341 this.maximumReaderCharacters = maximumReaderCharacters; 342 return this; 343 } 344 345 /** 346 * Sets the maximum JSON object/array nesting depth. 347 * 348 * @param maximumJsonNestingDepth nesting limit from 1 through {@link #MAXIMUM_JSON_NESTING_DEPTH} 349 * @return this builder, not null 350 */ 351 @NonNull 352 public Builder maximumJsonNestingDepth(@NonNull Integer maximumJsonNestingDepth) { 353 requireNonNull(maximumJsonNestingDepth); 354 if (maximumJsonNestingDepth <= 0 || maximumJsonNestingDepth > MAXIMUM_JSON_NESTING_DEPTH) 355 throw new IllegalArgumentException("maximumJsonNestingDepth must be between 1 and " + 356 MAXIMUM_JSON_NESTING_DEPTH); 357 358 this.maximumJsonNestingDepth = maximumJsonNestingDepth; 359 return this; 360 } 361 362 /** 363 * Controls whether classpath loading should inspect every filesystem and JAR root visible to the classloader when 364 * ordinary package-resource discovery is insufficient. This is disabled by default to avoid sweeping unrelated 365 * dependencies. It is primarily useful for JARs that omit directory entries. 366 * 367 * @param exhaustiveClasspathSearch true to enable exhaustive classpath-root searching 368 * @return this builder, not null 369 */ 370 @NonNull 371 public Builder exhaustiveClasspathSearch(@NonNull Boolean exhaustiveClasspathSearch) { 372 requireNonNull(exhaustiveClasspathSearch); 373 this.exhaustiveClasspathSearch = exhaustiveClasspathSearch; 374 return this; 375 } 376 377 /** 378 * Sets the maximum total bytes accepted by one filesystem, classpath, path, or input-stream load. 379 * This limit does not apply to reader input because its original byte representation is unavailable. 380 * 381 * @param maximumTotalInputBytes positive aggregate byte limit, not null 382 * @return this builder, not null 383 */ 384 @NonNull 385 public Builder maximumTotalInputBytes(@NonNull Long maximumTotalInputBytes) { 386 if (requireNonNull(maximumTotalInputBytes) <= 0) 387 throw new IllegalArgumentException("maximumTotalInputBytes must be positive"); 388 389 this.maximumTotalInputBytes = maximumTotalInputBytes; 390 return this; 391 } 392 393 /** 394 * Sets the maximum localized strings files accepted by one load. 395 * 396 * @param maximumLocalizedStringsFiles positive localized strings file limit, not null 397 * @return this builder, not null 398 */ 399 @NonNull 400 public Builder maximumLocalizedStringsFiles(@NonNull Integer maximumLocalizedStringsFiles) { 401 if (requireNonNull(maximumLocalizedStringsFiles) <= 0) 402 throw new IllegalArgumentException("maximumLocalizedStringsFiles must be positive"); 403 404 this.maximumLocalizedStringsFiles = maximumLocalizedStringsFiles; 405 return this; 406 } 407 408 /** 409 * Sets the maximum translation nodes accepted by one load. Translation nodes comprise root entries, 410 * whole-message alternatives, placeholder definitions, and expression-fragment alternatives. 411 * 412 * @param maximumTranslationNodes nonnegative translation-node limit, not null 413 * @return this builder, not null 414 */ 415 @NonNull 416 public Builder maximumTranslationNodes(@NonNull Integer maximumTranslationNodes) { 417 if (requireNonNull(maximumTranslationNodes) < 0) 418 throw new IllegalArgumentException("maximumTranslationNodes must be nonnegative"); 419 420 this.maximumTranslationNodes = maximumTranslationNodes; 421 return this; 422 } 423 424 /** 425 * Sets the maximum warnings emitted by one load. 426 * 427 * @param maximumWarnings nonnegative warning limit, not null 428 * @return this builder, not null 429 */ 430 @NonNull 431 public Builder maximumWarnings(@NonNull Integer maximumWarnings) { 432 if (requireNonNull(maximumWarnings) < 0) 433 throw new IllegalArgumentException("maximumWarnings must be nonnegative"); 434 435 this.maximumWarnings = maximumWarnings; 436 return this; 437 } 438 439 /** 440 * Sets the maximum number of entries examined while discovering filesystem or classpath resources. This budget is 441 * charged before filename and package filtering so irrelevant entries cannot cause unbounded discovery work. 442 * 443 * @param maximumDiscoveryEntries positive discovery-entry limit, at most 444 * {@link #MAXIMUM_DISCOVERY_ENTRIES} 445 * @return this builder, not null 446 */ 447 @NonNull 448 public Builder maximumDiscoveryEntries(@NonNull Integer maximumDiscoveryEntries) { 449 requireNonNull(maximumDiscoveryEntries); 450 if (maximumDiscoveryEntries <= 0 || maximumDiscoveryEntries > MAXIMUM_DISCOVERY_ENTRIES) 451 throw new IllegalArgumentException("maximumDiscoveryEntries must be between 1 and " + 452 MAXIMUM_DISCOVERY_ENTRIES); 453 454 this.maximumDiscoveryEntries = maximumDiscoveryEntries; 455 return this; 456 } 457 458 /** 459 * Builds immutable loading options. 460 * 461 * @return immutable loading options, not null 462 */ 463 @NonNull 464 public LocalizedStringLoadingOptions build() { 465 return new LocalizedStringLoadingOptions(maximumInputBytes, maximumReaderCharacters, 466 maximumJsonNestingDepth, exhaustiveClasspathSearch, maximumTotalInputBytes, 467 maximumLocalizedStringsFiles, 468 maximumTranslationNodes, maximumWarnings, maximumDiscoveryEntries); 469 } 470 } 471}