@@ -113,6 +113,136 @@ public enum FileSystem {
113113 }, true , true , '\\' , NameLengthStrategy .UTF16_CODE_UNITS );
114114 // @formatter:on
115115
116+ /**
117+ * Strategy for measuring and truncating file or path names in different units.
118+ * Implementations measure length and can truncate to a specified limit.
119+ */
120+ enum NameLengthStrategy {
121+ /** Length measured as encoded bytes. */
122+ BYTES {
123+ @ Override
124+ int getLength (final CharSequence value , final Charset charset ) {
125+ final CharsetEncoder enc = charset .newEncoder ()
126+ .onMalformedInput (CodingErrorAction .REPORT )
127+ .onUnmappableCharacter (CodingErrorAction .REPORT );
128+ try {
129+ return enc .encode (CharBuffer .wrap (value )).remaining ();
130+ } catch (CharacterCodingException e ) {
131+ // Unencodable, does not fit any byte limit.
132+ return Integer .MAX_VALUE ;
133+ }
134+ }
135+
136+ @ Override
137+ CharSequence truncate (final CharSequence value , final int limit , final Charset charset ) {
138+ final CharsetEncoder encoder = charset .newEncoder ()
139+ .onMalformedInput (CodingErrorAction .REPORT )
140+ .onUnmappableCharacter (CodingErrorAction .REPORT );
141+
142+ if (!encoder .canEncode (value )) {
143+ throw new IllegalArgumentException (
144+ "The value " + value + " cannot be encoded using " + charset .name ());
145+ }
146+
147+ // Fast path: if even the worst-case expansion fits, we're done.
148+ if (value .length () <= Math .floor (limit / encoder .maxBytesPerChar ())) {
149+ return value ;
150+ }
151+
152+ // Slow path: encode into a fixed-size byte buffer.
153+ // 1. Compute length of extension in bytes (if any).
154+ final CharSequence [] parts = splitExtension (value );
155+ final int extensionLength = getLength (parts [1 ], charset );
156+ if (extensionLength > 0 && extensionLength >= limit ) {
157+ // Extension itself does not fit
158+ throw new IllegalArgumentException (
159+ "The extension of " + value + " is too long to fit within " + limit + " bytes" );
160+ }
161+
162+ // 2. Compute the character part that fits within the remaining byte budget.
163+ final ByteBuffer byteBuffer = ByteBuffer .allocate (limit - extensionLength );
164+ final CharBuffer charBuffer = CharBuffer .wrap (parts [0 ]);
165+
166+ // Encode until the first character that would exceed the byte budget.
167+ final CoderResult cr = encoder .encode (charBuffer , byteBuffer , true );
168+
169+ if (cr .isUnderflow ()) {
170+ // Entire candidate fit within maxFileNameLength bytes.
171+ return value ;
172+ }
173+
174+ final CharSequence truncated = safeTruncate (value , charBuffer .position ());
175+ return extensionLength == 0 ? truncated : truncated .toString () + parts [1 ];
176+ }
177+ },
178+
179+ /** Length measured as UTF-16 code units (i.e., {@code CharSequence.length()}). */
180+ UTF16_CODE_UNITS {
181+ @ Override
182+ int getLength (final CharSequence value , final Charset charset ) {
183+ return value .length ();
184+ }
185+
186+ @ Override
187+ CharSequence truncate (final CharSequence value , final int limit , final Charset charset ) {
188+ if (!UTF_16 .newEncoder ().canEncode (value )) {
189+ throw new IllegalArgumentException (
190+ "The value " + value + " can not be encoded using " + UTF_16 .name ());
191+ }
192+
193+ // Fast path: no truncation needed.
194+ if (value .length () <= limit ) {
195+ return value ;
196+ }
197+
198+ // Slow path: truncate to limit.
199+ // 1. Compute length of extension in chars (if any).
200+ final CharSequence [] parts = splitExtension (value );
201+ final int extensionLength = parts [1 ].length ();
202+ if (extensionLength > 0 && extensionLength >= limit ) {
203+ // Extension itself does not fit
204+ throw new IllegalArgumentException (
205+ "The extension of " + value + " is too long to fit within " + limit + " characters" );
206+ }
207+
208+ // 2. Truncate the non-extension part and append the extension (if any).
209+ final CharSequence truncated = safeTruncate (value , limit - extensionLength );
210+ return extensionLength == 0 ? truncated : truncated .toString () + parts [1 ];
211+ }
212+ };
213+
214+ /**
215+ * Gets the measured length in this strategy’s unit.
216+ *
217+ * @param value The value to measure, not null.
218+ * @param charset The charset to use when measuring in bytes.
219+ * @return The length in this strategy’s unit.
220+ */
221+ abstract int getLength (CharSequence value , Charset charset );
222+
223+ /**
224+ * Tests if the measured length is less or equal the {@code limit}.
225+ *
226+ * @param value The value to measure, not null.
227+ * @param limit The limit to compare to.
228+ * @param charset The charset to use when measuring in bytes.
229+ * @return {@code true} if the measured length is less or equal the {@code limit}, {@code false} otherwise.
230+ */
231+ final boolean isWithinLimit (final CharSequence value , final int limit , final Charset charset ) {
232+ return getLength (value , charset ) <= limit ;
233+ }
234+
235+ /**
236+ * Truncates to {@code limit} in this strategy’s unit (no-op if already within limit).
237+ *
238+ * @param value The value to truncate, not null.
239+ * @param limit The limit to truncate to.
240+ * @param charset The charset to use when measuring in bytes.
241+ * @return The truncated value, not null.
242+ */
243+ abstract CharSequence truncate (CharSequence value , int limit , Charset charset );
244+ }
245+
116246 /**
117247 * <p>
118248 * Is {@code true} if this is Linux.
@@ -260,7 +390,42 @@ private static boolean isOsNameMatch(final String osName, final String osNamePre
260390 private static String replace (final String path , final char oldChar , final char newChar ) {
261391 return path == null ? null : path .replace (oldChar , newChar );
262392 }
263-
393+ /**
394+ * Truncates a string respecting grapheme cluster boundaries.
395+ *
396+ * @param value The value to truncate.
397+ * @param limit The maximum length.
398+ * @return The truncated value.
399+ * @throws IllegalArgumentException If the first grapheme cluster is longer than the limit.
400+ */
401+ private static CharSequence safeTruncate (final CharSequence value , final int limit ) {
402+ if (value .length () <= limit ) {
403+ return value ;
404+ }
405+ final BreakIterator boundary = BreakIterator .getCharacterInstance (Locale .ROOT );
406+ final String text = value .toString ();
407+ boundary .setText (text );
408+ final int end = boundary .preceding (limit + 1 );
409+ assert end != BreakIterator .DONE ;
410+ if (end == 0 ) {
411+ final String limitMessage = limit <= 1 ? "1 character" : limit + " characters" ;
412+ throw new IllegalArgumentException ("The value " + value + " can not be truncated to " + limitMessage
413+ + " without breaking the first codepoint or grapheme cluster" );
414+ }
415+ return text .substring (0 , end );
416+ }
417+ static CharSequence [] splitExtension (final CharSequence value ) {
418+ final int index = indexOfFirstDot (value );
419+ // An initial dot is not an extension
420+ return index < 1
421+ ? new CharSequence [] {value , "" }
422+ : new CharSequence [] {value .subSequence (0 , index ), value .subSequence (index , value .length ())};
423+ }
424+ static CharSequence trimExtension (final CharSequence cs ) {
425+ final int index = indexOfFirstDot (cs );
426+ // An initial dot is not an extension
427+ return index < 1 ? cs : cs .subSequence (0 , index );
428+ }
264429 private final int blockSize ;
265430 private final boolean casePreserving ;
266431 private final boolean caseSensitive ;
@@ -269,9 +434,13 @@ private static String replace(final String path, final char oldChar, final char
269434 private final int maxPathLength ;
270435 private final String [] reservedFileNames ;
271436 private final boolean reservedFileNamesExtensions ;
437+
272438 private final boolean supportsDriveLetter ;
439+
273440 private final char nameSeparator ;
441+
274442 private final char nameSeparatorOther ;
443+
275444 private final NameLengthStrategy nameLengthStrategy ;
276445
277446 /**
@@ -571,173 +740,4 @@ public String toLegalFileName(final String candidate, final char replacement, fi
571740 return new String (array , 0 , array .length );
572741 }
573742
574- static CharSequence trimExtension (final CharSequence cs ) {
575- final int index = indexOfFirstDot (cs );
576- // An initial dot is not an extension
577- return index < 1 ? cs : cs .subSequence (0 , index );
578- }
579-
580- static CharSequence [] splitExtension (final CharSequence value ) {
581- final int index = indexOfFirstDot (value );
582- // An initial dot is not an extension
583- return index < 1
584- ? new CharSequence [] {value , "" }
585- : new CharSequence [] {value .subSequence (0 , index ), value .subSequence (index , value .length ())};
586- }
587-
588- /**
589- * Truncates a string respecting grapheme cluster boundaries.
590- *
591- * @param value The value to truncate.
592- * @param limit The maximum length.
593- * @return The truncated value.
594- * @throws IllegalArgumentException If the first grapheme cluster is longer than the limit.
595- */
596- private static CharSequence safeTruncate (final CharSequence value , final int limit ) {
597- if (value .length () <= limit ) {
598- return value ;
599- }
600- final BreakIterator boundary = BreakIterator .getCharacterInstance (Locale .ROOT );
601- final String text = value .toString ();
602- boundary .setText (text );
603- final int end = boundary .preceding (limit + 1 );
604- assert end != BreakIterator .DONE ;
605- if (end == 0 ) {
606- final String limitMessage = limit <= 1 ? "1 character" : limit + " characters" ;
607- throw new IllegalArgumentException ("The value " + value + " can not be truncated to " + limitMessage
608- + " without breaking the first codepoint or grapheme cluster" );
609- }
610- return text .substring (0 , end );
611- }
612-
613- /**
614- * Strategy for measuring and truncating file or path names in different units.
615- * Implementations measure length and can truncate to a specified limit.
616- */
617- enum NameLengthStrategy {
618- /** Length measured as encoded bytes. */
619- BYTES {
620- @ Override
621- int getLength (final CharSequence value , final Charset charset ) {
622- final CharsetEncoder enc = charset .newEncoder ()
623- .onMalformedInput (CodingErrorAction .REPORT )
624- .onUnmappableCharacter (CodingErrorAction .REPORT );
625- try {
626- return enc .encode (CharBuffer .wrap (value )).remaining ();
627- } catch (CharacterCodingException e ) {
628- // Unencodable, does not fit any byte limit.
629- return Integer .MAX_VALUE ;
630- }
631- }
632-
633- @ Override
634- CharSequence truncate (final CharSequence value , final int limit , final Charset charset ) {
635- final CharsetEncoder encoder = charset .newEncoder ()
636- .onMalformedInput (CodingErrorAction .REPORT )
637- .onUnmappableCharacter (CodingErrorAction .REPORT );
638-
639- if (!encoder .canEncode (value )) {
640- throw new IllegalArgumentException (
641- "The value " + value + " cannot be encoded using " + charset .name ());
642- }
643-
644- // Fast path: if even the worst-case expansion fits, we're done.
645- if (value .length () <= Math .floor (limit / encoder .maxBytesPerChar ())) {
646- return value ;
647- }
648-
649- // Slow path: encode into a fixed-size byte buffer.
650- // 1. Compute length of extension in bytes (if any).
651- final CharSequence [] parts = splitExtension (value );
652- final int extensionLength = getLength (parts [1 ], charset );
653- if (extensionLength > 0 && extensionLength >= limit ) {
654- // Extension itself does not fit
655- throw new IllegalArgumentException (
656- "The extension of " + value + " is too long to fit within " + limit + " bytes" );
657- }
658-
659- // 2. Compute the character part that fits within the remaining byte budget.
660- final ByteBuffer byteBuffer = ByteBuffer .allocate (limit - extensionLength );
661- final CharBuffer charBuffer = CharBuffer .wrap (parts [0 ]);
662-
663- // Encode until the first character that would exceed the byte budget.
664- final CoderResult cr = encoder .encode (charBuffer , byteBuffer , true );
665-
666- if (cr .isUnderflow ()) {
667- // Entire candidate fit within maxFileNameLength bytes.
668- return value ;
669- }
670-
671- final CharSequence truncated = safeTruncate (value , charBuffer .position ());
672- return extensionLength == 0 ? truncated : truncated .toString () + parts [1 ];
673- }
674- },
675-
676- /** Length measured as UTF-16 code units (i.e., {@code CharSequence.length()}). */
677- UTF16_CODE_UNITS {
678- @ Override
679- int getLength (final CharSequence value , final Charset charset ) {
680- return value .length ();
681- }
682-
683- @ Override
684- CharSequence truncate (final CharSequence value , final int limit , final Charset charset ) {
685- if (!UTF_16 .newEncoder ().canEncode (value )) {
686- throw new IllegalArgumentException (
687- "The value " + value + " can not be encoded using " + UTF_16 .name ());
688- }
689-
690- // Fast path: no truncation needed.
691- if (value .length () <= limit ) {
692- return value ;
693- }
694-
695- // Slow path: truncate to limit.
696- // 1. Compute length of extension in chars (if any).
697- final CharSequence [] parts = splitExtension (value );
698- final int extensionLength = parts [1 ].length ();
699- if (extensionLength > 0 && extensionLength >= limit ) {
700- // Extension itself does not fit
701- throw new IllegalArgumentException (
702- "The extension of " + value + " is too long to fit within " + limit + " characters" );
703- }
704-
705- // 2. Truncate the non-extension part and append the extension (if any).
706- final CharSequence truncated = safeTruncate (value , limit - extensionLength );
707- return extensionLength == 0 ? truncated : truncated .toString () + parts [1 ];
708- }
709- };
710-
711- /**
712- * Gets the measured length in this strategy’s unit.
713- *
714- * @param value The value to measure, not null.
715- * @param charset The charset to use when measuring in bytes.
716- * @return The length in this strategy’s unit.
717- */
718- abstract int getLength (CharSequence value , Charset charset );
719-
720- /**
721- * Tests if the measured length is less or equal the {@code limit}.
722- *
723- * @param value The value to measure, not null.
724- * @param limit The limit to compare to.
725- * @param charset The charset to use when measuring in bytes.
726- * @return {@code true} if the measured length is less or equal the {@code limit}, {@code false} otherwise.
727- */
728- final boolean isWithinLimit (final CharSequence value , final int limit , final Charset charset ) {
729- return getLength (value , charset ) <= limit ;
730- }
731-
732- /**
733- * Truncates to {@code limit} in this strategy’s unit (no-op if already within limit).
734- *
735- * @param value The value to truncate, not null.
736- * @param limit The limit to truncate to.
737- * @param charset The charset to use when measuring in bytes.
738- * @return The truncated value, not null.
739- */
740- abstract CharSequence truncate (CharSequence value , int limit , Charset charset );
741- }
742-
743743}
0 commit comments