All Downloads are FREE. Search and download functionalities are using the official Maven repository.

oshi.util.ParseUtil Maven / Gradle / Ivy

There is a newer version: 6.6.5
Show newest version
/*
 * Copyright 2016-2024 The OSHI Project Contributors
 * SPDX-License-Identifier: MIT
 */
package oshi.util;

import java.math.BigInteger;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import oshi.annotation.SuppressForbidden;
import oshi.annotation.concurrent.ThreadSafe;
import oshi.util.tuples.Pair;
import oshi.util.tuples.Triplet;

/**
 * String parsing utility.
 */
@ThreadSafe
@SuppressForbidden(reason = "Require parse methods to parse in utility class")
public final class ParseUtil {

    private static final Logger LOG = LoggerFactory.getLogger(ParseUtil.class);

    private static final String DEFAULT_LOG_MSG = "{} didn't parse. Returning default. {}";

    /*
     * Used for matching
     */
    private static final Pattern HERTZ_PATTERN = Pattern.compile("(\\d+(.\\d+)?) ?([kKMGT]?Hz).*");
    private static final Pattern BYTES_PATTERN = Pattern.compile("(\\d+) ?([kKMGT]?B?).*");
    private static final Pattern UNITS_PATTERN = Pattern.compile("(\\d+(.\\d+)?)[\\s]?([kKMGT])?");

    /*
     * Used to check validity of a hexadecimal string
     */
    private static final Pattern VALID_HEX = Pattern.compile("[0-9a-fA-F]+");

    /*
     * Pattern for [dd-[hh:[mm:[ss[.sss]]]]]
     */
    private static final Pattern DHMS = Pattern.compile("(?:(\\d+)-)?(?:(\\d+):)??(?:(\\d+):)?(\\d+)(?:\\.(\\d+))?");

    /*
     * Pattern for a UUID
     */
    private static final Pattern UUID_PATTERN = Pattern
            .compile(".*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}).*");

    /*
     * Pattern for Windows DeviceID vendor and product ID and serial
     */
    private static final Pattern VENDOR_PRODUCT_ID_SERIAL = Pattern
            .compile(".*(?:VID|VEN)_(\\p{XDigit}{4})&(?:PID|DEV)_(\\p{XDigit}{4})(.*)\\\\(.*)");

    /*
     * Pattern for Linux lspci machine readable
     */
    private static final Pattern LSPCI_MACHINE_READABLE = Pattern.compile("(.+)\\s\\[(.*?)\\]");

    /*
     * Pattern for Linux lspci memory
     */
    private static final Pattern LSPCI_MEMORY_SIZE = Pattern.compile(".+\\s\\[size=(\\d+)([kKMGT])\\]");

    /*
     * Hertz related variables.
     */
    private static final String HZ = "Hz";
    private static final String KHZ = "kHz";
    private static final String MHZ = "MHz";
    private static final String GHZ = "GHz";
    private static final String THZ = "THz";
    private static final String PHZ = "PHz";

    private static final Map multipliers;

    // PDH timestamps are 1601 epoch, local time
    // Constants to convert to UTC millis
    private static final long EPOCH_DIFF = 11_644_473_600_000L;
    private static final int TZ_OFFSET = TimeZone.getDefault().getOffset(System.currentTimeMillis());

    /** Constant whitespacesColonWhitespace */
    public static final Pattern whitespacesColonWhitespace = Pattern.compile("\\s+:\\s");

    /** Constant whitespaces */
    public static final Pattern whitespaces = Pattern.compile("\\s+");

    /** Constant notDigits */
    public static final Pattern notDigits = Pattern.compile("[^0-9]+");

    /** Constant startWithNotDigits */
    public static final Pattern startWithNotDigits = Pattern.compile("^[^0-9]*");

    /** Constant forwardSlash */
    public static final Pattern slash = Pattern.compile("\\/");

    static {
        multipliers = new HashMap<>();
        multipliers.put(HZ, 1L);
        multipliers.put(KHZ, 1_000L);
        multipliers.put(MHZ, 1_000_000L);
        multipliers.put(GHZ, 1_000_000_000L);
        multipliers.put(THZ, 1_000_000_000_000L);
        multipliers.put(PHZ, 1_000_000_000_000_000L);
    }

    // Fast decimal exponentiation: pow(10,y) --> POWERS_OF_10[y]
    private static final long[] POWERS_OF_TEN = { 1L, 10L, 100L, 1_000L, 10_000L, 100_000L, 1_000_000L, 10_000_000L,
            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,
            100_000_000_000_000L, 1_000_000_000_000_000L, 10_000_000_000_000_000L, 100_000_000_000_000_000L,
            1_000_000_000_000_000_000L };

    // Format returned by WMI for DateTime
    private static final DateTimeFormatter CIM_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss.SSSSSSZZZZZ",
            Locale.US);

    private ParseUtil() {
    }

    /**
     * Parse hertz from a string, eg. "2.00MHz" is 2000000L.
     *
     * @param hertz Hertz size.
     * @return {@link java.lang.Long} Hertz value or -1 if not parseable.
     */
    public static long parseHertz(String hertz) {
        Matcher matcher = HERTZ_PATTERN.matcher(hertz.trim());
        if (matcher.find() && matcher.groupCount() == 3) {
            // Regexp enforces #(.#) format so no test for NFE required
            double value = Double.valueOf(matcher.group(1)) * multipliers.getOrDefault(matcher.group(3), -1L);
            if (value >= 0d) {
                return (long) value;
            }
        }
        return -1L;
    }

    /**
     * Parse the last element of a space-delimited string to a value
     *
     * @param s The string to parse
     * @param i Default integer if not parsable
     * @return value or the given default if not parsable
     */
    public static int parseLastInt(String s, int i) {
        try {
            String ls = parseLastString(s);
            if (ls.toLowerCase(Locale.ROOT).startsWith("0x")) {
                return Integer.decode(ls);
            } else {
                return Integer.parseInt(ls);
            }
        } catch (NumberFormatException e) {
            LOG.trace(DEFAULT_LOG_MSG, s, e);
            return i;
        }
    }

    /**
     * Parse the last element of a space-delimited string to a value
     *
     * @param s  The string to parse
     * @param li Default long integer if not parsable
     * @return value or the given default if not parsable
     */
    public static long parseLastLong(String s, long li) {
        try {
            String ls = parseLastString(s);
            if (ls.toLowerCase(Locale.ROOT).startsWith("0x")) {
                return Long.decode(ls);
            } else {
                return Long.parseLong(ls);
            }
        } catch (NumberFormatException e) {
            LOG.trace(DEFAULT_LOG_MSG, s, e);
            return li;
        }
    }

    /**
     * Parse the last element of a space-delimited string to a value
     *
     * @param s The string to parse
     * @param d Default double if not parsable
     * @return value or the given default if not parsable
     */
    public static double parseLastDouble(String s, double d) {
        try {
            return Double.parseDouble(parseLastString(s));
        } catch (NumberFormatException e) {
            LOG.trace(DEFAULT_LOG_MSG, s, e);
            return d;
        }
    }

    /**
     * Parse the last element of a space-delimited string to a string
     *
     * @param s The string to parse
     * @return last space-delimited element
     */
    public static String parseLastString(String s) {
        String[] ss = whitespaces.split(s);
        // guaranteed at least one element
        return ss[ss.length - 1];
    }

    /**
     * Parse a byte array into a string of hexadecimal digits including all array bytes as digits
     *
     * @param bytes The byte array to represent
     * @return A string of hex characters corresponding to the bytes. The string is upper case.
     */
    public static String byteArrayToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 2);
        for (byte b : bytes) {
            sb.append(Character.forDigit((b & 0xf0) >>> 4, 16));
            sb.append(Character.forDigit(b & 0x0f, 16));
        }
        return sb.toString().toUpperCase(Locale.ROOT);
    }

    /**
     * Parse a string of hexadecimal digits into a byte array
     *
     * @param digits The string to be parsed
     * @return a byte array with each pair of characters converted to a byte, or empty array if the string is not valid
     *         hex
     */
    public static byte[] hexStringToByteArray(String digits) {
        int len = digits.length();
        // Check if string is valid hex
        if (!VALID_HEX.matcher(digits).matches() || (len & 0x1) != 0) {
            LOG.warn("Invalid hexadecimal string: {}", digits);
            return new byte[0];
        }
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) (Character.digit(digits.charAt(i), 16) << 4
                    | Character.digit(digits.charAt(i + 1), 16));
        }
        return data;
    }

    /**
     * Parse a human readable ASCII string into a byte array, truncating or padding with zeros (if necessary) so the
     * array has the specified length.
     *
     * @param text   The string to be parsed
     * @param length Length of the returned array.
     * @return A byte array of specified length, with each of the first length characters converted to a byte. If length
     *         is longer than the provided string length, will be filled with zeroes.
     */
    public static byte[] asciiStringToByteArray(String text, int length) {
        return Arrays.copyOf(text.getBytes(StandardCharsets.US_ASCII), length);
    }

    /**
     * Convert a long value to a byte array using Big Endian, truncating or padding with zeros (if necessary) so the
     * array has the specified length.
     *
     * @param value     The value to be converted
     * @param valueSize Number of bytes representing the value
     * @param length    Number of bytes to return
     * @return A byte array of specified length representing the long in the first valueSize bytes
     */
    public static byte[] longToByteArray(long value, int valueSize, int length) {
        long val = value;
        // Convert the long to 8-byte BE representation
        byte[] b = new byte[8];
        for (int i = 7; i >= 0 && val != 0L; i--) {
            b[i] = (byte) val;
            val >>>= 8;
        }
        // Then copy the rightmost valueSize bytes
        // e.g., for an integer we want rightmost 4 bytes
        return Arrays.copyOfRange(b, 8 - valueSize, 8 + length - valueSize);
    }

    /**
     * Convert a string to an integer representation.
     *
     * @param str  A human readable ASCII string
     * @param size Number of characters to convert to the long. May not exceed 8.
     * @return An integer representing the string where each character is treated as a byte
     */
    public static long strToLong(String str, int size) {
        return byteArrayToLong(str.getBytes(StandardCharsets.US_ASCII), size);
    }

    /**
     * Convert a byte array to its (long) integer representation assuming big endian ordering.
     *
     * @param bytes An array of bytes no smaller than the size to be converted
     * @param size  Number of bytes to convert to the long. May not exceed 8.
     * @return A long integer representing the byte array
     */
    public static long byteArrayToLong(byte[] bytes, int size) {
        return byteArrayToLong(bytes, size, true);
    }

    /**
     * Convert a byte array to its (long) integer representation in the specified endianness.
     *
     * @param bytes     An array of bytes no smaller than the size to be converted
     * @param size      Number of bytes to convert to the long. May not exceed 8.
     * @param bigEndian True to parse big-endian, false to parse little-endian
     * @return An long integer representing the byte array
     */
    public static long byteArrayToLong(byte[] bytes, int size, boolean bigEndian) {
        if (size > 8) {
            throw new IllegalArgumentException("Can't convert more than 8 bytes.");
        }
        if (size > bytes.length) {
            throw new IllegalArgumentException("Size can't be larger than array length.");
        }
        long total = 0L;
        for (int i = 0; i < size; i++) {
            if (bigEndian) {
                total = total << 8 | bytes[i] & 0xff;
            } else {
                total = total << 8 | bytes[size - i - 1] & 0xff;
            }
        }
        return total;
    }

    /**
     * Convert a byte array to its floating point representation.
     *
     * @param bytes  An array of bytes no smaller than the size to be converted
     * @param size   Number of bytes to convert to the float. May not exceed 8.
     * @param fpBits Number of bits representing the decimal
     * @return A float; the integer portion representing the byte array as an integer shifted by the bits specified in
     *         fpBits; with the remaining bits used as a decimal
     */
    public static float byteArrayToFloat(byte[] bytes, int size, int fpBits) {
        return byteArrayToLong(bytes, size) / (float) (1 << fpBits);
    }

    /**
     * Convert an unsigned integer to a long value. The method assumes that all bits in the specified integer value are
     * 'data' bits, including the most-significant bit which Java normally considers a sign bit. The method must be used
     * only when it is certain that the integer value represents an unsigned integer, for example when the integer is
     * returned by JNA library in a structure which holds unsigned integers.
     *
     * @param unsignedValue The unsigned integer value to convert.
     * @return The unsigned integer value widened to a long.
     */
    public static long unsignedIntToLong(int unsignedValue) {
        // use standard Java widening conversion to long which does
        // sign-extension,
        // then drop any copies of the sign bit, to prevent the value being
        // considered a negative one by Java if it is set
        long longValue = unsignedValue;
        return longValue & 0xffff_ffffL;
    }

    /**
     * Convert an unsigned long to a signed long value by stripping the sign bit. This method "rolls over" long values
     * greater than the max value but ensures the result is never negative.
     *
     * @param unsignedValue The unsigned long value to convert.
     * @return The signed long value.
     */
    public static long unsignedLongToSignedLong(long unsignedValue) {
        return unsignedValue & 0x7fff_ffff_ffff_ffffL;
    }

    /**
     * Parses a string of hex digits to a string where each pair of hex digits represents an ASCII character
     *
     * @param hexString A sequence of hex digits
     * @return The corresponding string if valid hex; otherwise the original hexString
     */
    public static String hexStringToString(String hexString) {
        // Odd length strings won't parse, return
        if (hexString.length() % 2 > 0) {
            return hexString;
        }
        int charAsInt;
        StringBuilder sb = new StringBuilder();
        try {
            for (int pos = 0; pos < hexString.length(); pos += 2) {
                charAsInt = Integer.parseInt(hexString.substring(pos, pos + 2), 16);
                if (charAsInt < 32 || charAsInt > 127) {
                    return hexString;
                }
                sb.append((char) charAsInt);
            }
        } catch (NumberFormatException e) {
            LOG.trace(DEFAULT_LOG_MSG, hexString, e);
            // Hex failed to parse, just return the existing string
            return hexString;
        }
        return sb.toString();
    }

    /**
     * Attempts to parse a string to an int. If it fails, returns the default
     *
     * @param s          The string to parse
     * @param defaultInt The value to return if parsing fails
     * @return The parsed int, or the default if parsing failed
     */
    public static int parseIntOrDefault(String s, int defaultInt) {
        try {
            return Integer.parseInt(s);
        } catch (NumberFormatException e) {
            LOG.trace(DEFAULT_LOG_MSG, s, e);
            return defaultInt;
        }
    }

    /**
     * Attempts to parse a string to a long. If it fails, returns the default
     *
     * @param s           The string to parse
     * @param defaultLong The value to return if parsing fails
     * @return The parsed long, or the default if parsing failed
     */
    public static long parseLongOrDefault(String s, long defaultLong) {
        try {
            return Long.parseLong(s);
        } catch (NumberFormatException e) {
            LOG.trace(DEFAULT_LOG_MSG, s, e);
            return defaultLong;
        }
    }

    /**
     * Attempts to parse a string to an "unsigned" long. If it fails, returns the default
     *
     * @param s           The string to parse
     * @param defaultLong The value to return if parsing fails
     * @return The parsed long containing the same 64 bits that an unsigned long would contain (which may produce a
     *         negative value)
     */
    public static long parseUnsignedLongOrDefault(String s, long defaultLong) {
        try {
            return new BigInteger(s).longValue();
        } catch (NumberFormatException e) {
            LOG.trace(DEFAULT_LOG_MSG, s, e);
            return defaultLong;
        }
    }

    /**
     * Attempts to parse a string to a double. If it fails, returns the default
     *
     * @param s             The string to parse
     * @param defaultDouble The value to return if parsing fails
     * @return The parsed double, or the default if parsing failed
     */
    public static double parseDoubleOrDefault(String s, double defaultDouble) {
        try {
            return Double.parseDouble(s);
        } catch (NumberFormatException e) {
            LOG.trace(DEFAULT_LOG_MSG, s, e);
            return defaultDouble;
        }
    }

    /**
     * Attempts to parse a string of the form [DD-[hh:]]mm:ss[.ddd] to a number of milliseconds. If it fails, returns
     * the default.
     *
     * @param s           The string to parse
     * @param defaultLong The value to return if parsing fails
     * @return The parsed number of seconds, or the default if parsing fails
     */
    public static long parseDHMSOrDefault(String s, long defaultLong) {
        Matcher m = DHMS.matcher(s);
        if (m.matches()) {
            long milliseconds = 0L;
            if (m.group(1) != null) {
                milliseconds += parseLongOrDefault(m.group(1), 0L) * 86_400_000L;
            }
            if (m.group(2) != null) {
                milliseconds += parseLongOrDefault(m.group(2), 0L) * 3_600_000L;
            }
            if (m.group(3) != null) {
                milliseconds += parseLongOrDefault(m.group(3), 0L) * 60_000L;
            }
            milliseconds += parseLongOrDefault(m.group(4), 0L) * 1000L;
            if (m.group(5) != null) {
                milliseconds += (long) (1000 * parseDoubleOrDefault("0." + m.group(5), 0d));
            }
            return milliseconds;
        }
        return defaultLong;
    }

    /**
     * Attempts to parse a UUID. If it fails, returns the default.
     *
     * @param s          The string to parse
     * @param defaultStr The value to return if parsing fails
     * @return The parsed UUID, or the default if parsing fails
     */
    public static String parseUuidOrDefault(String s, String defaultStr) {
        Matcher m = UUID_PATTERN.matcher(s.toLowerCase(Locale.ROOT));
        if (m.matches()) {
            return m.group(1);
        }
        return defaultStr;
    }

    /**
     * Parses a string key = 'value' (string)
     *
     * @param line The entire string
     * @return the value contained between single tick marks
     */
    public static String getSingleQuoteStringValue(String line) {
        return getStringBetween(line, '\'');
    }

    /**
     * Parse a string key = "value" (string)
     *
     * @param line the entire string
     * @return the value contained between double tick marks
     */
    public static String getDoubleQuoteStringValue(String line) {
        return getStringBetween(line, '"');
    }

    /**
     * Gets a value between two characters having multiple same characters between them. Examples : 
     * 
    *
  • "name = 'James Gosling's Java'" returns "James Gosling's Java"
  • *
  • "pci.name = 'Realtek AC'97 Audio Device'" returns "Realtek AC'97 Audio Device"
  • *
* * @param line The "key-value" pair line. * @param c The Trailing And Leading characters of the string line * @return : The value having the characters between them. */ public static String getStringBetween(String line, char c) { int firstOcc = line.indexOf(c); if (firstOcc < 0) { return ""; } return line.substring(firstOcc + 1, line.lastIndexOf(c)).trim(); } /** * 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 * more consecutive digits * * @param line The entire string * @return the value of first integer if any; 0 otherwise */ public static int getFirstIntValue(String line) { return getNthIntValue(line, 1); } /** * 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 * more consecutive digits * * @param line The entire string * @param n Which set of integers to return * @return the value of nth integer if any; 0 otherwise */ public static int getNthIntValue(String line, int n) { // Split the string by non-digits, String[] split = notDigits.split(startWithNotDigits.matcher(line).replaceFirst("")); if (split.length >= n) { return parseIntOrDefault(split[n - 1], 0); } return 0; } /** * Removes all matching sub strings from the string. More efficient than regexp. * * @param original source String to remove from * @param toRemove the sub string to be removed * @return The string with all matching substrings removed */ public static String removeMatchingString(final String original, final String toRemove) { if (original == null || original.isEmpty() || toRemove == null || toRemove.isEmpty()) { return original; } int matchIndex = original.indexOf(toRemove, 0); if (matchIndex == -1) { return original; } StringBuilder buffer = new StringBuilder(original.length() - toRemove.length()); int currIndex = 0; do { buffer.append(original.substring(currIndex, matchIndex)); currIndex = matchIndex + toRemove.length(); matchIndex = original.indexOf(toRemove, currIndex); } while (matchIndex != -1); buffer.append(original.substring(currIndex)); return buffer.toString(); } /** * Parses a delimited string to an array of longs. Optimized for processing predictable-length arrays such as * outputs of reliably formatted Linux proc or sys filesystem, minimizing new object creation. Users should perform * other sanity checks of data. * * As a special case, non-numeric fields (such as UUIDs in OpenVZ) at the end of the list are ignored. Values * greater than the max long value return the max long value. * * The indices parameters are referenced assuming the length as specified, and leading characters are ignored. For * 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. * * @param s The string to parse * @param indices An array indicating which indexes should be populated in the final array; other values will be * skipped. This idex is zero-referenced assuming the rightmost delimited fields of the string * contain the array. * @param length The total number of elements in the string array. It is permissible for the string to have more * elements than this; leading elements will be ignored. This should be calculated once per text * format by {@link #countStringToLongArray}. * @param delimiter The character to delimit by. * @return If successful, an array of parsed longs. If parsing errors occurred, will be an array of zeros. */ public static long[] parseStringToLongArray(String s, int[] indices, int length, char delimiter) { // Ensure that the last character is a number s = s.trim(); long[] parsed = new long[indices.length]; // Iterate from right-to-left of String // Fill right to left of result array using index array int charIndex = s.length(); int parsedIndex = indices.length - 1; int stringIndex = length - 1; int power = 0; int c; boolean delimCurrent = false; boolean numeric = true; boolean numberFound = false; // ignore nonnumeric at end boolean dashSeen = false; // to flag uuids as nonnumeric while (--charIndex >= 0 && parsedIndex >= 0) { c = s.charAt(charIndex); if (c == delimiter) { // first parseable number? if (!numberFound && numeric) { numberFound = true; } if (!delimCurrent) { if (numberFound && indices[parsedIndex] == stringIndex--) { parsedIndex--; } delimCurrent = true; power = 0; dashSeen = false; numeric = true; } } else if (indices[parsedIndex] != stringIndex || c == '+' || !numeric) { // Doesn't impact parsing, ignore delimCurrent = false; } else if (c >= '0' && c <= '9' && !dashSeen) { if (power > 18 || power == 17 && c == '9' && parsed[parsedIndex] > 223_372_036_854_775_807L) { parsed[parsedIndex] = Long.MAX_VALUE; } else { parsed[parsedIndex] += (c - '0') * ParseUtil.POWERS_OF_TEN[power++]; } delimCurrent = false; } else if (c == '-') { parsed[parsedIndex] *= -1L; delimCurrent = false; dashSeen = true; } else { // Flag as nonnumeric and continue unless we've seen a numeric // error on everything else if (numberFound) { if (!noLog(s)) { LOG.error("Illegal character parsing string '{}' to long array: {}", s, s.charAt(charIndex)); } return new long[indices.length]; } parsed[parsedIndex] = 0; numeric = false; } } if (parsedIndex > 0) { if (!noLog(s)) { LOG.error("Not enough fields in string '{}' parsing to long array: {}", s, indices.length - parsedIndex); } return new long[indices.length]; } return parsed; } /** * Test whether to log this message * * @param s The string to log * @return True if the string begins with {@code NOLOG} */ private static boolean noLog(String s) { return s.startsWith("NOLOG: "); } /** * Parses a delimited string to count elements of an array of longs. Intended to be called once to calculate the * {@code length} field for {@link #parseStringToLongArray}. * * As a special case, non-numeric fields (such as UUIDs in OpenVZ) at the end of the list are ignored. * * @param s The string to parse * @param delimiter The character to delimit by * @return The number of parsable long values which follow the last unparsable value. */ public static int countStringToLongArray(String s, char delimiter) { // Ensure that the last character is a number s = s.trim(); // Iterate from right-to-left of String // Fill right to left of result array using index array int charIndex = s.length(); int numbers = 0; int c; boolean delimCurrent = false; boolean numeric = true; boolean dashSeen = false; // to flag uuids as nonnumeric while (--charIndex >= 0) { c = s.charAt(charIndex); if (c == delimiter) { if (!delimCurrent) { if (numeric) { numbers++; } delimCurrent = true; dashSeen = false; numeric = true; } } else if (c == '+' || !numeric) { // Doesn't impact parsing, ignore delimCurrent = false; } else if (c >= '0' && c <= '9' && !dashSeen) { delimCurrent = false; } else if (c == '-') { delimCurrent = false; dashSeen = true; } else { // we found non-digit or delimiter. If not last field, exit if (numbers > 0) { return numbers; } // Else flag as nonnumeric and continue numeric = false; } } // We got to beginning of string with only numbers, count start as a delimiter // and exit return numbers + 1; } /** * Get a String in a line of text between two marker strings * * @param text Text to search for match * @param before Start matching after this text * @param after End matching before this text * @return Text between the strings before and after, or empty string if either marker does not exist */ public static String getTextBetweenStrings(String text, String before, String after) { String result = ""; if (text.indexOf(before) >= 0 && text.indexOf(after) >= 0) { result = text.substring(text.indexOf(before) + before.length(), text.length()); result = result.substring(0, result.indexOf(after)); } return result; } /** * Convert a long representing filetime (100-ns since 1601 epoch) to ms since 1970 epoch * * @param filetime A 64-bit value equivalent to FILETIME * @param local True if converting from a local filetime (PDH counter); false if already UTC (WMI PerfRawData * classes) * @return Equivalent milliseconds since the epoch */ public static long filetimeToUtcMs(long filetime, boolean local) { return filetime / 10_000L - EPOCH_DIFF - (local ? TZ_OFFSET : 0L); } /** * Parse a date in MM-DD-YYYY or MM/DD/YYYY to YYYY-MM-DD * * @param dateString The date in MM DD YYYY format * @return The date in ISO YYYY-MM-DD format if parseable, or the original string */ public static String parseMmDdYyyyToYyyyMmDD(String dateString) { try { // Date is MM-DD-YYYY, convert to YYYY-MM-DD return String.format(Locale.ROOT, "%s-%s-%s", dateString.substring(6, 10), dateString.substring(0, 2), dateString.substring(3, 5)); } catch (StringIndexOutOfBoundsException e) { return dateString; } } /** * Converts a string in CIM Date Format, as returned by WMI for DateTime types, into a * {@link java.time.OffsetDateTime}. * * @param cimDateTime A non-null DateTime String in CIM date format, e.g., 20160513072950.782000-420 * @return The parsed {@link java.time.OffsetDateTime} if the string is parsable, otherwise * {@link oshi.util.Constants#UNIX_EPOCH}. */ public static OffsetDateTime parseCimDateTimeToOffset(String cimDateTime) { // Keep first 22 characters: digits, decimal, and + or - sign // But alter last 3 characters from a minute offset to hh:mm try { // From WMI as 20160513072950.782000-420, int tzInMinutes = Integer.parseInt(cimDateTime.substring(22)); // modified to 20160513072950.782000-07:00 which can be parsed LocalTime offsetAsLocalTime = LocalTime.MIDNIGHT.plusMinutes(tzInMinutes); return OffsetDateTime.parse( cimDateTime.substring(0, 22) + offsetAsLocalTime.format(DateTimeFormatter.ISO_LOCAL_TIME), ParseUtil.CIM_FORMAT); } catch (IndexOutOfBoundsException // if cimDate not 22+ chars | NumberFormatException // if TZ minutes doesn't parse | DateTimeParseException e) { LOG.trace("Unable to parse {} to CIM DateTime.", cimDateTime); return Constants.UNIX_EPOCH; } } /** * Checks if a file path equals or starts with an prefix in the given list * * @param prefixList A list of path prefixes * @param path a string path to check * @return true if the path exactly equals, or starts with one of the strings in prefixList */ public static boolean filePathStartsWith(List prefixList, String path) { for (String match : prefixList) { if (path.equals(match) || path.startsWith(match + "/")) { return true; } } return false; } /** * Parses a string like "53G" or "54.904 M" to its long value. * * @param count A count with a multiplyer like "4096 M" * @return the count parsed to a long */ public static long parseMultipliedToLongs(String count) { Matcher matcher = UNITS_PATTERN.matcher(count.trim()); String[] mem; if (matcher.find() && matcher.groupCount() == 3) { mem = new String[2]; mem[0] = matcher.group(1); mem[1] = matcher.group(3); } else { mem = new String[] { count }; } double number = ParseUtil.parseDoubleOrDefault(mem[0], 0L); if (mem.length == 2 && mem[1] != null && mem[1].length() >= 1) { switch (mem[1].charAt(0)) { case 'T': number *= 1_000_000_000_000L; break; case 'G': number *= 1_000_000_000L; break; case 'M': number *= 1_000_000L; break; case 'K': case 'k': number *= 1_000L; break; default: } } return (long) number; } /** * Parses a string such as "4096 MB" to its long. Used to parse macOS and *nix memory chip sizes. Although the units * given are decimal they must parse to binary units. * * @param size A string of memory sizes like "4096 MB" * @return the size parsed to a long */ public static long parseDecimalMemorySizeToBinary(String size) { String[] mem = ParseUtil.whitespaces.split(size); if (mem.length < 2) { // If no spaces, use regexp Matcher matcher = BYTES_PATTERN.matcher(size.trim()); if (matcher.find() && matcher.groupCount() == 2) { mem = new String[2]; mem[0] = matcher.group(1); mem[1] = matcher.group(2); } } long capacity = ParseUtil.parseLongOrDefault(mem[0], 0L); if (mem.length == 2 && mem[1].length() > 1) { switch (mem[1].charAt(0)) { case 'T': capacity <<= 40; break; case 'G': capacity <<= 30; break; case 'M': capacity <<= 20; break; case 'K': case 'k': capacity <<= 10; break; default: break; } } return capacity; } /** * Parse a Windows DeviceID to get the vendor ID, product ID, and Serial Number * * @param deviceId The DeviceID * @return A {@link Triplet} where the first element is the vendor ID, the second element is the product ID, and the * third element is either a serial number or empty string if parsing was successful, or {@code null} * otherwise */ public static Triplet parseDeviceIdToVendorProductSerial(String deviceId) { Matcher m = VENDOR_PRODUCT_ID_SERIAL.matcher(deviceId); if (m.matches()) { String vendorId = "0x" + m.group(1).toLowerCase(Locale.ROOT); String productId = "0x" + m.group(2).toLowerCase(Locale.ROOT); String serial = m.group(4); return new Triplet<>(vendorId, productId, !m.group(3).isEmpty() || serial.contains("&") ? "" : serial); } return null; } /** * Parse a Linux lshw resources string to calculate the memory size * * @param resources A string containing one or more elements of the form {@code memory:b00000000-bffffffff} * @return The number of bytes consumed by the memory in the {@code resources} string */ public static long parseLshwResourceString(String resources) { long bytes = 0L; // First split by whitespace String[] resourceArray = whitespaces.split(resources); for (String r : resourceArray) { // Remove prefix if (r.startsWith("memory:")) { // Split to low and high String[] mem = r.substring(7).split("-"); if (mem.length == 2) { try { // Parse the hex strings bytes += Long.parseLong(mem[1], 16) - Long.parseLong(mem[0], 16) + 1; } catch (NumberFormatException e) { LOG.trace(DEFAULT_LOG_MSG, r, e); } } } } return bytes; } /** * Parse a Linux lspci machine readble line to its name and id * * @param line A string in the form Foo [bar] * @return A pair separating the String before the square brackets and within them if found, null otherwise */ public static Pair parseLspciMachineReadable(String line) { Matcher matcher = LSPCI_MACHINE_READABLE.matcher(line); if (matcher.matches()) { return new Pair<>(matcher.group(1), matcher.group(2)); } return null; } /** * Parse a Linux lspci line containing memory size * * @param line A string in the form Foo [size=256M] * @return A the memory size in bytes */ public static long parseLspciMemorySize(String line) { Matcher matcher = LSPCI_MEMORY_SIZE.matcher(line); if (matcher.matches()) { return parseDecimalMemorySizeToBinary(matcher.group(1) + " " + matcher.group(2) + "B"); } return 0; } /** * Parse a space-delimited list of integers which include hyphenated ranges to a list of just the integers. For * example, 0 1 4-7 parses to a list containing 0, 1, 4, 5, 6, and 7. Also support comma separated entries like 0, * 2-5, 7-8, 9 to a list containing 0, 2, 3, 4, 5, 7, 8, 9. * * @param str A string containing space-delimited integers or ranges of integers with a hyphen * @return A list of integers representing the provided range(s). */ public static List parseHyphenatedIntList(String str) { List result = new ArrayList<>(); String[] csvTokens = str.split(","); for (String csvToken : csvTokens) { csvToken = csvToken.trim(); for (String s : whitespaces.split(csvToken)) { if (s.contains("-")) { int first = getFirstIntValue(s); int last = getNthIntValue(s, 2); for (int i = first; i <= last; i++) { result.add(i); } } else { int only = ParseUtil.parseIntOrDefault(s, -1); if (only >= 0) { result.add(only); } } } } return result; } /** * Parse an integer in big endian IP format to its component bytes representing an IPv4 address * * @param ip The address as an integer * @return The address as an array of four bytes */ public static byte[] parseIntToIP(int ip) { return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(ip).array(); } /** * Parse an integer array in big endian IP format to its component bytes representing an IPv6 address * * @param ip6 The address as an integer array * @return The address as an array of sizteen bytes */ public static byte[] parseIntArrayToIP(int[] ip6) { ByteBuffer bb = ByteBuffer.allocate(16).order(ByteOrder.LITTLE_ENDIAN); for (int i : ip6) { bb.putInt(i); } return bb.array(); } /** * TCP network addresses and ports are in big endian format by definition. The order of the two bytes in the 16-bit * unsigned short port value must be reversed * * @param port The port number in big endian order * @return The port number * @see ntohs */ public static int bigEndian16ToLittleEndian(int port) { // 20480 = 0x5000 should be 0x0050 = 80 // 47873 = 0xBB01 should be 0x01BB = 443 return port >> 8 & 0xff | port << 8 & 0xff00; } /** * Parse an integer array to an IPv4 or IPv6 as appropriate. *

* Intended for use on Utmp structures's {@code ut_addr_v6} element. * * @param utAddrV6 An array of 4 integers representing an IPv6 address. IPv4 address uses just utAddrV6[0] * @return A string representation of the IP address. */ public static String parseUtAddrV6toIP(int[] utAddrV6) { if (utAddrV6.length != 4) { throw new IllegalArgumentException("ut_addr_v6 must have exactly 4 elements"); } // IPv4 has only first element if (utAddrV6[1] == 0 && utAddrV6[2] == 0 && utAddrV6[3] == 0) { // Special case for all 0's if (utAddrV6[0] == 0) { return "::"; } // Parse using InetAddress byte[] ipv4 = ByteBuffer.allocate(4).putInt(utAddrV6[0]).array(); try { return InetAddress.getByAddress(ipv4).getHostAddress(); } catch (UnknownHostException e) { // Shouldn't happen with length 4 or 16 return Constants.UNKNOWN; } } // Parse all 16 bytes byte[] ipv6 = ByteBuffer.allocate(16).putInt(utAddrV6[0]).putInt(utAddrV6[1]).putInt(utAddrV6[2]) .putInt(utAddrV6[3]).array(); try { return InetAddress.getByAddress(ipv6).getHostAddress() .replaceAll("((?:(?:^|:)0+\\b){2,8}):?(?!\\S*\\b\\1:0+\\b)(\\S*)", "::$2"); } catch (UnknownHostException e) { // Shouldn't happen with length 4 or 16 return Constants.UNKNOWN; } } /** * Parses a string of hex digits to an int value. * * @param hexString A sequence of hex digits * @param defaultValue default value to return if parsefails * @return The corresponding int value */ public static int hexStringToInt(String hexString, int defaultValue) { if (hexString != null) { try { if (hexString.startsWith("0x")) { return new BigInteger(hexString.substring(2), 16).intValue(); } else { return new BigInteger(hexString, 16).intValue(); } } catch (NumberFormatException e) { LOG.trace(DEFAULT_LOG_MSG, hexString, e); } } // Hex failed to parse, just return the default long return defaultValue; } /** * Parses a string of hex digits to a long value. * * @param hexString A sequence of hex digits * @param defaultValue default value to return if parsefails * @return The corresponding long value */ public static long hexStringToLong(String hexString, long defaultValue) { if (hexString != null) { try { if (hexString.startsWith("0x")) { return new BigInteger(hexString.substring(2), 16).longValue(); } else { return new BigInteger(hexString, 16).longValue(); } } catch (NumberFormatException e) { LOG.trace(DEFAULT_LOG_MSG, hexString, e); } } // Hex failed to parse, just return the default long return defaultValue; } /** * Parses a String "....foo" to "foo" * * @param dotPrefixedStr A string with possibly leading dots * @return The string without the dots */ public static String removeLeadingDots(String dotPrefixedStr) { int pos = 0; while (pos < dotPrefixedStr.length() && dotPrefixedStr.charAt(pos) == '.') { pos++; } return pos < dotPrefixedStr.length() ? dotPrefixedStr.substring(pos) : ""; } /** * Parse a null-delimited byte array to a list of strings. * * @param bytes A byte array containing Strings delimited by null characters. Two consecutive null characters mark * the end of the list. * @return A list of Strings between the nulls. */ public static List parseByteArrayToStrings(byte[] bytes) { List strList = new ArrayList<>(); int start = 0; int end = 0; // Iterate characters do { // If we've reached a delimiter or the end of the array, add to list if (end == bytes.length || bytes[end] == 0) { // Zero length string means two nulls, we're done if (start == end) { break; } // Otherwise add string and reset start // Intentionally using platform default charset strList.add(new String(bytes, start, end - start, StandardCharsets.UTF_8)); start = end + 1; } } while (end++ < bytes.length); return strList; } /** * Parse a null-delimited byte array to a map of string keys and values. * * @param bytes A byte array containing String key-value pairs with keys and values delimited by {@code =} and pairs * delimited by null characters. Two consecutive null characters mark the end of the map. * @return A map of String key-value pairs between the nulls. */ public static Map parseByteArrayToStringMap(byte[] bytes) { // API does not specify any particular order of entries, but it is reasonable to // maintain whatever order the OS provided to the end user Map strMap = new LinkedHashMap<>(); int start = 0; int end = 0; String key = null; // Iterate characters do { // If we've reached a delimiter or the end of the array, add to list if (end == bytes.length || bytes[end] == 0) { // Zero length string with no key, we're done if (start == end && key == null) { break; } // Otherwise add string (possibly empty) and reset start // Intentionally using platform default charset strMap.put(key, new String(bytes, start, end - start, StandardCharsets.UTF_8)); key = null; start = end + 1; } else if (bytes[end] == '=' && key == null) { key = new String(bytes, start, end - start, StandardCharsets.UTF_8); start = end + 1; } } while (end++ < bytes.length); return strMap; } /** * Parse a null-delimited char array to a map of string keys and values. * * @param chars A char array containing String key-value pairs with keys and values delimited by {@code =} and pairs * delimited by null characters. Two consecutive null characters mark the end of the map. * @return A map of String key-value pairs between the nulls. */ public static Map parseCharArrayToStringMap(char[] chars) { // API does not specify any particular order of entries, but it is reasonable to // maintain whatever order the OS provided to the end user Map strMap = new LinkedHashMap<>(); int start = 0; int end = 0; String key = null; // Iterate characters do { // If we've reached a delimiter or the end of the array, add to list if (end == chars.length || chars[end] == 0) { // Zero length string with no key, we're done if (start == end && key == null) { break; } // Otherwise add string (possibly empty) and reset start // Intentionally using platform default charset strMap.put(key, new String(chars, start, end - start)); key = null; start = end + 1; } else if (chars[end] == '=' && key == null) { key = new String(chars, start, end - start); start = end + 1; } } while (end++ < chars.length); return strMap; } /** * Parses a delimited String into an enum map. Multiple consecutive delimiters are treated as one. * * @param a type extending Enum * @param clazz The enum class * @param values A delimited String to be parsed into the map * @param delim the delimiter to use * @return An EnumMap populated in order using the delimited String values. If there are fewer String values than * enum values, the later enum values are not mapped. The final enum value will contain the remainder of the * String, including excess delimiters. */ public static > Map stringToEnumMap(Class clazz, String values, char delim) { EnumMap map = new EnumMap<>(clazz); int start = 0; int len = values.length(); EnumSet keys = EnumSet.allOf(clazz); int keySize = keys.size(); for (K key : keys) { // If this is the last enum, put the index at the end of the string, otherwise // put at delimiter int idx = --keySize == 0 ? len : values.indexOf(delim, start); if (idx >= 0) { map.put(key, values.substring(start, idx)); start = idx; do { start++; } while (start < len && values.charAt(start) == delim); } else { map.put(key, values.substring(start)); break; } } return map; } /** * Checks if value exists in map for the given key or not and returns value or unknown based on it * * @param map A map of String key-value pairs * @param key Fetch value for the given key * @return Returns the value for the key if it exists in the map else it returns unknown */ public static String getValueOrUnknown(Map map, String key) { String value = map.getOrDefault(key, ""); return value.isEmpty() ? Constants.UNKNOWN : value; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy