1   /*
2    * Copyright 2017 LINE Corporation
3    *
4    * LINE Corporation licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  
17  package com.linecorp.centraldogma.internal;
18  
19  import static com.google.common.base.Preconditions.checkArgument;
20  import static java.util.Objects.requireNonNull;
21  
22  import java.io.BufferedReader;
23  import java.io.File;
24  import java.io.IOException;
25  import java.io.StringReader;
26  import java.nio.file.Files;
27  import java.util.ArrayList;
28  import java.util.List;
29  import java.util.regex.Matcher;
30  import java.util.regex.Pattern;
31  
32  import com.jayway.jsonpath.JsonPath;
33  
34  /**
35   * This class borrowed some of its methods from a <a href="https://github.com/netty/netty/blob/4.1/common
36   * /src/main/java/io/netty/util/NetUtil.java">NetUtil class</a> which was part of Netty project.
37   */
38  public final class Util {
39  
40      private static final Pattern FILE_NAME_PATTERN = Pattern.compile(
41              "^(?:[-_0-9a-zA-Z](?:[-_.0-9a-zA-Z]*[-_0-9a-zA-Z])?)+$");
42      private static final Pattern FILE_PATH_PATTERN = Pattern.compile(
43              "^(?:/[-_0-9a-zA-Z](?:[-_.0-9a-zA-Z]*[-_0-9a-zA-Z])?)+$");
44      private static final Pattern JSON_FILE_PATH_PATTERN = Pattern.compile(
45              "^(?:/[-_0-9a-zA-Z](?:[-_.0-9a-zA-Z]*[-_0-9a-zA-Z])?)+\\.(?i)json$");
46      private static final Pattern DIR_PATH_PATTERN = Pattern.compile(
47              "^(?:/[-_0-9a-zA-Z](?:[-_.0-9a-zA-Z]*[-_0-9a-zA-Z])?)*/?$");
48      private static final Pattern PATH_PATTERN_PATTERN = Pattern.compile("^[- /*_.,0-9a-zA-Z]+$");
49      private static final Pattern EMAIL_PATTERN = Pattern.compile(
50              "^[_A-Za-z0-9-+]+(?:\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(?:\\.[A-Za-z0-9]+)*(?:\\.[A-Za-z]{2,})$");
51      private static final Pattern GENERAL_EMAIL_PATTERN = Pattern.compile(
52              "^[_A-Za-z0-9-+]+(?:\\.[_A-Za-z0-9-]+)*@(.+)$");
53  
54      /**
55       * Start with an alphanumeric character.
56       * An alphanumeric character, minus, plus, underscore and dot are allowed in the middle.
57       * End with an alphanumeric character.
58       * Use this pattern for project and repository names that are entered by users.
59       */
60      public static final Pattern USER_INPUT_PROJECT_AND_REPO_NAME_PATTERN =
61              Pattern.compile("^(?!.*\\.git$)[0-9A-Za-z](?:[-+_0-9A-Za-z.]*[0-9A-Za-z])?$");
62  
63      public static final String INTERNAL_PROJECT_PREFIX = "@";
64  
65      /**
66       * The difference between this and {@link #USER_INPUT_PROJECT_AND_REPO_NAME_PATTERN} is that this
67       * allows {@value INTERNAL_PROJECT_PREFIX} at the beginning for internal projects.
68       */
69      public static final Pattern PROJECT_AND_REPO_NAME_PATTERN =
70              Pattern.compile("^(?!.*\\.git$)(" + INTERNAL_PROJECT_PREFIX +
71                              "|[0-9A-Za-z])(?:[-+_0-9A-Za-z.]*[0-9A-Za-z])?$");
72  
73      public static String validateFileName(String name, String paramName) {
74          requireNonNull(name, paramName);
75          checkArgument(isValidFileName(name),
76                        "%s: %s (expected: %s)", paramName, name, FILE_NAME_PATTERN);
77          return name;
78      }
79  
80      public static boolean isValidFileName(String name) {
81          requireNonNull(name, "name");
82          return !name.isEmpty() && FILE_NAME_PATTERN.matcher(name).matches();
83      }
84  
85      public static String validateFilePath(String path, String paramName) {
86          requireNonNull(path, paramName);
87          checkArgument(isValidFilePath(path),
88                        "%s: %s (expected: %s)", paramName, path, FILE_PATH_PATTERN);
89          return path;
90      }
91  
92      public static boolean isValidFilePath(String path) {
93          requireNonNull(path, "path");
94          return !path.isEmpty() && path.charAt(0) == '/' &&
95                 FILE_PATH_PATTERN.matcher(path).matches();
96      }
97  
98      public static String validateJsonFilePath(String path, String paramName) {
99          requireNonNull(path, paramName);
100         checkArgument(isValidJsonFilePath(path),
101                       "%s: %s (expected: %s)", paramName, path, JSON_FILE_PATH_PATTERN);
102         return path;
103     }
104 
105     public static boolean isValidJsonFilePath(String path) {
106         requireNonNull(path, "path");
107         return !path.isEmpty() && path.charAt(0) == '/' &&
108                JSON_FILE_PATH_PATTERN.matcher(path).matches();
109     }
110 
111     public static String validateJsonPath(String jsonPath, String paramName) {
112         requireNonNull(jsonPath, paramName);
113         checkArgument(isValidJsonPath(jsonPath),
114                       "%s: %s (expected: a valid JSON path)", paramName, jsonPath);
115         return jsonPath;
116     }
117 
118     public static boolean isValidJsonPath(String jsonPath) {
119         try {
120             JsonPath.compile(jsonPath);
121             return true;
122         } catch (Exception e) {
123             return false;
124         }
125     }
126 
127     public static String validateDirPath(String path, String paramName) {
128         requireNonNull(path, paramName);
129         checkArgument(isValidDirPath(path),
130                       "%s: %s (expected: %s)", paramName, path, DIR_PATH_PATTERN);
131         return path;
132     }
133 
134     public static boolean isValidDirPath(String path) {
135         return isValidDirPath(path, false);
136     }
137 
138     public static boolean isValidDirPath(String path, boolean mustEndWithSlash) {
139         requireNonNull(path);
140         if (mustEndWithSlash && !path.endsWith("/")) {
141             return false;
142         }
143         return !path.isEmpty() && path.charAt(0) == '/' &&
144                DIR_PATH_PATTERN.matcher(path).matches();
145     }
146 
147     public static String validatePathPattern(String pathPattern, String paramName) {
148         requireNonNull(pathPattern, paramName);
149         checkArgument(isValidPathPattern(pathPattern),
150                       "%s: %s (expected: %s)", paramName, pathPattern, PATH_PATTERN_PATTERN);
151         return pathPattern;
152     }
153 
154     public static boolean isValidPathPattern(String pathPattern) {
155         requireNonNull(pathPattern, "pathPattern");
156         return PATH_PATTERN_PATTERN.matcher(pathPattern).matches();
157     }
158 
159     public static String validateProjectName(String projectName, String paramName, boolean allowInternal) {
160         requireNonNull(projectName, paramName);
161         if (allowInternal) {
162             checkArgument(isValidProjectName(projectName, true),
163                           "%s: %s (expected: %s)", paramName, projectName,
164                           PROJECT_AND_REPO_NAME_PATTERN);
165         } else {
166             checkArgument(isValidProjectName(projectName, false),
167                           "%s: %s (expected: %s)", paramName, projectName,
168                           USER_INPUT_PROJECT_AND_REPO_NAME_PATTERN);
169         }
170         return projectName;
171     }
172 
173     public static boolean isValidProjectName(String projectName, boolean allowInternal) {
174         requireNonNull(projectName, "projectName");
175         if (allowInternal) {
176             return PROJECT_AND_REPO_NAME_PATTERN.matcher(projectName).matches();
177         } else {
178             return USER_INPUT_PROJECT_AND_REPO_NAME_PATTERN.matcher(projectName).matches();
179         }
180     }
181 
182     public static boolean isValidProjectName(String projectName) {
183         requireNonNull(projectName, "projectName");
184         return USER_INPUT_PROJECT_AND_REPO_NAME_PATTERN.matcher(projectName).matches();
185     }
186 
187     public static String validateRepositoryName(String repoName, String paramName) {
188         requireNonNull(repoName, paramName);
189         checkArgument(isValidRepositoryName(repoName),
190                       "%s: %s (expected: %s)", paramName, repoName, USER_INPUT_PROJECT_AND_REPO_NAME_PATTERN);
191         return repoName;
192     }
193 
194     public static boolean isValidRepositoryName(String repoName) {
195         requireNonNull(repoName, "repoName");
196         return USER_INPUT_PROJECT_AND_REPO_NAME_PATTERN.matcher(repoName).matches();
197     }
198 
199     public static String validateEmailAddress(String emailAddr, String paramName) {
200         requireNonNull(emailAddr, paramName);
201         checkArgument(isValidEmailAddress(emailAddr),
202                       "%s: %s (expected: a valid e-mail address)", paramName, emailAddr);
203         return emailAddr;
204     }
205 
206     public static boolean isValidEmailAddress(String emailAddr) {
207         requireNonNull(emailAddr, "emailAddr");
208         if (EMAIL_PATTERN.matcher(emailAddr).matches()) {
209             return true;
210         }
211         // Try to check whether the domain part is IP address format.
212         final Matcher m = GENERAL_EMAIL_PATTERN.matcher(emailAddr);
213         if (m.matches()) {
214             final String domainPart = m.group(1);
215             return isValidIpV4Address(domainPart) ||
216                    isValidIpV6Address(domainPart);
217         }
218         return false;
219     }
220 
221     public static String toEmailAddress(String emailAddr, String paramName) {
222         requireNonNull(emailAddr, paramName);
223         if (isValidEmailAddress(emailAddr)) {
224             return emailAddr;
225         }
226         return emailAddr + "@localhost.localdomain";
227     }
228 
229     public static String emailToUsername(String emailAddr, String paramName) {
230         validateEmailAddress(emailAddr, paramName);
231         return emailAddr.substring(0, emailAddr.indexOf('@'));
232     }
233 
234     public static List<String> stringToLines(String str) {
235         final BufferedReader reader = new BufferedReader(new StringReader(str));
236         final List<String> lines = new ArrayList<>(128);
237         try {
238             String line;
239             while ((line = reader.readLine()) != null) {
240                 lines.add(line);
241             }
242         } catch (IOException ignored) {
243             // Should never happen.
244         }
245         return lines;
246     }
247 
248     /**
249      * Returns the simplified name of the type of the specified object.
250      */
251     public static String simpleTypeName(Object obj) {
252         if (obj == null) {
253             return "null";
254         }
255 
256         return simpleTypeName(obj.getClass(), false);
257     }
258 
259     /**
260      * Returns the simplified name of the specified type.
261      */
262     public static String simpleTypeName(Class<?> clazz) {
263         return simpleTypeName(clazz, false);
264     }
265 
266     /**
267      * Returns the simplified and (optionally) decapitalized name of the specified type.
268      */
269     public static String simpleTypeName(Class<?> clazz, boolean decapitalize) {
270         if (clazz == null) {
271             return "null";
272         }
273 
274         String className = clazz.getName();
275         final int lastDotIdx = className.lastIndexOf('.');
276         if (lastDotIdx >= 0) {
277             className = className.substring(lastDotIdx + 1);
278         }
279 
280         if (!decapitalize) {
281             return className;
282         }
283 
284         final StringBuilder buf = new StringBuilder(className.length());
285         boolean lowercase = true;
286         for (int i = 0; i < className.length(); i++) {
287             final char c1 = className.charAt(i);
288             final char c2;
289             if (lowercase) {
290                 c2 = Character.toLowerCase(c1);
291                 if (c1 == c2) {
292                     lowercase = false;
293                 }
294             } else {
295                 c2 = c1;
296             }
297             buf.append(c2);
298         }
299 
300         return buf.toString();
301     }
302 
303     /**
304      * Casts an object unsafely. Used when you want to suppress the unchecked type warnings.
305      */
306     @SuppressWarnings("unchecked")
307     public static <T> T unsafeCast(Object o) {
308         return (T) o;
309     }
310 
311     /**
312      * Makes sure the specified {@code values} and all its elements are not {@code null}.
313      */
314     public static <T> Iterable<T> requireNonNullElements(Iterable<T> values, String name) {
315         requireNonNull(values, name);
316 
317         int i = 0;
318         for (T v : values) {
319             if (v == null) {
320                 throw new NullPointerException(name + '[' + i + ']');
321             }
322             i++;
323         }
324 
325         return values;
326     }
327 
328     private static boolean isValidIpV4Address(String ip) {
329         return isValidIpV4Address(ip, 0, ip.length());
330     }
331 
332     @SuppressWarnings("DuplicateBooleanBranch")
333     private static boolean isValidIpV4Address(String ip, int from, int toExcluded) {
334         final int len = toExcluded - from;
335         int i;
336         return len <= 15 && len >= 7 &&
337                (i = ip.indexOf('.', from + 1)) > 0 && isValidIpV4Word(ip, from, i) &&
338                (i = ip.indexOf('.', from = i + 2)) > 0 && isValidIpV4Word(ip, from - 1, i) &&
339                (i = ip.indexOf('.', from = i + 2)) > 0 && isValidIpV4Word(ip, from - 1, i) &&
340                isValidIpV4Word(ip, i + 1, toExcluded);
341     }
342 
343     private static boolean isValidIpV4Word(CharSequence word, int from, int toExclusive) {
344         final int len = toExclusive - from;
345         final char c0;
346         final char c1;
347         final char c2;
348         if (len < 1 || len > 3 || (c0 = word.charAt(from)) < '0') {
349             return false;
350         }
351         if (len == 3) {
352             return (c1 = word.charAt(from + 1)) >= '0' &&
353                    (c2 = word.charAt(from + 2)) >= '0' &&
354                    (c0 <= '1' && c1 <= '9' && c2 <= '9' ||
355                     c0 == '2' && c1 <= '5' && (c2 <= '5' || c1 < '5' && c2 <= '9'));
356         }
357         return c0 <= '9' && (len == 1 || isValidNumericChar(word.charAt(from + 1)));
358     }
359 
360     private static boolean isValidIpV6Address(String ip) {
361         int end = ip.length();
362         if (end < 2) {
363             return false;
364         }
365 
366         // strip "[]"
367         int start;
368         char c = ip.charAt(0);
369         if (c == '[') {
370             end--;
371             if (ip.charAt(end) != ']') {
372                 // must have a close ]
373                 return false;
374             }
375             start = 1;
376             c = ip.charAt(1);
377         } else {
378             start = 0;
379         }
380 
381         int colons;
382         int compressBegin;
383         if (c == ':') {
384             // an IPv6 address can start with "::" or with a number
385             if (ip.charAt(start + 1) != ':') {
386                 return false;
387             }
388             colons = 2;
389             compressBegin = start;
390             start += 2;
391         } else {
392             colons = 0;
393             compressBegin = -1;
394         }
395 
396         int wordLen = 0;
397         loop:
398         for (int i = start; i < end; i++) {
399             c = ip.charAt(i);
400             if (isValidHexChar(c)) {
401                 if (wordLen < 4) {
402                     wordLen++;
403                     continue;
404                 }
405                 return false;
406             }
407 
408             switch (c) {
409                 case ':':
410                     if (colons > 7) {
411                         return false;
412                     }
413                     if (ip.charAt(i - 1) == ':') {
414                         if (compressBegin >= 0) {
415                             return false;
416                         }
417                         compressBegin = i - 1;
418                     } else {
419                         wordLen = 0;
420                     }
421                     colons++;
422                     break;
423                 case '.':
424                     // case for the last 32-bits represented as IPv4 x:x:x:x:x:x:d.d.d.d
425 
426                     // check a normal case (6 single colons)
427                     if (compressBegin < 0 && colons != 6 ||
428                         // a special case ::1:2:3:4:5:d.d.d.d allows 7 colons with an
429                         // IPv4 ending, otherwise 7 :'s is bad
430                         colons == 7 && compressBegin >= start || colons > 7) {
431                         return false;
432                     }
433 
434                     // Verify this address is of the correct structure to contain an IPv4 address.
435                     // It must be IPv4-Mapped or IPv4-Compatible
436                     // (see https://tools.ietf.org/html/rfc4291#section-2.5.5).
437                     final int ipv4Start = i - wordLen;
438                     int j = ipv4Start - 2; // index of character before the previous ':'.
439                     if (isValidIPv4MappedChar(ip.charAt(j))) {
440                         if (!isValidIPv4MappedChar(ip.charAt(j - 1)) ||
441                             !isValidIPv4MappedChar(ip.charAt(j - 2)) ||
442                             !isValidIPv4MappedChar(ip.charAt(j - 3))) {
443                             return false;
444                         }
445                         j -= 5;
446                     }
447 
448                     for (; j >= start; --j) {
449                         final char tmpChar = ip.charAt(j);
450                         if (tmpChar != '0' && tmpChar != ':') {
451                             return false;
452                         }
453                     }
454 
455                     // 7 - is minimum IPv4 address length
456                     int ipv4End = ip.indexOf('%', ipv4Start + 7);
457                     if (ipv4End < 0) {
458                         ipv4End = end;
459                     }
460                     return isValidIpV4Address(ip, ipv4Start, ipv4End);
461                 case '%':
462                     // strip the interface name/index after the percent sign
463                     end = i;
464                     break loop;
465                 default:
466                     return false;
467             }
468         }
469 
470         // normal case without compression
471         if (compressBegin < 0) {
472             return colons == 7 && wordLen > 0;
473         }
474 
475         return compressBegin + 2 == end ||
476                // 8 colons is valid only if compression in start or end
477                wordLen > 0 && (colons < 8 || compressBegin <= start);
478     }
479 
480     private static boolean isValidNumericChar(char c) {
481         return c >= '0' && c <= '9';
482     }
483 
484     private static boolean isValidIPv4MappedChar(char c) {
485         return c == 'f' || c == 'F';
486     }
487 
488     private static boolean isValidHexChar(char c) {
489         return c >= '0' && c <= '9' || c >= 'A' && c <= 'F' || c >= 'a' && c <= 'f';
490     }
491 
492     /**
493      * Deletes the specified {@code directory} recursively.
494      */
495     public static void deleteFileTree(File directory) throws IOException {
496         if (directory.exists()) {
497             Files.walkFileTree(directory.toPath(), DeletingFileVisitor.INSTANCE);
498         }
499     }
500 
501     private Util() {}
502 }