Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[FIXED JENKINS-40865] Org folders should encode child project names
- Also switches to name mangling for multi-branch projects
  • Loading branch information
stephenc committed Jan 12, 2017
1 parent 151d076 commit 3105d46
Show file tree
Hide file tree
Showing 4 changed files with 226 additions and 4 deletions.
2 changes: 1 addition & 1 deletion src/main/java/jenkins/branch/Branch.java
Expand Up @@ -153,7 +153,7 @@ public String getName() {
* @since 0.2-beta-7
*/
public String getEncodedName() {
return Util.rawEncode(getName());
return NameMangler.apply(getName());
}

/**
Expand Down
136 changes: 136 additions & 0 deletions src/main/java/jenkins/branch/NameMangler.java
@@ -0,0 +1,136 @@
/*
* 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.
*
* @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;
for (char c : name.toCharArray()) {
if (!isSafe(c)) {
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 == ' ') {
buf.append('_');
} else if (c <= 0xff){
buf.append('%');
buf.append(StringUtils.leftPad(Integer.toHexString(c & 0xff), 2, '0'));
} else {
buf.append('%');
buf.append(StringUtils.leftPad(Integer.toHexString(c & 0xff), 2, '0'));
buf.append('%');
buf.append(StringUtils.leftPad(Integer.toHexString((c & 0xffff) >> 8), 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("MD5 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
|| '.' == c
|| '-' == c
|| '@' == c;
}
}
10 changes: 7 additions & 3 deletions src/main/java/jenkins/branch/OrganizationFolder.java
Expand Up @@ -1056,7 +1056,8 @@ public void complete() throws IllegalStateException, InterruptedException {
if (factory == null) {
return;
}
MultiBranchProject<?, ?> existing = observer.shouldUpdate(projectName);
String encodedName = NameMangler.apply(projectName);
MultiBranchProject<?, ?> existing = observer.shouldUpdate(encodedName);
if (existing != null) {
PersistedList<BranchSource> sourcesList = existing.getSourcesList();
sourcesList.clear();
Expand All @@ -1066,13 +1067,16 @@ public void complete() throws IllegalStateException, InterruptedException {
existing.scheduleBuild();
return;
}
if (!observer.mayCreate(projectName)) {
if (!observer.mayCreate(encodedName)) {
listener.getLogger().println("Ignoring duplicate child " + projectName);
return;
}
MultiBranchProject<?, ?> project = factory.createNewProject(
OrganizationFolder.this, projectName, sources, attributes, listener
OrganizationFolder.this, encodedName, sources, attributes, listener
);
if (!encodedName.equals(projectName)) {
project.setDisplayName(projectName);
}
project.setOrphanedItemStrategy(getOrphanedItemStrategy());
project.getSourcesList().addAll(createBranchSources());
try {
Expand Down
82 changes: 82 additions & 0 deletions src/test/java/jenkins/branch/NameManglerTest.java
@@ -0,0 +1,82 @@
/*
* 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 org.junit.Test;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;

public class NameManglerTest {

@Test
public void safeNames() {
assertThat(NameMangler.apply("foo"), is("foo"));
assertThat(NameMangler.apply("foo-bar"), is("foo-bar"));
assertThat(NameMangler.apply("foo bar"), is("foo_bar-074vf0"));
}

@Test
public void slashNames() {
assertThat(NameMangler.apply("foo/bar"), is("foo_bar-nj9av9"));
assertThat(NameMangler.apply("foo/bar/fu manchu"), is("foo_bar_fu_manchu-k630nd"));
assertThat(NameMangler.apply("foo/bar/fu manchu/1"), is("foo_bar_fu_manchu_1-vgnr4j"));
assertThat(NameMangler.apply("foo/bar/fu manchu/12"), is("foo_bar_fu_manchu_12-6urklv"));
assertThat(NameMangler.apply("foo/bar/fu manchu/123"), is("foo_bar_fu_manchu_123-nap1h9"));
assertThat(NameMangler.apply("foo/bar/fu manchu/1234"), is("foo_bar_fu_manchu_1234-kstl5e"));
assertThat(NameMangler.apply("foo/bar/fu manchu/12345"), is("foo_bar_fu_manchu_12345-i2apnp"));
assertThat(NameMangler.apply("foo/bar/fu manchu/123456"), is( "foo_bar_fu_manchu_123456-8vabkm"));
assertThat(NameMangler.apply("foo/bar/fu manchu/1234567"), is("foo_bar_fu_manchu_1234567-5h1u4c"));
assertThat(NameMangler.apply("foo/bar/fu manchu/12345678"), is("foo_bar_fu_m-vrohpg-chu_12345678"));
assertThat(NameMangler.apply("foo/bar/fu manchu/123456789"), is("foo_bar_fu_m-403j04-hu_123456789"));
assertThat(NameMangler.apply("foo/bar/fu manchu/1234567890"), is("foo_bar_fu_m-jrvb2f-u_1234567890"));
assertThat(NameMangler.apply("foo/bar/fu manchu/1234567890a"), is("foo_bar_fu_m-1dcfvj-_1234567890a"));
assertThat(NameMangler.apply("foo/bar/fu manchu/1234567890ab"), is("foo_bar_fu_m-mdl920-1234567890ab"));
assertThat(NameMangler.apply("foo/bar/fu manchu/1234567890abc"), is("foo_bar_fu_m-aql4gn-234567890abc"));
assertThat(NameMangler.apply("foo/bar/fu manchu/1234567890abce"), is("foo_bar_fu_m-bt3j2r-34567890abce"));
assertThat(NameMangler.apply("foo/bar/fu manchu/1234567890abcef"), is("foo_bar_fu_m-jjum74-4567890abcef"));
assertThat(NameMangler.apply("foo/bar/fu manchu/1234567890abcefg"), is("foo_bar_fu_m-vddees-567890abcefg"));
}

@Test
public void longNames() {
assertThat(NameMangler.apply("cafebabedeadbeefcafebabedeadbeef"), is("cafebabedeadbeefcafebabedeadbeef"));
assertThat(NameMangler.apply("cafebabedeadbeefcafebabedeadbeefcafebabedeadbeef"), is("cafebabed-98h82o58mhfo-edeadbeef"));
assertThat(NameMangler.apply("cafebabedeadbeefcafebabeDeadbeefcafebabedeadbeef"), is("cafebabed-a67pve49oi0n-edeadbeef"));
assertThat(NameMangler.apply("cafebabedeadbeefcafebabedeadbeef1"), is("cafebabedead-dfcoms-abedeadbeef1"));
assertThat(NameMangler.apply("cafebabedeadbeefcafebabedeadbeef2"), is("cafebabedead-m0u50r-abedeadbeef2"));
}

@Test
public void nonSafeNames() {
assertThat(NameMangler.apply("Is maith liom criospaí"), is("Is_maith_liom_criospa%ed-0g5uh9"));
assertThat(NameMangler.apply("Ich liebe Fußball"), is("Ich_liebe_Fu%dfball-fp53tq"));
assertThat(NameMangler.apply("我喜欢披萨"), is("%11%62%9c%55-f9c1g4-%ab%62%28%84"));
assertThat(NameMangler.apply("特征/新"), is("%79%72%81%5f_%b0%65-nt1m48"));
assertThat(NameMangler.apply("특색/새로운"), is("%b9%d2%c9%c0-ps50ht-%5c%b8%b4%c6"));
assertThat(NameMangler.apply("gné/nua"), is("gn%e9_nua-updi5h"));
assertThat(NameMangler.apply("característica/nuevo"), is("caracter%edstica_nuevo-h5da9f"));
assertThat(NameMangler.apply("особенность/новый"), is("%3e%04%41-n168ksdsksof-%04%39%04"));
}
}

0 comments on commit 3105d46

Please sign in to comment.