1 package nom.tam.fits;
2
3 /*
4 * #%L
5 * nom.tam FITS library
6 * %%
7 * Copyright (C) 2004 - 2024 nom-tam-fits
8 * %%
9 * This is free and unencumbered software released into the public domain.
10 *
11 * Anyone is free to copy, modify, publish, use, compile, sell, or
12 * distribute this software, either in source code form or as a compiled
13 * binary, for any purpose, commercial or non-commercial, and by any
14 * means.
15 *
16 * In jurisdictions that recognize copyright laws, the author or authors
17 * of this software dedicate any and all copyright interest in the
18 * software to the public domain. We make this dedication for the benefit
19 * of the public at large and to the detriment of our heirs and
20 * successors. We intend this dedication to be an overt act of
21 * relinquishment in perpetuity of all present and future rights to this
22 * software under copyright law.
23 *
24 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
25 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
26 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
27 * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
28 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
29 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
30 * OTHER DEALINGS IN THE SOFTWARE.
31 * #L%
32 */
33
34 import java.io.Closeable;
35 import java.io.IOException;
36 import java.io.InputStream;
37 import java.lang.reflect.Array;
38 import java.net.HttpURLConnection;
39 import java.net.ProtocolException;
40 import java.net.URL;
41 import java.net.URLConnection;
42 import java.text.ParsePosition;
43 import java.util.ArrayList;
44 import java.util.Arrays;
45 import java.util.logging.Level;
46 import java.util.logging.Logger;
47
48 import nom.tam.util.ArrayDataOutput;
49 import nom.tam.util.AsciiFuncs;
50 import nom.tam.util.FitsDecoder;
51 import nom.tam.util.FitsEncoder;
52 import nom.tam.util.FitsIO;
53 import nom.tam.util.RandomAccess;
54
55 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
56
57 /**
58 * Static utility functions used throughout the FITS classes.
59 */
60 public final class FitsUtil {
61
62 /** Lowest ASCII value that can be in FITS strings */
63 static final byte BLANK_SPACE = 0x20;
64
65 /** Highest ASCII value that can be in FITS strings */
66 static final byte MIN_ASCII_VALUE = 0x20;
67
68 /** Highest ASCII value that can be in FITS strings */
69 static final byte MAX_ASCII_VALUE = 0x7e;
70
71 /** Highest ASCII value that can be in FITS strings */
72 static final byte ASCII_NULL = 0x00;
73
74 /**
75 * the logger to log to.
76 */
77 private static final Logger LOG = Logger.getLogger(FitsUtil.class.getName());
78
79 private static boolean wroteCheckingError = false;
80
81 /**
82 * Utility class, do not instantiate it.
83 */
84 private FitsUtil() {
85 }
86
87 /**
88 * @deprecated use {@link #addPadding(long)} instead. Calculates the amount of padding needed to complete the
89 * last FITS block at the specified current size.
90 *
91 * @return Total size of blocked FITS element, using e.v. padding to fits block size.
92 *
93 * @param size the current size.
94 */
95 public static int addPadding(int size) {
96 return size + padding(size);
97 }
98
99 /**
100 * Calculates the amount of padding needed to complete the last FITS block at the specified current size.
101 *
102 * @return Total size of blocked FITS element, using e.v. padding to fits block size.
103 *
104 * @param size the current size.
105 *
106 * @see #padding(long)
107 * @see #pad(ArrayDataOutput, long)
108 */
109 public static long addPadding(long size) {
110 return size + padding(size);
111 }
112
113 /**
114 * Converts an array of <code>boolean</code> or {@link Boolean} values to FITS logicals (bytes containint 'T', 'F'
115 * or '\0'). The shapes and size of the resulting array matches that of the input. Values of '\0' are converted to
116 * <code>null</code> values.
117 *
118 * @param o a new array of <code>Boolean</code>
119 *
120 * @return an array of FITS logical values with the same size and shape as the input
121 *
122 * @see #bytesToBooleanObjects(Object)
123 * @see #byteToBoolean(byte[])
124 *
125 * @since 1.18
126 */
127 static Object booleansToBytes(Object o) {
128 if (o == null) {
129 return new byte[] {FitsEncoder.byteForBoolean(null)};
130 }
131
132 if (o instanceof Boolean) {
133 return new byte[] {FitsEncoder.byteForBoolean((Boolean) o)};
134 }
135
136 if (o instanceof boolean[]) {
137 boolean[] bool = (boolean[]) o;
138 byte[] b = new byte[bool.length];
139 for (int i = 0; i < bool.length; i++) {
140 b[i] = FitsEncoder.byteForBoolean(bool[i]);
141 }
142 return b;
143 }
144
145 if (o instanceof Boolean[]) {
146 Boolean[] bool = (Boolean[]) o;
147 byte[] b = new byte[bool.length];
148 for (int i = 0; i < bool.length; i++) {
149 b[i] = FitsEncoder.byteForBoolean(bool[i]);
150 }
151 return b;
152 }
153
154 if (o instanceof Object[]) {
155 Object[] array = (Object[]) o;
156 Object[] b = null;
157
158 for (int i = 0; i < array.length; i++) {
159 Object e = booleansToBytes(array[i]);
160 if (b == null) {
161 b = (Object[]) Array.newInstance(e.getClass(), array.length);
162 }
163 b[i] = e;
164 }
165
166 return b;
167 }
168
169 throw new IllegalArgumentException("Not boolean values: " + o.getClass().getName());
170 }
171
172 /**
173 * Converts an array of FITS logicals (bytes containint 'T', 'F' or '\0') to an array of {@link Boolean}. The shapes
174 * and size of the resulting array matches that of the input. Values of '\0' are converted to <code>null</code>
175 * values.
176 *
177 * @param bytes and array of FITS logical values
178 *
179 * @return a new array of <code>Boolean</code> with the same size and shape as the input
180 *
181 * @see #booleansToBytes(Object)
182 *
183 * @since 1.18
184 */
185 static Object bytesToBooleanObjects(Object bytes) {
186 if (bytes instanceof Byte) {
187 return FitsDecoder.booleanObjectFor(((Number) bytes).intValue());
188 }
189
190 if (bytes instanceof byte[]) {
191 byte[] b = (byte[]) bytes;
192 Boolean[] bool = new Boolean[b.length];
193 for (int i = 0; i < b.length; i++) {
194 bool[i] = FitsDecoder.booleanObjectFor(b[i]);
195 }
196 return bool;
197 }
198
199 if (bytes instanceof Object[]) {
200 Object[] array = (Object[]) bytes;
201 Object[] bool = null;
202
203 for (int i = 0; i < array.length; i++) {
204 Object e = bytesToBooleanObjects(array[i]);
205 if (bool == null) {
206 bool = (Object[]) Array.newInstance(e.getClass(), array.length);
207 }
208 bool[i] = e;
209 }
210
211 return bool;
212 }
213
214 throw new IllegalArgumentException("Cannot convert to boolean values: " + bytes.getClass().getName());
215 }
216
217 /**
218 * Converts an array of booleans into the bits packed into a block of bytes.
219 *
220 * @param bits an array of bits
221 *
222 * @return a new byte array containing the packed bits (in big-endian order)
223 *
224 * @see #bytesToBits(Object)
225 *
226 * @since 1.18
227 */
228 static byte[] bitsToBytes(boolean[] bits) throws IllegalArgumentException {
229 byte[] bytes = new byte[(bits.length + Byte.SIZE - 1) / Byte.SIZE];
230 for (int i = 0; i < bits.length; i++) {
231 if (bits[i]) {
232 int pos = Byte.SIZE - 1 - i % Byte.SIZE;
233 bytes[i / Byte.SIZE] |= 1 << pos;
234 }
235 }
236
237 return bytes;
238 }
239
240 /**
241 * Converts an array of bit segments into the bits packed into a blocks of bytes.
242 *
243 * @param bits an array of bits
244 * @param l the number of bits in a segment that are to be kept together
245 *
246 * @return a new byte array containing the packed bits (in big-endian order)
247 *
248 * @see #bytesToBits(Object)
249 *
250 * @since 1.18
251 */
252 static byte[] bitsToBytes(boolean[] bits, int l) throws IllegalArgumentException {
253 int n = bits.length / l; // Number of bit segments
254 int bl = (l + Byte.SIZE - 1) / Byte.SIZE; // Number of bytes per segment
255 byte[] bytes = new byte[n * bl]; // The converted byte array
256
257 for (int i = 0; i < n; i++) {
258 int off = i * l; // bit offset
259 int boff = i * bl; // byte offset
260
261 for (int j = 0; j < l; j++) {
262 if (bits[off + j]) {
263 int pos = Byte.SIZE - 1 - j % Byte.SIZE;
264 bytes[boff + (j / Byte.SIZE)] |= 1 << pos;
265 }
266 }
267 }
268
269 return bytes;
270 }
271
272 /**
273 * Converts the bits packed into a block of bytes into a boolean array.
274 *
275 * @param bytes the byte array containing the packed bits (in big-endian order)
276 * @param count the number of bits to extract
277 *
278 * @return an array of boolean with the separated bit values.
279 *
280 * @see #bitsToBytes(Object)
281 *
282 * @since 1.18
283 */
284 static boolean[] bytesToBits(byte[] bytes, int count) {
285 boolean[] bits = new boolean[count];
286
287 for (int i = 0; i < bits.length; i++) {
288 int pos = Byte.SIZE - 1 - i % Byte.SIZE;
289 bits[i] = ((bytes[i / Byte.SIZE] >>> pos) & 1) == 1;
290 }
291
292 return bits;
293 }
294
295 /**
296 * Extracts a string from a byte array at the specified offset, maximal length and termination byte. This method
297 * trims trailing spaces but not leading ones.
298 *
299 * @param bytes an array of ASCII bytes
300 * @param offset the array index at which the string begins
301 * @param maxLen the maximum number of bytes to extract from the position
302 * @param terminator the byte value that terminates the string, such as 0x00.
303 *
304 * @return a new String with the relevant bytes, with length not exceeding the specified limit.
305 *
306 * @since 1.18
307 */
308 static String extractString(byte[] bytes, ParsePosition pos, int maxLen, byte terminator) {
309 int offset = pos.getIndex();
310
311 if (offset >= bytes.length) {
312 return "";
313 }
314
315 if (offset + maxLen > bytes.length) {
316 maxLen = bytes.length - offset;
317 }
318
319 int end = -1;
320
321 // Check up to the specified length or termination
322 for (int i = 0; i < maxLen; i++) {
323 byte b = bytes[offset + i];
324 pos.setIndex(offset + i);
325
326 if (b == terminator || b == 0) {
327 break;
328 }
329
330 if (b != BLANK_SPACE) {
331 end = i;
332 }
333 }
334
335 pos.setIndex(pos.getIndex() + 1);
336
337 byte[] sanitized = new byte[end + 1];
338 boolean checking = FitsFactory.getCheckAsciiStrings();
339
340 // Up to the specified length or terminator
341 for (int i = 0; i <= end; i++) {
342 byte b = bytes[offset + i];
343
344 if (checking && (b < BLANK_SPACE || b > MAX_ASCII_VALUE)) {
345 if (!wroteCheckingError) {
346 LOG.warning("WARNING! Converting invalid table string character[s] to spaces.");
347 wroteCheckingError = true;
348 }
349 b = BLANK_SPACE;
350 }
351
352 sanitized[i] = b;
353 }
354
355 return AsciiFuncs.asciiString(sanitized);
356 }
357
358 /**
359 * Converts a FITS byte sequence to a Java string array, triming spaces at the heads and tails of each element.
360 * While FITS typically considers leading spaces significant, this library has been removing them from regularly
361 * shaped string arrays for a very long time, apparently based on request by users then... Even though it seems like
362 * a bad choice, since users could always call {@link String#trim()} if they needed to, we cannot recover the
363 * leading spaces once the string was trimmed. At this point we have no real choice but to continue the tradition,
364 * lest we want to break exising applications, which may rely on this behavior.
365 *
366 * @return Convert bytes to Strings, removing leading and trailing spaces from each entry.
367 *
368 * @param bytes byte array to convert
369 * @param maxLen the max string length
370 *
371 * @deprecated (<i>for internal use</i>) No longer used internally, will be removed in the future.
372 */
373 public static String[] byteArrayToStrings(byte[] bytes, int maxLen) {
374 // Note that if a String in a binary table contains an internal 0,
375 // the FITS standard says that it is to be considered as terminating
376 // the string at that point, so that software reading the
377 // data back may not include subsequent characters.
378 // No warning of this truncation is given.
379
380 String[] res = new String[bytes.length / maxLen];
381 for (int i = 0; i < res.length; i++) {
382 res[i] = extractString(bytes, new ParsePosition(i * maxLen), maxLen, (byte) '\0').trim();
383 }
384 return res;
385 }
386
387 /**
388 * Converts a FITS representation of boolean values as bytes to a java boolean array. This implementation does not
389 * handle FITS <code>null</code> values.
390 *
391 * @return Convert an array of bytes to booleans.
392 *
393 * @param bytes the array of bytes to get the booleans from.
394 *
395 * @see FitsDecoder#booleanFor(int)
396 */
397 static boolean[] byteToBoolean(byte[] bytes) {
398 boolean[] bool = new boolean[bytes.length];
399
400 for (int i = 0; i < bytes.length; i++) {
401 bool[i] = FitsDecoder.booleanFor(bytes[i]);
402 }
403 return bool;
404 }
405
406 /**
407 * Parses a logical value from a string, using loose conversion. The string may contain either 'true'/'false' or
408 * 'T'/'F' (case insensitive), or else a zero (<code>false</code>) or non-zero number value (<code>true</code>). All
409 * other strings will return <code>null</code> corresponding to an undefined logical value.
410 *
411 * @param s A string
412 *
413 * @return <code>true</code>, <code>false</code>, or <code>null</code> (if undefined).
414 */
415 @SuppressFBWarnings(value = "NP_BOOLEAN_RETURN_NULL", justification = "null has specific meaning here")
416 static Boolean parseLogical(String s) {
417 if (s == null) {
418 return null;
419 }
420
421 s = s.trim();
422
423 if (s.isEmpty()) {
424 return null;
425 }
426 if (s.equalsIgnoreCase("true") || s.equalsIgnoreCase("t")) {
427 return true;
428 }
429 if (s.equalsIgnoreCase("false") || s.equalsIgnoreCase("f")) {
430 return false;
431 }
432
433 try {
434 long l = Long.parseLong(s);
435 return l != 0;
436 } catch (NumberFormatException e) {
437 // Nothing to do....
438 }
439
440 try {
441 double d = Double.parseDouble(s);
442 if (!Double.isNaN(d)) {
443 return d != 0;
444 }
445 } catch (NumberFormatException e) {
446 // Nothing to do....
447 }
448
449 return null;
450 }
451
452 /**
453 * Gets the file offset for the given IO resource.
454 *
455 * @return The offset from the beginning of file (if random accessible), or -1 otherwise.
456 *
457 * @param o the stream to get the position
458 *
459 * @deprecated (<i>for internal use</i>) Visibility may be reduced to the package level in the future.
460 */
461 public static long findOffset(Closeable o) {
462 if (o instanceof RandomAccess) {
463 return ((RandomAccess) o).getFilePointer();
464 }
465 return -1;
466 }
467
468 /**
469 * Gets and input stream for a given URL resource.
470 *
471 * @return Get a stream to a URL accommodating possible redirections. Note that if a redirection request
472 * points to a different protocol than the original request, then the redirection is not
473 * handled automatically.
474 *
475 * @param url the url to get the stream from
476 * @param level max levels of redirection
477 *
478 * @throws IOException if the operation failed
479 */
480 public static InputStream getURLStream(URL url, int level) throws IOException {
481 URLConnection conn = null;
482 int code = -1;
483 try {
484 conn = url.openConnection();
485 if (conn instanceof HttpURLConnection) {
486 code = ((HttpURLConnection) conn).getResponseCode();
487 }
488 return conn.getInputStream();
489 } catch (ProtocolException e) {
490 LOG.log(Level.WARNING, "could not connect to " + url + (code >= 0 ? " got responce-code" + code : ""), e);
491 throw e;
492 }
493 }
494
495 /**
496 * Returns the maximum string length in an array.
497 *
498 * @return the maximum length of string in an array.
499 *
500 * @param strings array of strings to check
501 *
502 * @deprecated (<i>for internal use</i>) No longer used internally, may be removed in the future.
503 */
504 public static int maxLength(String[] strings) {
505 int max = 0;
506 for (String element : strings) {
507 if (element != null && element.length() > max) {
508 max = element.length();
509 }
510 }
511 return max;
512 }
513
514 /**
515 * Returns the maximum string length in an array of strings. Non-string elements nd null values are ignored.
516 *
517 * @return the maximum length of strings in an array.
518 *
519 * @param o array of strings to check
520 */
521 static int maxStringLength(Object o) {
522 if (o instanceof String) {
523 return ((String) o).length();
524 }
525
526 int max = 0;
527
528 if (o instanceof Object[]) {
529 for (Object e : (Object[]) o) {
530 if (e == null) {
531 continue;
532 }
533
534 int l = maxStringLength(e);
535 if (l > max) {
536 max = l;
537 }
538 }
539 }
540
541 return max;
542 }
543
544 /**
545 * Returns the minimum string length in an array of strings. Non-string elements nd null values are ignored.
546 *
547 * @return the minimum length of strings in an array.
548 *
549 * @param o strings array of strings to check
550 */
551 static int minStringLength(Object o) {
552 if (o instanceof String) {
553 return ((String) o).length();
554 }
555
556 int min = -1;
557
558 if (o instanceof Object[]) {
559 for (Object e : (Object[]) o) {
560 if (e == null) {
561 return 0;
562 }
563
564 int l = minStringLength(e);
565 if (l == 0) {
566 return 0;
567 }
568
569 if (min < 0 || l < min) {
570 min = l;
571 }
572 }
573 }
574
575 return min < 0 ? 0 : min;
576 }
577
578 /**
579 * Adds the necessary amount of padding needed to complete the last FITS block.
580 *
581 * @param stream stream to pad
582 * @param size the current size of the stream (total number of bytes written to it since the beginning
583 * of the FITS).
584 *
585 * @throws FitsException if the operation failed
586 *
587 * @see #pad(ArrayDataOutput, long, byte)
588 *
589 * @deprecated (<i>for internal use</i>) Visibility may be reduced to package level in the future
590 */
591 public static void pad(ArrayDataOutput stream, long size) throws FitsException {
592 pad(stream, size, (byte) 0);
593 }
594
595 /**
596 * Adds the necessary amount of padding needed to complete the last FITS block., usign the designated padding byte
597 * value.
598 *
599 * @param stream stream to pad
600 * @param size the current size of the stream (total number of bytes written to it since the beginning
601 * of the FITS).
602 * @param fill the byte value to use for the padding
603 *
604 * @throws FitsException if the operation failed
605 *
606 * @see #pad(ArrayDataOutput, long)
607 *
608 * @deprecated (<i>for internal use</i>) Visibility may be reduced to private in the future
609 */
610 public static void pad(ArrayDataOutput stream, long size, byte fill) throws FitsException {
611 int len = padding(size);
612 if (len > 0) {
613 byte[] buf = new byte[len];
614 Arrays.fill(buf, fill);
615 try {
616 stream.write(buf);
617 stream.flush();
618 } catch (Exception e) {
619 throw new FitsException("Unable to write padding", e);
620 }
621 }
622 }
623
624 /**
625 * @deprecated see Use {@link #padding(long)} instead.
626 *
627 * @return How many bytes are needed to fill a 2880 block?
628 *
629 * @param size the size without padding
630 */
631 public static int padding(int size) {
632 return padding((long) size);
633 }
634
635 /**
636 * Calculated the amount of padding we need to add given the current size of a FITS file (under construction)
637 *
638 * @param size the current size of our FITS file before the padding
639 *
640 * @return the number of bytes of padding we need to add at the end to complete the FITS block.
641 *
642 * @see #addPadding(long)
643 * @see #pad(ArrayDataOutput, long)
644 */
645 public static int padding(long size) {
646 int mod = (int) (size % FitsFactory.FITS_BLOCK_SIZE);
647 if (mod > 0) {
648 return FitsFactory.FITS_BLOCK_SIZE - mod;
649 }
650 return 0;
651 }
652
653 /**
654 * Attempts to reposition a FITS input ot output. The call will succeed only if the underlying input or output is
655 * random accessible. Othewise, an exception will be thrown.
656 *
657 * @deprecated This method wraps an {@link IOException} into a {@link FitsException} for no good
658 * reason really. A revision of the API could reduce the visibility of this method,
659 * and/or procees the underlying exception instead.
660 *
661 * @param o the FITS input or output
662 * @param offset the offset to position it to.
663 *
664 * @throws FitsException if the underlying input/output is not random accessible or if the requested position is
665 * invalid.
666 */
667 @Deprecated
668 public static void reposition(FitsIO o, long offset) throws FitsException {
669 // TODO AK: argument should be RandomAccess instead of Closeable, since
670 // that's the only type we actually handle...
671
672 if (o == null) {
673 throw new FitsException("Attempt to reposition null stream");
674 }
675
676 if (!(o instanceof RandomAccess) || offset < 0) {
677 throw new FitsException(
678 "Invalid attempt to reposition stream " + o + " of type " + o.getClass().getName() + " to " + offset);
679 }
680
681 try {
682 ((RandomAccess) o).seek(offset);
683 } catch (IOException e) {
684 throw new FitsException("Unable to repostion stream " + o + " of type " + o.getClass().getName() + " to "
685 + offset + ": " + e.getMessage(), e);
686 }
687 }
688
689 /**
690 * Converts a string to ASCII bytes in the specified array, padding (with 0x00) or truncating as necessary to
691 * provide the expected length at the specified arrya offset.
692 *
693 * @param s a string
694 * @param res the byte array into which to extract the ASCII bytes
695 * @param offset array index in the byte array at which the extracted bytes should begin
696 * @param len the maximum number of bytes to extract, truncating or padding (with 0x00) as needed.
697 * @param pad the byte value to use to pad the remainder of the string
698 */
699 private static void stringToBytes(String s, byte[] res, int offset, int len, byte pad) {
700 int l = 0;
701
702 if (s != null) {
703 byte[] b = AsciiFuncs.getBytes(s);
704 l = Math.min(b.length, len);
705 if (l > 0) {
706 System.arraycopy(b, 0, res, offset, l);
707 }
708 }
709
710 // Terminate and pad as necessary
711 if (l < len) {
712 Arrays.fill(res, offset + l, offset + len, pad);
713 }
714 }
715
716 /**
717 * Converts a string to an array of ASCII bytes, padding (with 0x00) or truncating as necessary to provide the
718 * expected length.
719 *
720 * @param s a string
721 * @param len the number of bytes for the return value, also the maximum number of bytes that are extracted from
722 * the string
723 *
724 * @return a byte array of the specified length containing the truncated or padded string value.
725 *
726 * @see #stringsToByteArray(String[], int, byte) q
727 *
728 * @since 1.18
729 */
730 static byte[] stringToByteArray(String s, int len) {
731 byte[] res = new byte[len];
732 stringToBytes(s, res, 0, len, BLANK_SPACE);
733 return res;
734 }
735
736 /**
737 * Convert an array of Strings to bytes. padding (with 0x00) or truncating as necessary to provide the expected
738 * length.
739 *
740 * @return the resulting bytes
741 *
742 * @param stringArray the array with Strings
743 * @param len the number of bytes used for each string element. The string will be truncated ot padded
744 * as necessary to fit into that size.
745 *
746 * @deprecated (<i>for internal use</i>) Visibility may be reduced to package level in the future.
747 */
748 public static byte[] stringsToByteArray(String[] stringArray, int len) {
749 return stringsToByteArray(stringArray, len, BLANK_SPACE);
750 }
751
752 /**
753 * Convert an array of Strings to bytes. padding (with 0x00) or truncating as necessary to provide the expected
754 * length.
755 *
756 * @return the resulting bytes
757 *
758 * @param stringArray the array with Strings
759 * @param len the number of bytes used for each string element. The string will be truncated ot padded as
760 * necessary to fit into that size.
761 * @param pad the byte value to use for padding strings as necessary to the requisite length.
762 */
763 static byte[] stringsToByteArray(String[] stringArray, int len, byte pad) {
764 byte[] res = new byte[stringArray.length * len];
765 for (int i = 0; i < stringArray.length; i++) {
766 stringToBytes(stringArray[i], res, i * len, len, pad);
767 }
768 return res;
769 }
770
771 /**
772 * Convert an array of strings to a delimited sequence of bytes, e.g. for sequentialized variable-sized storage of
773 * multiple string elements. The last string component is terminated by an ASCII NUL (0x00).
774 *
775 * @return the resulting bytes
776 *
777 * @param array the array with Strings
778 * @param maxlen the maximum string length or -1 of unlimited.
779 * @param delim the byte value that delimits string components
780 *
781 * @see #stringToByteArray(String, int)
782 *
783 * @since 1.18
784 */
785 static byte[] stringsToDelimitedBytes(String[] array, int maxlen, byte delim) {
786 int l = array.length - 1;
787 for (String s : array) {
788 l += (s == null) ? 1 : Math.max(s.length() + 1, maxlen);
789 }
790 byte[] b = new byte[l];
791 l = 0;
792 for (String s : array) {
793 if (s != null) {
794 stringToBytes(s, b, l, s.length(), BLANK_SPACE);
795 l += s.length();
796 }
797 b[l++] = delim;
798 }
799 b[l - 1] = (byte) 0;
800 return b;
801 }
802
803 /**
804 * Extracts strings from a packed delimited byte sequence. Strings start either immediately after the prior string
805 * reached its maximum length, or else immediately after the specified delimiter byte value.
806 *
807 * @param bytes bytes containing the packed strings
808 * @param maxlen the maximum length of individual string components
809 * @param delim the byte value that delimits strings shorter than the maximum length
810 *
811 * @return An array of the extracted strings
812 *
813 * @see #stringsToDelimitedBytes(String[], int, byte)
814 *
815 * @since 1.18
816 */
817 private static String[] delimitedBytesToStrings(byte[] bytes, byte delim) {
818 ArrayList<String> s = new ArrayList<>();
819 ParsePosition pos = new ParsePosition(0);
820 while (pos.getIndex() < bytes.length) {
821 s.add(extractString(bytes, pos, bytes.length, delim));
822 }
823 String[] a = new String[s.size()];
824 s.toArray(a);
825 return a;
826 }
827
828 /**
829 * Extracts strings from a packed delimited byte sequence. Strings start either immediately after the prior string
830 * reached its maximum length, or else immediately after the specified delimiter byte value.
831 *
832 * @param bytes bytes containing the packed strings
833 * @param maxlen the maximum length of individual string components
834 * @param delim the byte value that delimits strings shorter than the maximum length
835 *
836 * @return An array of the extracted strings
837 *
838 * @see #stringsToDelimitedBytes(String[], int, byte)
839 *
840 * @since 1.18
841 */
842 static String[] delimitedBytesToStrings(byte[] bytes, int maxlen, byte delim) {
843 if (maxlen <= 0) {
844 return delimitedBytesToStrings(bytes, delim);
845 }
846
847 String[] res = new String[(bytes.length + maxlen - 1) / maxlen];
848 ParsePosition pos = new ParsePosition(0);
849 for (int i = 0; i < res.length; i++) {
850 res[i] = extractString(bytes, pos, maxlen, delim);
851 }
852 return res;
853 }
854
855 }