001/* 002 * Copyright 2007-2021 The jdeb developers. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.vafer.jdeb.utils; 017 018import java.io.ByteArrayOutputStream; 019import java.io.File; 020import java.io.FileNotFoundException; 021import java.io.IOException; 022import java.io.InputStream; 023import java.io.InputStreamReader; 024import java.io.OutputStream; 025import java.text.SimpleDateFormat; 026import java.util.Collection; 027import java.util.Date; 028import java.util.Iterator; 029import java.util.LinkedHashSet; 030import java.util.Map; 031import java.util.TimeZone; 032import java.util.regex.Matcher; 033import java.util.regex.Pattern; 034 035import org.apache.tools.ant.filters.FixCrLfFilter; 036import org.apache.tools.ant.util.ReaderInputStream; 037 038/** 039 * Simple utils functions. 040 * 041 * ATTENTION: don't use outside of jdeb 042 */ 043public final class Utils { 044 private static final Pattern BETA_PATTERN = Pattern.compile("^(?:(?:(.*?)([.\\-_]))|(.*[^a-z]))(alpha|a|beta|b|milestone|m|cr|rc)([^a-z].*)?$", Pattern.CASE_INSENSITIVE); 045 046 private static final Pattern SNAPSHOT_PATTERN = Pattern.compile("(.*)[\\-+]SNAPSHOT"); 047 048 public static int copy( final InputStream pInput, final OutputStream pOutput ) throws IOException { 049 final byte[] buffer = new byte[2048]; 050 int count = 0; 051 int n; 052 while (-1 != (n = pInput.read(buffer))) { 053 pOutput.write(buffer, 0, n); 054 count += n; 055 } 056 return count; 057 } 058 059 public static String toHex( final byte[] bytes ) { 060 final StringBuilder sb = new StringBuilder(); 061 062 for (byte b : bytes) { 063 sb.append(Integer.toHexString((b >> 4) & 0x0f)); 064 sb.append(Integer.toHexString(b & 0x0f)); 065 } 066 067 return sb.toString(); 068 } 069 070 public static String stripPath( final int p, final String s ) { 071 072 if (p <= 0) { 073 return s; 074 } 075 076 int x = 0; 077 for (int i = 0; i < p; i++) { 078 x = s.indexOf('/', x + 1); 079 if (x < 0) { 080 return s; 081 } 082 } 083 084 return s.substring(x + 1); 085 } 086 087 private static String joinPath(char sep, String ...paths) { 088 final StringBuilder sb = new StringBuilder(); 089 for (String p : paths) { 090 if (p == null) continue; 091 if (!p.startsWith("/") && sb.length() > 0) { 092 sb.append(sep); 093 } 094 sb.append(p); 095 } 096 return sb.toString(); 097 } 098 099 public static String joinUnixPath(String ...paths) { 100 return joinPath('/', paths); 101 } 102 103 public static String joinLocalPath(String ...paths) { 104 return joinPath(File.separatorChar, paths); 105 } 106 107 public static String stripLeadingSlash( final String s ) { 108 if (s == null) { 109 return s; 110 } 111 if (s.length() == 0) { 112 return s; 113 } 114 if (s.charAt(0) == '/' || s.charAt(0) == '\\') { 115 return s.substring(1); 116 } 117 return s; 118 } 119 120 /** 121 * Substitute the variables in the given expression with the 122 * values from the resolver 123 * 124 * @param pResolver 125 * @param pExpression 126 */ 127 public static String replaceVariables( final VariableResolver pResolver, final String pExpression, final String pOpen, final String pClose ) { 128 final char[] open = pOpen.toCharArray(); 129 final char[] close = pClose.toCharArray(); 130 131 final StringBuilder out = new StringBuilder(); 132 StringBuilder sb = new StringBuilder(); 133 char[] last = null; 134 int wo = 0; 135 int wc = 0; 136 int level = 0; 137 for (char c : pExpression.toCharArray()) { 138 if (c == open[wo]) { 139 if (wc > 0) { 140 sb.append(close, 0, wc); 141 } 142 wc = 0; 143 wo++; 144 if (open.length == wo) { 145 // found open 146 if (last == open) { 147 out.append(open); 148 } 149 level++; 150 out.append(sb); 151 sb = new StringBuilder(); 152 wo = 0; 153 last = open; 154 } 155 } else if (c == close[wc]) { 156 if (wo > 0) { 157 sb.append(open, 0, wo); 158 } 159 wo = 0; 160 wc++; 161 if (close.length == wc) { 162 // found close 163 if (last == open) { 164 final String variable = pResolver.get(sb.toString()); 165 if (variable != null) { 166 out.append(variable); 167 } else { 168 out.append(open); 169 out.append(sb); 170 out.append(close); 171 } 172 } else { 173 out.append(sb); 174 out.append(close); 175 } 176 sb = new StringBuilder(); 177 level--; 178 wc = 0; 179 last = close; 180 } 181 } else { 182 183 if (wo > 0) { 184 sb.append(open, 0, wo); 185 } 186 187 if (wc > 0) { 188 sb.append(close, 0, wc); 189 } 190 191 sb.append(c); 192 193 wo = wc = 0; 194 } 195 } 196 197 if (wo > 0) { 198 sb.append(open, 0, wo); 199 } 200 201 if (wc > 0) { 202 sb.append(close, 0, wc); 203 } 204 205 if (level > 0) { 206 out.append(open); 207 } 208 out.append(sb); 209 210 return out.toString(); 211 } 212 213 /** 214 * Replaces new line delimiters in the input stream with the Unix line feed. 215 * 216 * @param input 217 */ 218 public static byte[] toUnixLineEndings( InputStream input ) throws IOException { 219 String encoding = "ISO-8859-1"; 220 FixCrLfFilter filter = new FixCrLfFilter(new InputStreamReader(input, encoding)); 221 filter.setEol(FixCrLfFilter.CrLf.newInstance("unix")); 222 223 ByteArrayOutputStream filteredFile = new ByteArrayOutputStream(); 224 Utils.copy(new ReaderInputStream(filter, encoding), filteredFile); 225 226 return filteredFile.toByteArray(); 227 } 228 229 private static String formatSnapshotTemplate( String template, Date timestamp ) { 230 int startBracket = template.indexOf('['); 231 int endBracket = template.indexOf(']'); 232 if(startBracket == -1 || endBracket == -1) { 233 return template; 234 } else { 235 // prefix[yyMMdd]suffix 236 final String date = new SimpleDateFormat(template.substring(startBracket + 1, endBracket)).format(timestamp); 237 String datePrefix = startBracket == 0 ? "" : template.substring(0, startBracket); 238 String dateSuffix = endBracket == template.length() ? "" : template.substring(endBracket + 1); 239 return datePrefix + date + dateSuffix; 240 } 241 } 242 243 /** 244 * Convert the project version to a version suitable for a Debian package. 245 * -SNAPSHOT suffixes are replaced with a timestamp (~yyyyMMddHHmmss). 246 * The separator before a rc, alpha or beta version is replaced with '~' 247 * such that the version is always ordered before the final or GA release. 248 * 249 * @param version the project version to convert to a Debian package version 250 * @param template the template used to replace -SNAPSHOT, the timestamp format is in brackets, 251 * the rest of the string is preserved (prefix[yyMMdd]suffix -> prefix151230suffix) 252 * @param timestamp the UTC date used as the timestamp to replace the SNAPSHOT suffix. 253 */ 254 public static String convertToDebianVersion( String version, boolean apply, String envName, String template, Date timestamp ) { 255 Matcher matcher = SNAPSHOT_PATTERN.matcher(version); 256 if (matcher.matches()) { 257 version = matcher.group(1) + "~"; 258 259 if (apply) { 260 final String envValue = System.getenv(envName); 261 if(template != null && template.length() > 0) { 262 version += formatSnapshotTemplate(template, timestamp); 263 } else if (envValue != null && envValue.length() > 0) { 264 version += envValue; 265 } else { 266 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss"); 267 dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); 268 version += dateFormat.format(timestamp); 269 } 270 } else { 271 version += "SNAPSHOT"; 272 } 273 } else { 274 matcher = BETA_PATTERN.matcher(version); 275 if (matcher.matches()) { 276 if (matcher.group(1) != null) { 277 version = matcher.group(1) + "~" + matcher.group(4) + matcher.group(5); 278 } else { 279 version = matcher.group(3) + "~" + matcher.group(4) + matcher.group(5); 280 } 281 } 282 } 283 284 // safest upstream_version should only contain full stop, plus, tilde, and alphanumerics 285 // https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version 286 version = version.replaceAll("[^.+~A-Za-z0-9]", "+").replaceAll("\\++", "+"); 287 288 return version; 289 } 290 291 /** 292 * Construct new path by replacing file directory part. No 293 * files are actually modified. 294 * @param file path to move 295 * @param target new path directory 296 */ 297 public static String movePath( final String file, 298 final String target ) { 299 final String name = new File(file).getName(); 300 return target.endsWith("/") ? target + name : target + '/' + name; 301 } 302 303 /** 304 * Extracts value from map if given value is null. 305 * @param value current value 306 * @param props properties to extract value from 307 * @param key property name to extract 308 * @return initial value or value extracted from map 309 */ 310 public static String lookupIfEmpty( final String value, 311 final Map<String, String> props, 312 final String key ) { 313 return value != null ? value : props.get(key); 314 } 315 316 /** 317 * Get the known locations where the secure keyring can be located. 318 * Looks through known locations of the GNU PG secure keyring. 319 * 320 * @return The location of the PGP secure keyring if it was found, 321 * null otherwise 322 */ 323 public static Collection<String> getKnownPGPSecureRingLocations() { 324 final LinkedHashSet<String> locations = new LinkedHashSet<>(); 325 326 final String os = System.getProperty("os.name"); 327 final boolean runOnWindows = os == null || os.toLowerCase().contains("win"); 328 329 if (runOnWindows) { 330 // The user's roaming profile on Windows, via environment 331 final String windowsRoaming = System.getenv("APPDATA"); 332 if (windowsRoaming != null) { 333 locations.add(joinLocalPath(windowsRoaming, "gnupg", "secring.gpg")); 334 } 335 336 // The user's local profile on Windows, via environment 337 final String windowsLocal = System.getenv("LOCALAPPDATA"); 338 if (windowsLocal != null) { 339 locations.add(joinLocalPath(windowsLocal, "gnupg", "secring.gpg")); 340 } 341 342 // The Windows installation directory 343 final String windir = System.getProperty("WINDIR"); 344 if (windir != null) { 345 // Local Profile on Windows 98 and ME 346 locations.add(joinLocalPath(windir, "Application Data", "gnupg", "secring.gpg")); 347 } 348 } 349 350 final String home = System.getProperty("user.home"); 351 352 if (home != null && runOnWindows) { 353 // These are for various flavours of Windows 354 // if the environment variables above have failed 355 356 // Roaming profile on Vista and later 357 locations.add(joinLocalPath(home, "AppData", "Roaming", "gnupg", "secring.gpg")); 358 // Local profile on Vista and later 359 locations.add(joinLocalPath(home, "AppData", "Local", "gnupg", "secring.gpg")); 360 // Roaming profile on 2000 and XP 361 locations.add(joinLocalPath(home, "Application Data", "gnupg", "secring.gpg")); 362 // Local profile on 2000 and XP 363 locations.add(joinLocalPath(home, "Local Settings", "Application Data", "gnupg", "secring.gpg")); 364 } 365 366 // *nix, including OS X 367 if (home != null) { 368 locations.add(joinLocalPath(home, ".gnupg", "secring.gpg")); 369 } 370 371 return locations; 372 } 373 374 /** 375 * Tries to guess location of the user secure keyring using various 376 * heuristics. 377 * 378 * @return path to the keyring file 379 * @throws FileNotFoundException if no keyring file found 380 */ 381 public static File guessKeyRingFile() throws FileNotFoundException { 382 final Collection<String> possibleLocations = getKnownPGPSecureRingLocations(); 383 for (final String location : possibleLocations) { 384 final File candidate = new File(location); 385 if (candidate.exists()) { 386 return candidate; 387 } 388 } 389 final StringBuilder message = new StringBuilder("Could not locate secure keyring, locations tried: "); 390 final Iterator<String> it = possibleLocations.iterator(); 391 while (it.hasNext()) { 392 message.append(it.next()); 393 if (it.hasNext()) { 394 message.append(", "); 395 } 396 } 397 throw new FileNotFoundException(message.toString()); 398 } 399 400 /** 401 * Returns true if string is null or empty. 402 */ 403 public static boolean isNullOrEmpty(final String str) { 404 return str == null || str.length() == 0; 405 } 406 407 /** 408 * Return fallback if first string is null or empty 409 */ 410 public static String defaultString(final String str, final String fallback) { 411 return isNullOrEmpty(str) ? fallback : str; 412 } 413 414 415 /** 416 * Check if a CharSequence is whitespace, empty ("") or null. 417 * 418 * <pre> 419 * StringUtils.isBlank(null) = true 420 * StringUtils.isBlank("") = true 421 * StringUtils.isBlank(" ") = true 422 * StringUtils.isBlank("bob") = false 423 * StringUtils.isBlank(" bob ") = false 424 * </pre> 425 * 426 * @param cs 427 * the CharSequence to check, may be null 428 * @return {@code true} if the CharSequence is null, empty or whitespace 429 */ 430 public static boolean isBlank(final CharSequence cs) { 431 int strLen; 432 if (cs == null || (strLen = cs.length()) == 0) { 433 return true; 434 } 435 for (int i = 0; i < strLen; i++) { 436 if (!Character.isWhitespace(cs.charAt(i))) { 437 return false; 438 } 439 } 440 return true; 441 } 442}