jenkins.branch.NameMangler.java Source code

Java tutorial

Introduction

Here is the source code for jenkins.branch.NameMangler.java

Source

/*
 * The MIT License
 *
 * Copyright (c) 2017, CloudBees, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package jenkins.branch;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Locale;
import org.apache.commons.lang.StringUtils;

/**
 * Mangles names that are not nice so that they are safe to use on filesystem.
 * We try to keep names that are alpha-numeric and optionally contain the {@code -} character unmangled as long as they
 * are shorter than {@link #MAX_SAFE_LENGTH} characters. For all other names we mangle. In mangling we try to keep
 * the semantic meaning of separator characters like {@code /\._} and space by mangling as {@code -}. All other
 * characters are encoded using {@code _} as a prefix for the hex code. Finally we inject the hash with {@code .} used
 * to identify the hash portion.
 *
 * @since 2.0.0
 */
public final class NameMangler {

    private static final int MAX_SAFE_LENGTH = 32;
    private static final int MIN_HASH_LENGTH = 6;
    private static final int MAX_HASH_LENGTH = 12;

    /**
     * Utility class.
     */
    private NameMangler() {
        throw new IllegalAccessError("Utility class");
    }

    public static String apply(String name) {
        if (name.length() <= MAX_SAFE_LENGTH) {
            boolean unsafe = false;
            boolean first = true;
            for (char c : name.toCharArray()) {
                if (first) {
                    if (c == '-') {
                        // no leading dash
                        unsafe = true;
                        break;
                    }
                    first = false;
                }
                if (!isSafe(c)) {
                    unsafe = true;
                    break;
                }
            }
            // See https://msdn.microsoft.com/en-us/library/aa365247 we need to consistently reserve names across all OS
            if (!unsafe) {
                // we know it is only US-ASCII if we got to here
                switch (name.toLowerCase(Locale.ENGLISH)) {
                case ".":
                case "..":
                case "con":
                case "prn":
                case "aux":
                case "nul":
                case "com1":
                case "com2":
                case "com3":
                case "com4":
                case "com5":
                case "com6":
                case "com7":
                case "com8":
                case "com9":
                case "lpt1":
                case "lpt2":
                case "lpt3":
                case "lpt4":
                case "lpt5":
                case "lpt6":
                case "lpt7":
                case "lpt8":
                case "lpt9":
                    unsafe = true;
                    break;
                default:
                    if (name.endsWith(".")) {
                        unsafe = true;
                    }
                    break;
                }
            }
            if (!unsafe) {
                return name;
            }
        }
        StringBuilder buf = new StringBuilder(name.length() + 16);
        for (char c : name.toCharArray()) {
            if (isSafe(c)) {
                buf.append(c);
            } else if (c == '/' || c == '\\' || c == ' ' || c == '.' || c == '_') {
                if (buf.length() == 0) {
                    buf.append("0-");
                } else {
                    buf.append('-');
                }
            } else if (c <= 0xff) {
                if (buf.length() == 0) {
                    buf.append("0_");
                } else {
                    buf.append('_');
                }
                buf.append(StringUtils.leftPad(Integer.toHexString(c & 0xff), 2, '0'));
            } else {
                if (buf.length() == 0) {
                    buf.append("0_");
                } else {
                    buf.append('_');
                }
                buf.append(StringUtils.leftPad(Integer.toHexString(((c & 0xffff) >> 8) & 0xff), 2, '0'));
                buf.append('_');
                buf.append(StringUtils.leftPad(Integer.toHexString(c & 0xff), 2, '0'));
            }
        }
        // use the digest of the original name
        String digest;
        try {
            MessageDigest sha = MessageDigest.getInstance("SHA-1");
            byte[] bytes = sha.digest(name.getBytes(StandardCharsets.UTF_8));
            int bits = 0;
            int data = 0;
            StringBuffer dd = new StringBuffer(32);
            for (byte b : bytes) {
                while (bits >= 5) {
                    dd.append(toDigit(data & 0x1f));
                    bits -= 5;
                    data = data >> 5;
                }
                data = data | ((b & 0xff) << bits);
                bits += 8;
            }
            digest = dd.toString();
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("SHA-1 not installed", e); // impossible
        }
        if (buf.length() <= MAX_SAFE_LENGTH - MIN_HASH_LENGTH - 1) {
            // we have room to add the min hash
            buf.append('.');
            buf.append(StringUtils.right(digest, MIN_HASH_LENGTH));
            return buf.toString();
        }
        // buf now holds the mangled string, we will now try and rip the middle out to put in some of the digest
        int overage = buf.length() - MAX_SAFE_LENGTH;
        String hash;
        if (overage <= MIN_HASH_LENGTH) {
            hash = "." + StringUtils.right(digest, MIN_HASH_LENGTH) + ".";
        } else if (overage > MAX_HASH_LENGTH) {
            hash = "." + StringUtils.right(digest, MAX_HASH_LENGTH) + ".";
        } else {
            hash = "." + StringUtils.right(digest, overage) + ".";
        }
        int start = (MAX_SAFE_LENGTH - hash.length()) / 2;
        buf.delete(start, start + hash.length() + overage);
        buf.insert(start, hash);
        return buf.toString();
    }

    private static char toDigit(int n) {
        return (char) (n < 10 ? '0' + n : 'a' + n - 10);
    }

    private static boolean isSafe(char c) {
        // we use a smaller set than is strictly possible as we would prefer to mangle outside this set
        return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9') || '-' == c;
    }
}