View Javadoc
1   /*
2    * Copyright 2016-2025 The OSHI Project Contributors
3    * SPDX-License-Identifier: MIT
4    */
5   package oshi.util;
6   
7   import java.math.BigInteger;
8   import java.net.InetAddress;
9   import java.net.UnknownHostException;
10  import java.nio.ByteBuffer;
11  import java.nio.ByteOrder;
12  import java.nio.charset.StandardCharsets;
13  import java.time.LocalDateTime;
14  import java.time.ZoneId;
15  import java.time.LocalTime;
16  import java.time.OffsetDateTime;
17  import java.time.format.DateTimeFormatter;
18  import java.time.format.DateTimeFormatterBuilder;
19  import java.time.format.DateTimeParseException;
20  import java.time.temporal.ChronoField;
21  import java.util.ArrayList;
22  import java.util.Arrays;
23  import java.util.EnumMap;
24  import java.util.EnumSet;
25  import java.util.HashMap;
26  import java.util.LinkedHashMap;
27  import java.util.List;
28  import java.util.Locale;
29  import java.util.Map;
30  import java.util.TimeZone;
31  import java.util.regex.Matcher;
32  import java.util.regex.Pattern;
33  
34  import org.slf4j.Logger;
35  import org.slf4j.LoggerFactory;
36  
37  import oshi.annotation.SuppressForbidden;
38  import oshi.annotation.concurrent.ThreadSafe;
39  import oshi.util.tuples.Pair;
40  import oshi.util.tuples.Triplet;
41  
42  /**
43   * String parsing utility.
44   */
45  @ThreadSafe
46  @SuppressForbidden(reason = "Require parse methods to parse in utility class")
47  public final class ParseUtil {
48  
49      private static final Logger LOG = LoggerFactory.getLogger(ParseUtil.class);
50  
51      private static final String DEFAULT_LOG_MSG = "{} didn't parse. Returning default. {}";
52  
53      /*
54       * Used for matching
55       */
56      private static final Pattern HERTZ_PATTERN = Pattern.compile("(\\d+(.\\d+)?) ?([kKMGT]?Hz).*");
57      private static final Pattern BYTES_PATTERN = Pattern.compile("(\\d+) ?([kKMGT]?B?).*");
58      private static final Pattern UNITS_PATTERN = Pattern.compile("(\\d+(.\\d+)?)[\\s]?([kKMGT])?");
59  
60      /*
61       * Used to check validity of a hexadecimal string
62       */
63      private static final Pattern VALID_HEX = Pattern.compile("[0-9a-fA-F]+");
64  
65      /*
66       * Pattern for [dd-[hh:[mm:[ss[.sss]]]]]
67       */
68      private static final Pattern DHMS = Pattern.compile("(?:(\\d+)-)?(?:(\\d+):)??(?:(\\d+):)?(\\d+)(?:\\.(\\d+))?");
69  
70      /*
71       * Pattern for a UUID
72       */
73      private static final Pattern UUID_PATTERN = Pattern
74              .compile(".*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}).*");
75  
76      /*
77       * Pattern for Windows DeviceID vendor and product ID and serial
78       */
79      private static final Pattern VENDOR_PRODUCT_ID_SERIAL = Pattern
80              .compile(".*(?:VID|VEN)_(\\p{XDigit}{4})&(?:PID|DEV)_(\\p{XDigit}{4})(.*)\\\\(.*)");
81  
82      /*
83       * Pattern for Linux lspci machine readable
84       */
85      private static final Pattern LSPCI_MACHINE_READABLE = Pattern.compile("(.+)\\s\\[(.*?)\\]");
86  
87      /*
88       * Pattern for Linux lspci memory
89       */
90      private static final Pattern LSPCI_MEMORY_SIZE = Pattern.compile(".+\\s\\[size=(\\d+)([kKMGT])\\]");
91  
92      /*
93       * Hertz related variables.
94       */
95      private static final String HZ = "Hz";
96      private static final String KHZ = "kHz";
97      private static final String MHZ = "MHz";
98      private static final String GHZ = "GHz";
99      private static final String THZ = "THz";
100     private static final String PHZ = "PHz";
101 
102     private static final Map<String, Long> multipliers;
103 
104     // PDH timestamps are 1601 epoch, local time
105     // Constants to convert to UTC millis
106     private static final long EPOCH_DIFF = 11_644_473_600_000L;
107     private static final int TZ_OFFSET = TimeZone.getDefault().getOffset(System.currentTimeMillis());
108 
109     /** Constant <code>whitespacesColonWhitespace</code> */
110     public static final Pattern whitespacesColonWhitespace = Pattern.compile("\\s+:\\s");
111 
112     /** Constant <code>whitespaces</code> */
113     public static final Pattern whitespaces = Pattern.compile("\\s+");
114 
115     /** Constant <code>notDigits</code> */
116     public static final Pattern notDigits = Pattern.compile("[^0-9]+");
117 
118     /** Constant <code>startWithNotDigits</code> */
119     public static final Pattern startWithNotDigits = Pattern.compile("^[^0-9]*");
120 
121     /** Constant <code>forwardSlash</code> */
122     public static final Pattern slash = Pattern.compile("\\/");
123 
124     static {
125         multipliers = new HashMap<>();
126         multipliers.put(HZ, 1L);
127         multipliers.put(KHZ, 1_000L);
128         multipliers.put(MHZ, 1_000_000L);
129         multipliers.put(GHZ, 1_000_000_000L);
130         multipliers.put(THZ, 1_000_000_000_000L);
131         multipliers.put(PHZ, 1_000_000_000_000_000L);
132     }
133 
134     // Fast decimal exponentiation: pow(10,y) --> POWERS_OF_10[y]
135     private static final long[] POWERS_OF_TEN = { 1L, 10L, 100L, 1_000L, 10_000L, 100_000L, 1_000_000L, 10_000_000L,
136             100_000_000L, 1_000_000_000L, 10_000_000_000L, 100_000_000_000L, 1_000_000_000_000L, 10_000_000_000_000L,
137             100_000_000_000_000L, 1_000_000_000_000_000L, 10_000_000_000_000_000L, 100_000_000_000_000_000L,
138             1_000_000_000_000_000_000L };
139 
140     // Format returned by WMI for DateTime
141     private static final DateTimeFormatter CIM_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss.SSSSSSZZZZZ",
142             Locale.US);
143 
144     private ParseUtil() {
145     }
146 
147     /**
148      * Parse speed from a string, eg. "2.00 MT/s" is 2000000L.
149      *
150      * @param speed Transfer speed.
151      * @return {@link java.lang.Long} MT/s value. If not parseable, delegates to {#link {@link #parseHertz(String)}.
152      */
153     public static long parseSpeed(String speed) {
154         if (speed.contains("T/s")) {
155             return parseHertz(speed.replace("T/s", "Hz"));
156         }
157         return parseHertz(speed);
158     }
159 
160     /**
161      * Parse hertz from a string, eg. "2.00MHz" is 2000000L.
162      *
163      * @param hertz Hertz size.
164      * @return {@link java.lang.Long} Hertz value or -1 if not parseable.
165      */
166     public static long parseHertz(String hertz) {
167         Matcher matcher = HERTZ_PATTERN.matcher(hertz.trim());
168         if (matcher.find()) {
169             // Regexp enforces #(.#) format so no test for NFE required
170             double value = Double.valueOf(matcher.group(1)) * multipliers.getOrDefault(matcher.group(3), -1L);
171             if (value >= 0d) {
172                 return (long) value;
173             }
174         }
175         return -1L;
176     }
177 
178     /**
179      * Parse the last element of a space-delimited string to a value
180      *
181      * @param s The string to parse
182      * @param i Default integer if not parsable
183      * @return value or the given default if not parsable
184      */
185     public static int parseLastInt(String s, int i) {
186         try {
187             String ls = parseLastString(s);
188             if (ls.toLowerCase(Locale.ROOT).startsWith("0x")) {
189                 return Integer.decode(ls);
190             } else {
191                 return Integer.parseInt(ls);
192             }
193         } catch (NumberFormatException e) {
194             LOG.trace(DEFAULT_LOG_MSG, s, e);
195             return i;
196         }
197     }
198 
199     /**
200      * Parse the last element of a space-delimited string to a value
201      *
202      * @param s  The string to parse
203      * @param li Default long integer if not parsable
204      * @return value or the given default if not parsable
205      */
206     public static long parseLastLong(String s, long li) {
207         try {
208             String ls = parseLastString(s);
209             if (ls.toLowerCase(Locale.ROOT).startsWith("0x")) {
210                 return Long.decode(ls);
211             } else {
212                 return Long.parseLong(ls);
213             }
214         } catch (NumberFormatException e) {
215             LOG.trace(DEFAULT_LOG_MSG, s, e);
216             return li;
217         }
218     }
219 
220     /**
221      * Parse the last element of a space-delimited string to a value
222      *
223      * @param s The string to parse
224      * @param d Default double if not parsable
225      * @return value or the given default if not parsable
226      */
227     public static double parseLastDouble(String s, double d) {
228         try {
229             return Double.parseDouble(parseLastString(s));
230         } catch (NumberFormatException e) {
231             LOG.trace(DEFAULT_LOG_MSG, s, e);
232             return d;
233         }
234     }
235 
236     /**
237      * Parse the last element of a space-delimited string to a string
238      *
239      * @param s The string to parse
240      * @return last space-delimited element
241      */
242     public static String parseLastString(String s) {
243         String[] ss = whitespaces.split(s);
244         // guaranteed at least one element
245         return ss[ss.length - 1];
246     }
247 
248     /**
249      * Parse a byte array into a string of hexadecimal digits including all array bytes as digits
250      *
251      * @param bytes The byte array to represent
252      * @return A string of hex characters corresponding to the bytes. The string is upper case.
253      */
254     public static String byteArrayToHexString(byte[] bytes) {
255         StringBuilder sb = new StringBuilder(bytes.length * 2);
256         for (byte b : bytes) {
257             sb.append(Character.forDigit((b & 0xf0) >>> 4, 16));
258             sb.append(Character.forDigit(b & 0x0f, 16));
259         }
260         return sb.toString().toUpperCase(Locale.ROOT);
261     }
262 
263     /**
264      * Parse a string of hexadecimal digits into a byte array
265      *
266      * @param digits The string to be parsed
267      * @return a byte array with each pair of characters converted to a byte, or empty array if the string is not valid
268      *         hex
269      */
270     public static byte[] hexStringToByteArray(String digits) {
271         int len = digits.length();
272         // Check if string is valid hex
273         if (!VALID_HEX.matcher(digits).matches() || (len & 0x1) != 0) {
274             LOG.warn("Invalid hexadecimal string: {}", digits);
275             return new byte[0];
276         }
277         byte[] data = new byte[len / 2];
278         for (int i = 0; i < len; i += 2) {
279             data[i / 2] = (byte) (Character.digit(digits.charAt(i), 16) << 4
280                     | Character.digit(digits.charAt(i + 1), 16));
281         }
282         return data;
283     }
284 
285     /**
286      * Parse a human readable ASCII string into a byte array, truncating or padding with zeros (if necessary) so the
287      * array has the specified length.
288      *
289      * @param text   The string to be parsed
290      * @param length Length of the returned array.
291      * @return A byte array of specified length, with each of the first length characters converted to a byte. If length
292      *         is longer than the provided string length, will be filled with zeroes.
293      */
294     public static byte[] asciiStringToByteArray(String text, int length) {
295         return Arrays.copyOf(text.getBytes(StandardCharsets.US_ASCII), length);
296     }
297 
298     /**
299      * Convert a long value to a byte array using Big Endian, truncating or padding with zeros (if necessary) so the
300      * array has the specified length.
301      *
302      * @param value     The value to be converted
303      * @param valueSize Number of bytes representing the value
304      * @param length    Number of bytes to return
305      * @return A byte array of specified length representing the long in the first valueSize bytes
306      */
307     public static byte[] longToByteArray(long value, int valueSize, int length) {
308         long val = value;
309         // Convert the long to 8-byte BE representation
310         byte[] b = new byte[8];
311         for (int i = 7; i >= 0 && val != 0L; i--) {
312             b[i] = (byte) val;
313             val >>>= 8;
314         }
315         // Then copy the rightmost valueSize bytes
316         // e.g., for an integer we want rightmost 4 bytes
317         return Arrays.copyOfRange(b, 8 - valueSize, 8 + length - valueSize);
318     }
319 
320     /**
321      * Convert a string to an integer representation.
322      *
323      * @param str  A human readable ASCII string
324      * @param size Number of characters to convert to the long. May not exceed 8.
325      * @return An integer representing the string where each character is treated as a byte
326      */
327     public static long strToLong(String str, int size) {
328         return byteArrayToLong(str.getBytes(StandardCharsets.US_ASCII), size);
329     }
330 
331     /**
332      * Convert a byte array to its (long) integer representation assuming big endian ordering.
333      *
334      * @param bytes An array of bytes no smaller than the size to be converted
335      * @param size  Number of bytes to convert to the long. May not exceed 8.
336      * @return A long integer representing the byte array
337      */
338     public static long byteArrayToLong(byte[] bytes, int size) {
339         return byteArrayToLong(bytes, size, true);
340     }
341 
342     /**
343      * Convert a byte array to its (long) integer representation in the specified endianness.
344      *
345      * @param bytes     An array of bytes no smaller than the size to be converted
346      * @param size      Number of bytes to convert to the long. May not exceed 8.
347      * @param bigEndian True to parse big-endian, false to parse little-endian
348      * @return An long integer representing the byte array
349      */
350     public static long byteArrayToLong(byte[] bytes, int size, boolean bigEndian) {
351         if (size > 8) {
352             throw new IllegalArgumentException("Can't convert more than 8 bytes.");
353         }
354         if (size > bytes.length) {
355             throw new IllegalArgumentException("Size can't be larger than array length.");
356         }
357         long total = 0L;
358         for (int i = 0; i < size; i++) {
359             if (bigEndian) {
360                 total = total << 8 | bytes[i] & 0xff;
361             } else {
362                 total = total << 8 | bytes[size - i - 1] & 0xff;
363             }
364         }
365         return total;
366     }
367 
368     /**
369      * Convert a byte array to its floating point representation.
370      *
371      * @param bytes  An array of bytes no smaller than the size to be converted
372      * @param size   Number of bytes to convert to the float. May not exceed 8.
373      * @param fpBits Number of bits representing the decimal
374      * @return A float; the integer portion representing the byte array as an integer shifted by the bits specified in
375      *         fpBits; with the remaining bits used as a decimal
376      */
377     public static float byteArrayToFloat(byte[] bytes, int size, int fpBits) {
378         return byteArrayToLong(bytes, size) / (float) (1 << fpBits);
379     }
380 
381     /**
382      * Convert an unsigned integer to a long value. The method assumes that all bits in the specified integer value are
383      * 'data' bits, including the most-significant bit which Java normally considers a sign bit. The method must be used
384      * only when it is certain that the integer value represents an unsigned integer, for example when the integer is
385      * returned by JNA library in a structure which holds unsigned integers.
386      *
387      * @param unsignedValue The unsigned integer value to convert.
388      * @return The unsigned integer value widened to a long.
389      */
390     public static long unsignedIntToLong(int unsignedValue) {
391         // use standard Java widening conversion to long which does
392         // sign-extension,
393         // then drop any copies of the sign bit, to prevent the value being
394         // considered a negative one by Java if it is set
395         long longValue = unsignedValue;
396         return longValue & 0xffff_ffffL;
397     }
398 
399     /**
400      * Convert an unsigned long to a signed long value by stripping the sign bit. This method "rolls over" long values
401      * greater than the max value but ensures the result is never negative.
402      *
403      * @param unsignedValue The unsigned long value to convert.
404      * @return The signed long value.
405      */
406     public static long unsignedLongToSignedLong(long unsignedValue) {
407         return unsignedValue & 0x7fff_ffff_ffff_ffffL;
408     }
409 
410     /**
411      * Parses a string of hex digits to a string where each pair of hex digits represents an ASCII character
412      *
413      * @param hexString A sequence of hex digits
414      * @return The corresponding string if valid hex; otherwise the original hexString
415      */
416     public static String hexStringToString(String hexString) {
417         // Odd length strings won't parse, return
418         if (hexString.length() % 2 > 0) {
419             return hexString;
420         }
421         int charAsInt;
422         StringBuilder sb = new StringBuilder();
423         try {
424             for (int pos = 0; pos < hexString.length(); pos += 2) {
425                 charAsInt = Integer.parseInt(hexString.substring(pos, pos + 2), 16);
426                 if (charAsInt < 32 || charAsInt > 127) {
427                     return hexString;
428                 }
429                 sb.append((char) charAsInt);
430             }
431         } catch (NumberFormatException e) {
432             LOG.trace(DEFAULT_LOG_MSG, hexString, e);
433             // Hex failed to parse, just return the existing string
434             return hexString;
435         }
436         return sb.toString();
437     }
438 
439     /**
440      * Attempts to parse a string to an int. If it fails, returns the default
441      *
442      * @param s          The string to parse
443      * @param defaultInt The value to return if parsing fails
444      * @return The parsed int, or the default if parsing failed
445      */
446     public static int parseIntOrDefault(String s, int defaultInt) {
447         try {
448             return Integer.parseInt(s);
449         } catch (NumberFormatException e) {
450             LOG.trace(DEFAULT_LOG_MSG, s, e);
451             return defaultInt;
452         }
453     }
454 
455     /**
456      * Attempts to parse a string to a long. If it fails, returns the default
457      *
458      * @param s           The string to parse
459      * @param defaultLong The value to return if parsing fails
460      * @return The parsed long, or the default if parsing failed
461      */
462     public static long parseLongOrDefault(String s, long defaultLong) {
463         try {
464             return Long.parseLong(s);
465         } catch (NumberFormatException e) {
466             LOG.trace(DEFAULT_LOG_MSG, s, e);
467             return defaultLong;
468         }
469     }
470 
471     /**
472      * Attempts to parse a string to an "unsigned" long. If it fails, returns the default
473      *
474      * @param s           The string to parse
475      * @param defaultLong The value to return if parsing fails
476      * @return The parsed long containing the same 64 bits that an unsigned long would contain (which may produce a
477      *         negative value)
478      */
479     public static long parseUnsignedLongOrDefault(String s, long defaultLong) {
480         try {
481             return new BigInteger(s).longValue();
482         } catch (NumberFormatException e) {
483             LOG.trace(DEFAULT_LOG_MSG, s, e);
484             return defaultLong;
485         }
486     }
487 
488     /**
489      * Attempts to parse a string to a double. If it fails, returns the default
490      *
491      * @param s             The string to parse
492      * @param defaultDouble The value to return if parsing fails
493      * @return The parsed double, or the default if parsing failed
494      */
495     public static double parseDoubleOrDefault(String s, double defaultDouble) {
496         try {
497             return Double.parseDouble(s);
498         } catch (NumberFormatException e) {
499             LOG.trace(DEFAULT_LOG_MSG, s, e);
500             return defaultDouble;
501         }
502     }
503 
504     /**
505      * Attempts to parse a string of the form [DD-[hh:]]mm:ss[.ddd] to a number of milliseconds. If it fails, returns
506      * the default.
507      *
508      * @param s           The string to parse
509      * @param defaultLong The value to return if parsing fails
510      * @return The parsed number of seconds, or the default if parsing fails
511      */
512     public static long parseDHMSOrDefault(String s, long defaultLong) {
513         Matcher m = DHMS.matcher(s);
514         if (m.matches()) {
515             long milliseconds = 0L;
516             if (m.group(1) != null) {
517                 milliseconds += parseLongOrDefault(m.group(1), 0L) * 86_400_000L;
518             }
519             if (m.group(2) != null) {
520                 milliseconds += parseLongOrDefault(m.group(2), 0L) * 3_600_000L;
521             }
522             if (m.group(3) != null) {
523                 milliseconds += parseLongOrDefault(m.group(3), 0L) * 60_000L;
524             }
525             milliseconds += parseLongOrDefault(m.group(4), 0L) * 1000L;
526             if (m.group(5) != null) {
527                 milliseconds += (long) (1000 * parseDoubleOrDefault("0." + m.group(5), 0d));
528             }
529             return milliseconds;
530         }
531         return defaultLong;
532     }
533 
534     /**
535      * Attempts to parse a UUID. If it fails, returns the default.
536      *
537      * @param s          The string to parse
538      * @param defaultStr The value to return if parsing fails
539      * @return The parsed UUID, or the default if parsing fails
540      */
541     public static String parseUuidOrDefault(String s, String defaultStr) {
542         Matcher m = UUID_PATTERN.matcher(s.toLowerCase(Locale.ROOT));
543         if (m.matches()) {
544             return m.group(1);
545         }
546         return defaultStr;
547     }
548 
549     /**
550      * Parses a string key = 'value' (string)
551      *
552      * @param line The entire string
553      * @return the value contained between single tick marks
554      */
555     public static String getSingleQuoteStringValue(String line) {
556         return getStringBetween(line, '\'');
557     }
558 
559     /**
560      * Parse a string key = "value" (string)
561      *
562      * @param line the entire string
563      * @return the value contained between double tick marks
564      */
565     public static String getDoubleQuoteStringValue(String line) {
566         return getStringBetween(line, '"');
567     }
568 
569     /**
570      * Gets a value between two characters having multiple same characters between them. <b>Examples : </b>
571      * <ul>
572      * <li>"name = 'James Gosling's Java'" returns "James Gosling's Java"</li>
573      * <li>"pci.name = 'Realtek AC'97 Audio Device'" returns "Realtek AC'97 Audio Device"</li>
574      * </ul>
575      *
576      * @param line The "key-value" pair line.
577      * @param c    The Trailing And Leading characters of the string line
578      * @return : The value having the characters between them.
579      */
580     public static String getStringBetween(String line, char c) {
581         int firstOcc = line.indexOf(c);
582         if (firstOcc < 0) {
583             return "";
584         }
585         return line.substring(firstOcc + 1, line.lastIndexOf(c)).trim();
586     }
587 
588     /**
589      * Parses a string such as "10.12.2" or "key = 1 (0x1) (int)" to find the integer value of the first set of one or
590      * more consecutive digits
591      *
592      * @param line The entire string
593      * @return the value of first integer if any; 0 otherwise
594      */
595     public static int getFirstIntValue(String line) {
596         return getNthIntValue(line, 1);
597     }
598 
599     /**
600      * Parses a string such as "10.12.2" or "key = 1 (0x1) (int)" to find the integer value of the nth set of one or
601      * more consecutive digits
602      *
603      * @param line The entire string
604      * @param n    Which set of integers to return
605      * @return the value of nth integer if any; 0 otherwise
606      */
607     public static int getNthIntValue(String line, int n) {
608         // Split the string by non-digits,
609         String[] split = notDigits.split(startWithNotDigits.matcher(line).replaceFirst(""));
610         if (split.length >= n) {
611             return parseIntOrDefault(split[n - 1], 0);
612         }
613         return 0;
614     }
615 
616     /**
617      * Removes all matching sub strings from the string. More efficient than regexp.
618      *
619      * @param original source String to remove from
620      * @param toRemove the sub string to be removed
621      * @return The string with all matching substrings removed
622      */
623     public static String removeMatchingString(final String original, final String toRemove) {
624         if (original == null || original.isEmpty() || toRemove == null || toRemove.isEmpty()) {
625             return original;
626         }
627 
628         int matchIndex = original.indexOf(toRemove, 0);
629         if (matchIndex == -1) {
630             return original;
631         }
632 
633         StringBuilder buffer = new StringBuilder(original.length() - toRemove.length());
634         int currIndex = 0;
635         do {
636             buffer.append(original.substring(currIndex, matchIndex));
637             currIndex = matchIndex + toRemove.length();
638             matchIndex = original.indexOf(toRemove, currIndex);
639         } while (matchIndex != -1);
640 
641         buffer.append(original.substring(currIndex));
642         return buffer.toString();
643     }
644 
645     /**
646      * Parses a delimited string to an array of longs. Optimized for processing predictable-length arrays such as
647      * outputs of reliably formatted Linux proc or sys filesystem, minimizing new object creation. Users should perform
648      * other sanity checks of data.
649      *
650      * As a special case, non-numeric fields (such as UUIDs in OpenVZ) at the end of the list are ignored. Values
651      * greater than the max long value return the max long value.
652      *
653      * The indices parameters are referenced assuming the length as specified, and leading characters are ignored. For
654      * example, if the string is "foo 12 34 5" and the length is 3, then index 0 is 12, index 1 is 34, and index 2 is 5.
655      *
656      * @param s         The string to parse
657      * @param indices   An array indicating which indexes should be populated in the final array; other values will be
658      *                  skipped. This idex is zero-referenced assuming the rightmost delimited fields of the string
659      *                  contain the array.
660      * @param length    The total number of elements in the string array. It is permissible for the string to have more
661      *                  elements than this; leading elements will be ignored. This should be calculated once per text
662      *                  format by {@link #countStringToLongArray}.
663      * @param delimiter The character to delimit by.
664      * @return If successful, an array of parsed longs. If parsing errors occurred, will be an array of zeros.
665      */
666     public static long[] parseStringToLongArray(String s, int[] indices, int length, char delimiter) {
667         // Ensure that the last character is a number
668         s = s.trim();
669 
670         long[] parsed = new long[indices.length];
671         // Iterate from right-to-left of String
672         // Fill right to left of result array using index array
673         int charIndex = s.length();
674         int parsedIndex = indices.length - 1;
675         int stringIndex = length - 1;
676 
677         int power = 0;
678         int c;
679         boolean delimCurrent = false;
680         boolean numeric = true;
681         boolean numberFound = false; // ignore nonnumeric at end
682         boolean dashSeen = false; // to flag uuids as nonnumeric
683         while (--charIndex >= 0 && parsedIndex >= 0) {
684             c = s.charAt(charIndex);
685             if (c == delimiter) {
686                 // first parseable number?
687                 if (!numberFound && numeric) {
688                     numberFound = true;
689                 }
690                 if (!delimCurrent) {
691                     if (numberFound && indices[parsedIndex] == stringIndex--) {
692                         parsedIndex--;
693                     }
694                     delimCurrent = true;
695                     power = 0;
696                     dashSeen = false;
697                     numeric = true;
698                 }
699             } else if (indices[parsedIndex] != stringIndex || c == '+' || !numeric) {
700                 // Doesn't impact parsing, ignore
701                 delimCurrent = false;
702             } else if (c >= '0' && c <= '9' && !dashSeen) {
703                 if (power > 18 || power == 17 && c == '9' && parsed[parsedIndex] > 223_372_036_854_775_807L) {
704                     parsed[parsedIndex] = Long.MAX_VALUE;
705                 } else {
706                     parsed[parsedIndex] += (c - '0') * ParseUtil.POWERS_OF_TEN[power++];
707                 }
708                 delimCurrent = false;
709             } else if (c == '-') {
710                 parsed[parsedIndex] *= -1L;
711                 delimCurrent = false;
712                 dashSeen = true;
713             } else {
714                 // Flag as nonnumeric and continue unless we've seen a numeric
715                 // error on everything else
716                 if (numberFound) {
717                     if (!noLog(s)) {
718                         LOG.error("Illegal character parsing string '{}' to long array: {}", s, s.charAt(charIndex));
719                     }
720                     return new long[indices.length];
721                 }
722                 parsed[parsedIndex] = 0;
723                 numeric = false;
724             }
725         }
726         if (parsedIndex > 0) {
727             if (!noLog(s)) {
728                 LOG.error("Not enough fields in string '{}' parsing to long array: {}", s,
729                         indices.length - parsedIndex);
730             }
731             return new long[indices.length];
732         }
733         return parsed;
734     }
735 
736     /**
737      * Test whether to log this message
738      *
739      * @param s The string to log
740      * @return True if the string begins with {@code NOLOG}
741      */
742     private static boolean noLog(String s) {
743         return s.startsWith("NOLOG: ");
744     }
745 
746     /**
747      * Parses a delimited string to count elements of an array of longs. Intended to be called once to calculate the
748      * {@code length} field for {@link #parseStringToLongArray}.
749      *
750      * As a special case, non-numeric fields (such as UUIDs in OpenVZ) at the end of the list are ignored.
751      *
752      * @param s         The string to parse
753      * @param delimiter The character to delimit by
754      * @return The number of parsable long values which follow the last unparsable value.
755      */
756     public static int countStringToLongArray(String s, char delimiter) {
757         // Ensure that the last character is a number
758         s = s.trim();
759 
760         // Iterate from right-to-left of String
761         // Fill right to left of result array using index array
762         int charIndex = s.length();
763         int numbers = 0;
764 
765         int c;
766         boolean delimCurrent = false;
767         boolean numeric = true;
768         boolean dashSeen = false; // to flag uuids as nonnumeric
769         while (--charIndex >= 0) {
770             c = s.charAt(charIndex);
771             if (c == delimiter) {
772                 if (!delimCurrent) {
773                     if (numeric) {
774                         numbers++;
775                     }
776                     delimCurrent = true;
777                     dashSeen = false;
778                     numeric = true;
779                 }
780             } else if (c == '+' || !numeric) {
781                 // Doesn't impact parsing, ignore
782                 delimCurrent = false;
783             } else if (c >= '0' && c <= '9' && !dashSeen) {
784                 delimCurrent = false;
785             } else if (c == '-') {
786                 delimCurrent = false;
787                 dashSeen = true;
788             } else {
789                 // we found non-digit or delimiter. If not last field, exit
790                 if (numbers > 0) {
791                     return numbers;
792                 }
793                 // Else flag as nonnumeric and continue
794                 numeric = false;
795             }
796         }
797         // We got to beginning of string with only numbers, count start as a delimiter
798         // and exit
799         return numbers + 1;
800     }
801 
802     /**
803      * Get a String in a line of text between two marker strings
804      *
805      * @param text   Text to search for match
806      * @param before Start matching after this text
807      * @param after  End matching before this text
808      * @return Text between the strings before and after, or empty string if either marker does not exist
809      */
810     public static String getTextBetweenStrings(String text, String before, String after) {
811 
812         String result = "";
813 
814         if (text.indexOf(before) >= 0 && text.indexOf(after) >= 0) {
815             result = text.substring(text.indexOf(before) + before.length(), text.length());
816             result = result.substring(0, result.indexOf(after));
817         }
818         return result;
819     }
820 
821     /**
822      * Convert a long representing filetime (100-ns since 1601 epoch) to ms since 1970 epoch
823      *
824      * @param filetime A 64-bit value equivalent to FILETIME
825      * @param local    True if converting from a local filetime (PDH counter); false if already UTC (WMI PerfRawData
826      *                 classes)
827      * @return Equivalent milliseconds since the epoch
828      */
829     public static long filetimeToUtcMs(long filetime, boolean local) {
830         return filetime / 10_000L - EPOCH_DIFF - (local ? TZ_OFFSET : 0L);
831     }
832 
833     /**
834      * Parse a date in MM-DD-YYYY or MM/DD/YYYY to YYYY-MM-DD
835      *
836      * @param dateString The date in MM DD YYYY format
837      * @return The date in ISO YYYY-MM-DD format if parseable, or the original string
838      */
839     public static String parseMmDdYyyyToYyyyMmDD(String dateString) {
840         try {
841             // Date is MM-DD-YYYY, convert to YYYY-MM-DD
842             return String.format(Locale.ROOT, "%s-%s-%s", dateString.substring(6, 10), dateString.substring(0, 2),
843                     dateString.substring(3, 5));
844         } catch (StringIndexOutOfBoundsException e) {
845             return dateString;
846         }
847     }
848 
849     /**
850      * Converts a string in CIM Date Format, as returned by WMI for DateTime types, into a
851      * {@link java.time.OffsetDateTime}.
852      *
853      * @param cimDateTime A non-null DateTime String in CIM date format, e.g., <code>20160513072950.782000-420</code>
854      * @return The parsed {@link java.time.OffsetDateTime} if the string is parsable, otherwise
855      *         {@link oshi.util.Constants#UNIX_EPOCH}.
856      */
857     public static OffsetDateTime parseCimDateTimeToOffset(String cimDateTime) {
858         // Keep first 22 characters: digits, decimal, and + or - sign
859         // But alter last 3 characters from a minute offset to hh:mm
860         try {
861             // From WMI as 20160513072950.782000-420,
862             int tzInMinutes = Integer.parseInt(cimDateTime.substring(22));
863             // modified to 20160513072950.782000-07:00 which can be parsed
864             LocalTime offsetAsLocalTime = LocalTime.MIDNIGHT.plusMinutes(tzInMinutes);
865             return OffsetDateTime.parse(
866                     cimDateTime.substring(0, 22) + offsetAsLocalTime.format(DateTimeFormatter.ISO_LOCAL_TIME),
867                     ParseUtil.CIM_FORMAT);
868         } catch (IndexOutOfBoundsException // if cimDate not 22+ chars
869                 | NumberFormatException // if TZ minutes doesn't parse
870                 | DateTimeParseException e) {
871             LOG.trace("Unable to parse {} to CIM DateTime.", cimDateTime);
872             return Constants.UNIX_EPOCH;
873         }
874     }
875 
876     /**
877      * Checks if a file path equals or starts with an prefix in the given list
878      *
879      * @param prefixList A list of path prefixes
880      * @param path       a string path to check
881      * @return true if the path exactly equals, or starts with one of the strings in prefixList
882      */
883     public static boolean filePathStartsWith(List<String> prefixList, String path) {
884         for (String match : prefixList) {
885             if (path.equals(match) || path.startsWith(match + "/")) {
886                 return true;
887             }
888         }
889         return false;
890     }
891 
892     /**
893      * Parses a string like "53G" or "54.904 M" to its long value.
894      *
895      * @param count A count with a multiplyer like "4096 M"
896      * @return the count parsed to a long
897      */
898     public static long parseMultipliedToLongs(String count) {
899         Matcher matcher = UNITS_PATTERN.matcher(count.trim());
900         String[] mem;
901         if (matcher.find() && matcher.groupCount() == 3) {
902             mem = new String[2];
903             mem[0] = matcher.group(1);
904             mem[1] = matcher.group(3);
905         } else {
906             mem = new String[] { count };
907         }
908 
909         double number = ParseUtil.parseDoubleOrDefault(mem[0], 0L);
910         if (mem.length == 2 && mem[1] != null && mem[1].length() >= 1) {
911             switch (mem[1].charAt(0)) {
912             case 'T':
913                 number *= 1_000_000_000_000L;
914                 break;
915             case 'G':
916                 number *= 1_000_000_000L;
917                 break;
918             case 'M':
919                 number *= 1_000_000L;
920                 break;
921             case 'K':
922             case 'k':
923                 number *= 1_000L;
924                 break;
925             default:
926             }
927         }
928         return (long) number;
929     }
930 
931     /**
932      * Parses a string such as "4096 MB" to its long. Used to parse macOS and *nix memory chip sizes. Although the units
933      * given are decimal they must parse to binary units.
934      *
935      * @param size A string of memory sizes like "4096 MB"
936      * @return the size parsed to a long
937      */
938     public static long parseDecimalMemorySizeToBinary(String size) {
939         String[] mem = ParseUtil.whitespaces.split(size);
940         if (mem.length < 2) {
941             // If no spaces, use regexp
942             Matcher matcher = BYTES_PATTERN.matcher(size.trim());
943             if (matcher.find() && matcher.groupCount() == 2) {
944                 mem = new String[2];
945                 mem[0] = matcher.group(1);
946                 mem[1] = matcher.group(2);
947             }
948         }
949         long capacity = ParseUtil.parseLongOrDefault(mem[0], 0L);
950         if (mem.length == 2 && mem[1].length() > 1) {
951             switch (mem[1].charAt(0)) {
952             case 'T':
953                 capacity <<= 40;
954                 break;
955             case 'G':
956                 capacity <<= 30;
957                 break;
958             case 'M':
959                 capacity <<= 20;
960                 break;
961             case 'K':
962             case 'k':
963                 capacity <<= 10;
964                 break;
965             default:
966                 break;
967             }
968         }
969         return capacity;
970     }
971 
972     /**
973      * Parse a Windows DeviceID to get the vendor ID, product ID, and Serial Number
974      *
975      * @param deviceId The DeviceID
976      * @return A {@link Triplet} where the first element is the vendor ID, the second element is the product ID, and the
977      *         third element is either a serial number or empty string if parsing was successful, or {@code null}
978      *         otherwise
979      */
980     public static Triplet<String, String, String> parseDeviceIdToVendorProductSerial(String deviceId) {
981         Matcher m = VENDOR_PRODUCT_ID_SERIAL.matcher(deviceId);
982         if (m.matches()) {
983             String vendorId = "0x" + m.group(1).toLowerCase(Locale.ROOT);
984             String productId = "0x" + m.group(2).toLowerCase(Locale.ROOT);
985             String serial = m.group(4);
986             return new Triplet<>(vendorId, productId, !m.group(3).isEmpty() || serial.contains("&") ? "" : serial);
987         }
988         return null;
989     }
990 
991     /**
992      * Parse a Linux lshw resources string to calculate the memory size
993      *
994      * @param resources A string containing one or more elements of the form {@code memory:b00000000-bffffffff}
995      * @return The number of bytes consumed by the memory in the {@code resources} string
996      */
997     public static long parseLshwResourceString(String resources) {
998         long bytes = 0L;
999         // First split by whitespace
1000         String[] resourceArray = whitespaces.split(resources);
1001         for (String r : resourceArray) {
1002             // Remove prefix
1003             if (r.startsWith("memory:")) {
1004                 // Split to low and high
1005                 String[] mem = r.substring(7).split("-");
1006                 if (mem.length == 2) {
1007                     try {
1008                         // Parse the hex strings
1009                         bytes += Long.parseLong(mem[1], 16) - Long.parseLong(mem[0], 16) + 1;
1010                     } catch (NumberFormatException e) {
1011                         LOG.trace(DEFAULT_LOG_MSG, r, e);
1012                     }
1013                 }
1014             }
1015         }
1016         return bytes;
1017     }
1018 
1019     /**
1020      * Parse a Linux lspci machine readble line to its name and id
1021      *
1022      * @param line A string in the form Foo [bar]
1023      * @return A pair separating the String before the square brackets and within them if found, null otherwise
1024      */
1025     public static Pair<String, String> parseLspciMachineReadable(String line) {
1026         Matcher matcher = LSPCI_MACHINE_READABLE.matcher(line);
1027         if (matcher.matches()) {
1028             return new Pair<>(matcher.group(1), matcher.group(2));
1029         }
1030         return null;
1031     }
1032 
1033     /**
1034      * Parse a Linux lspci line containing memory size
1035      *
1036      * @param line A string in the form Foo [size=256M]
1037      * @return A the memory size in bytes
1038      */
1039     public static long parseLspciMemorySize(String line) {
1040         Matcher matcher = LSPCI_MEMORY_SIZE.matcher(line);
1041         if (matcher.matches()) {
1042             return parseDecimalMemorySizeToBinary(matcher.group(1) + " " + matcher.group(2) + "B");
1043         }
1044         return 0;
1045     }
1046 
1047     /**
1048      * Parse a space-delimited list of integers which include hyphenated ranges to a list of just the integers. For
1049      * example, 0 1 4-7 parses to a list containing 0, 1, 4, 5, 6, and 7. Also support comma separated entries like 0,
1050      * 2-5, 7-8, 9 to a list containing 0, 2, 3, 4, 5, 7, 8, 9.
1051      *
1052      * @param str A string containing space-delimited integers or ranges of integers with a hyphen
1053      * @return A list of integers representing the provided range(s).
1054      */
1055     public static List<Integer> parseHyphenatedIntList(String str) {
1056         List<Integer> result = new ArrayList<>();
1057         String[] csvTokens = str.split(",");
1058         for (String csvToken : csvTokens) {
1059             csvToken = csvToken.trim();
1060             for (String s : whitespaces.split(csvToken)) {
1061                 if (s.contains("-")) {
1062                     int first = getFirstIntValue(s);
1063                     int last = getNthIntValue(s, 2);
1064                     for (int i = first; i <= last; i++) {
1065                         result.add(i);
1066                     }
1067                 } else {
1068                     int only = ParseUtil.parseIntOrDefault(s, -1);
1069                     if (only >= 0) {
1070                         result.add(only);
1071                     }
1072                 }
1073             }
1074         }
1075         return result;
1076     }
1077 
1078     /**
1079      * Parse an integer in big endian IP format to its component bytes representing an IPv4 address
1080      *
1081      * @param ip The address as an integer
1082      * @return The address as an array of four bytes
1083      */
1084     public static byte[] parseIntToIP(int ip) {
1085         return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(ip).array();
1086     }
1087 
1088     /**
1089      * Parse an integer array in big endian IP format to its component bytes representing an IPv6 address
1090      *
1091      * @param ip6 The address as an integer array
1092      * @return The address as an array of sizteen bytes
1093      */
1094     public static byte[] parseIntArrayToIP(int[] ip6) {
1095         ByteBuffer bb = ByteBuffer.allocate(16).order(ByteOrder.LITTLE_ENDIAN);
1096         for (int i : ip6) {
1097             bb.putInt(i);
1098         }
1099         return bb.array();
1100     }
1101 
1102     /**
1103      * TCP network addresses and ports are in big endian format by definition. The order of the two bytes in the 16-bit
1104      * unsigned short port value must be reversed
1105      *
1106      * @param port The port number in big endian order
1107      * @return The port number
1108      * @see <a href= "https://docs.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-ntohs">ntohs</a>
1109      */
1110     public static int bigEndian16ToLittleEndian(int port) {
1111         // 20480 = 0x5000 should be 0x0050 = 80
1112         // 47873 = 0xBB01 should be 0x01BB = 443
1113         return port >> 8 & 0xff | port << 8 & 0xff00;
1114     }
1115 
1116     /**
1117      * Parse an integer array to an IPv4 or IPv6 as appropriate.
1118      * <p>
1119      * Intended for use on Utmp structures's {@code ut_addr_v6} element.
1120      *
1121      * @param utAddrV6 An array of 4 integers representing an IPv6 address. IPv4 address uses just utAddrV6[0]
1122      * @return A string representation of the IP address.
1123      */
1124     public static String parseUtAddrV6toIP(int[] utAddrV6) {
1125         if (utAddrV6.length != 4) {
1126             throw new IllegalArgumentException("ut_addr_v6 must have exactly 4 elements");
1127         }
1128         // IPv4 has only first element
1129         if (utAddrV6[1] == 0 && utAddrV6[2] == 0 && utAddrV6[3] == 0) {
1130             // Special case for all 0's
1131             if (utAddrV6[0] == 0) {
1132                 return "::";
1133             }
1134             // Parse using InetAddress
1135             byte[] ipv4 = ByteBuffer.allocate(4).putInt(utAddrV6[0]).array();
1136             try {
1137                 return InetAddress.getByAddress(ipv4).getHostAddress();
1138             } catch (UnknownHostException e) {
1139                 // Shouldn't happen with length 4 or 16
1140                 return Constants.UNKNOWN;
1141             }
1142         }
1143         // Parse all 16 bytes
1144         byte[] ipv6 = ByteBuffer.allocate(16).putInt(utAddrV6[0]).putInt(utAddrV6[1]).putInt(utAddrV6[2])
1145                 .putInt(utAddrV6[3]).array();
1146         try {
1147             return InetAddress.getByAddress(ipv6).getHostAddress()
1148                     .replaceAll("((?:(?:^|:)0+\\b){2,8}):?(?!\\S*\\b\\1:0+\\b)(\\S*)", "::$2");
1149         } catch (UnknownHostException e) {
1150             // Shouldn't happen with length 4 or 16
1151             return Constants.UNKNOWN;
1152         }
1153     }
1154 
1155     /**
1156      * Parses a string of hex digits to an int value.
1157      *
1158      * @param hexString    A sequence of hex digits
1159      * @param defaultValue default value to return if parsefails
1160      * @return The corresponding int value
1161      */
1162     public static int hexStringToInt(String hexString, int defaultValue) {
1163         if (hexString != null) {
1164             try {
1165                 if (hexString.startsWith("0x")) {
1166                     return new BigInteger(hexString.substring(2), 16).intValue();
1167                 } else {
1168                     return new BigInteger(hexString, 16).intValue();
1169                 }
1170             } catch (NumberFormatException e) {
1171                 LOG.trace(DEFAULT_LOG_MSG, hexString, e);
1172             }
1173         }
1174         // Hex failed to parse, just return the default long
1175         return defaultValue;
1176     }
1177 
1178     /**
1179      * Parses a string of hex digits to a long value.
1180      *
1181      * @param hexString    A sequence of hex digits
1182      * @param defaultValue default value to return if parsefails
1183      * @return The corresponding long value
1184      */
1185     public static long hexStringToLong(String hexString, long defaultValue) {
1186         if (hexString != null) {
1187             try {
1188                 if (hexString.startsWith("0x")) {
1189                     return new BigInteger(hexString.substring(2), 16).longValue();
1190                 } else {
1191                     return new BigInteger(hexString, 16).longValue();
1192                 }
1193             } catch (NumberFormatException e) {
1194                 LOG.trace(DEFAULT_LOG_MSG, hexString, e);
1195             }
1196         }
1197         // Hex failed to parse, just return the default long
1198         return defaultValue;
1199     }
1200 
1201     /**
1202      * Parses a String "....foo" to "foo"
1203      *
1204      * @param dotPrefixedStr A string with possibly leading dots
1205      * @return The string without the dots
1206      */
1207     public static String removeLeadingDots(String dotPrefixedStr) {
1208         int pos = 0;
1209         while (pos < dotPrefixedStr.length() && dotPrefixedStr.charAt(pos) == '.') {
1210             pos++;
1211         }
1212         return pos < dotPrefixedStr.length() ? dotPrefixedStr.substring(pos) : "";
1213     }
1214 
1215     /**
1216      * Parse a null-delimited byte array to a list of strings.
1217      *
1218      * @param bytes A byte array containing Strings delimited by null characters. Two consecutive null characters mark
1219      *              the end of the list.
1220      * @return A list of Strings between the nulls.
1221      */
1222     public static List<String> parseByteArrayToStrings(byte[] bytes) {
1223         List<String> strList = new ArrayList<>();
1224         int start = 0;
1225         int end = 0;
1226         // Iterate characters
1227         do {
1228             // If we've reached a delimiter or the end of the array or new line (linux), add to list
1229             if (end == bytes.length || bytes[end] == 0 || bytes[end] == '\n') {
1230                 // Zero length string means two nulls, we're done
1231                 if (start == end) {
1232                     break;
1233                 }
1234                 // Otherwise add string and reset start
1235                 // Intentionally using platform default charset
1236                 strList.add(new String(bytes, start, end - start, StandardCharsets.UTF_8));
1237                 start = end + 1;
1238             }
1239         } while (end++ < bytes.length);
1240         return strList;
1241     }
1242 
1243     /**
1244      * Parse a null-delimited byte array to a map of string keys and values.
1245      *
1246      * @param bytes A byte array containing String key-value pairs with keys and values delimited by {@code =} and pairs
1247      *              delimited by null characters. Two consecutive null characters mark the end of the map.
1248      * @return A map of String key-value pairs between the nulls.
1249      */
1250     public static Map<String, String> parseByteArrayToStringMap(byte[] bytes) {
1251         // API does not specify any particular order of entries, but it is reasonable to
1252         // maintain whatever order the OS provided to the end user
1253         Map<String, String> strMap = new LinkedHashMap<>();
1254         int start = 0;
1255         int end = 0;
1256         String key = null;
1257         // Iterate characters
1258         do {
1259             // If we've reached a delimiter or the end of the array, add to list
1260             if (end == bytes.length || bytes[end] == 0) {
1261                 // Zero length string with no key, we're done
1262                 if (start == end && key == null) {
1263                     break;
1264                 }
1265                 // Otherwise add string (possibly empty) and reset start
1266                 // Intentionally using platform default charset
1267                 strMap.put(key, new String(bytes, start, end - start, StandardCharsets.UTF_8));
1268                 key = null;
1269                 start = end + 1;
1270             } else if (bytes[end] == '=' && key == null) {
1271                 key = new String(bytes, start, end - start, StandardCharsets.UTF_8);
1272                 start = end + 1;
1273             }
1274         } while (end++ < bytes.length);
1275         return strMap;
1276     }
1277 
1278     /**
1279      * Parse a null-delimited char array to a map of string keys and values.
1280      *
1281      * @param chars A char array containing String key-value pairs with keys and values delimited by {@code =} and pairs
1282      *              delimited by null characters. Two consecutive null characters mark the end of the map.
1283      * @return A map of String key-value pairs between the nulls.
1284      */
1285     public static Map<String, String> parseCharArrayToStringMap(char[] chars) {
1286         // API does not specify any particular order of entries, but it is reasonable to
1287         // maintain whatever order the OS provided to the end user
1288         Map<String, String> strMap = new LinkedHashMap<>();
1289         int start = 0;
1290         int end = 0;
1291         String key = null;
1292         // Iterate characters
1293         do {
1294             // If we've reached a delimiter or the end of the array, add to list
1295             if (end == chars.length || chars[end] == 0) {
1296                 // Zero length string with no key, we're done
1297                 if (start == end && key == null) {
1298                     break;
1299                 }
1300                 // Otherwise add string (possibly empty) and reset start
1301                 // Intentionally using platform default charset
1302                 strMap.put(key, new String(chars, start, end - start));
1303                 key = null;
1304                 start = end + 1;
1305             } else if (chars[end] == '=' && key == null) {
1306                 key = new String(chars, start, end - start);
1307                 start = end + 1;
1308             }
1309         } while (end++ < chars.length);
1310         return strMap;
1311     }
1312 
1313     /**
1314      * Parses a delimited String into an enum map. Multiple consecutive delimiters are treated as one.
1315      *
1316      * @param <K>    a type extending Enum
1317      * @param clazz  The enum class
1318      * @param values A delimited String to be parsed into the map
1319      * @param delim  the delimiter to use
1320      * @return An EnumMap populated in order using the delimited String values. If there are fewer String values than
1321      *         enum values, the later enum values are not mapped. The final enum value will contain the remainder of the
1322      *         String, including excess delimiters.
1323      */
1324     public static <K extends Enum<K>> Map<K, String> stringToEnumMap(Class<K> clazz, String values, char delim) {
1325         EnumMap<K, String> map = new EnumMap<>(clazz);
1326         int start = 0;
1327         int len = values.length();
1328         EnumSet<K> keys = EnumSet.allOf(clazz);
1329         int keySize = keys.size();
1330         for (K key : keys) {
1331             // If this is the last enum, put the index at the end of the string, otherwise
1332             // put at delimiter
1333             int idx = --keySize == 0 ? len : values.indexOf(delim, start);
1334             if (idx >= 0) {
1335                 map.put(key, values.substring(start, idx));
1336                 start = idx;
1337                 do {
1338                     start++;
1339                 } while (start < len && values.charAt(start) == delim);
1340             } else {
1341                 map.put(key, values.substring(start));
1342                 break;
1343             }
1344         }
1345         return map;
1346     }
1347 
1348     /**
1349      * Checks if value exists in map for the given key or not and returns value or unknown based on it
1350      *
1351      * @param map A map of String key-value pairs
1352      * @param key Fetch value for the given key
1353      * @return Returns the value for the key if it exists in the map else it returns unknown
1354      */
1355     public static String getValueOrUnknown(Map<String, String> map, String key) {
1356         String value = map.getOrDefault(key, "");
1357         return value.isEmpty() ? Constants.UNKNOWN : value;
1358     }
1359 
1360     /**
1361      * Checks if a value exists in the map for the given key and returns the value or unknown based on it
1362      *
1363      * @param map A map where the keys can be of any type and the values are Strings.
1364      * @param key The key for which to fetch the value from the map. The key can be of any type that is compatible with
1365      *            the map's key type.
1366      * @return The value associated with the key if the key exists in the map and the value is not empty; otherwise,
1367      *         returns a predefined "unknown" string
1368      */
1369     public static String getValueOrUnknown(Map<?, String> map, Object key) {
1370         return getStringValueOrUnknown(map.get(key));
1371     }
1372 
1373     /**
1374      * Returns the given string value if it is not empty; otherwise, returns {@code Constants.UNKNOWN}.
1375      *
1376      * @param value The input string value.
1377      * @return The input value if it is non-empty; otherwise, {@code Constants.UNKNOWN}.
1378      */
1379     public static String getStringValueOrUnknown(String value) {
1380         return (value == null || value.isEmpty()) ? Constants.UNKNOWN : value;
1381     }
1382 
1383     /**
1384      * Parses a date string from a given format and converts it to epoch time (milliseconds since epoch). This method is
1385      * useful for handling date formats across different operating systems, such as:
1386      * <ul>
1387      * <li>{@code yyyyMMdd}</li>
1388      * <li>{@code dd/MM/yy, HH:mm}</li>
1389      * </ul>
1390      *
1391      * @param dateString  The date string to parse.
1392      * @param datePattern The expected date format pattern (e.g., {@code "yyyyMMdd"}).
1393      * @return The epoch time in milliseconds since January 1, 1970, UTC. Returns {@code 0} if parsing fails.
1394      */
1395     public static long parseDateToEpoch(String dateString, String datePattern) {
1396         if (dateString == null || dateString.equals(Constants.UNKNOWN) || dateString.isEmpty()
1397                 || datePattern.isEmpty()) {
1398             return 0; // Default value if date is unknown or empty or null
1399         }
1400         try {
1401             DateTimeFormatter formatter = new DateTimeFormatterBuilder().appendPattern(datePattern)
1402                     .parseDefaulting(ChronoField.HOUR_OF_DAY, 0).parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
1403                     .parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0).parseDefaulting(ChronoField.MILLI_OF_SECOND, 0)
1404                     .toFormatter(Locale.ROOT);
1405             LocalDateTime localDateTime = LocalDateTime.parse(dateString, formatter);
1406             return localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
1407         } catch (DateTimeParseException e) {
1408             LOG.trace("Unable to parse date string: " + dateString);
1409             return 0;
1410         }
1411     }
1412 }