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       */
59      private static final Pattern PROJECT_AND_REPO_NAME_PATTERN =
60              Pattern.compile("^[0-9A-Za-z](?:[-+_0-9A-Za-z.]*[0-9A-Za-z])?$");
61  
62      public static String validateFileName(String name, String paramName) {
63          requireNonNull(name, paramName);
64          checkArgument(isValidFileName(name),
65                        "%s: %s (expected: %s)", paramName, name, FILE_NAME_PATTERN);
66          return name;
67      }
68  
69      public static boolean isValidFileName(String name) {
70          requireNonNull(name, "name");
71          return !name.isEmpty() && FILE_NAME_PATTERN.matcher(name).matches();
72      }
73  
74      public static String validateFilePath(String path, String paramName) {
75          requireNonNull(path, paramName);
76          checkArgument(isValidFilePath(path),
77                        "%s: %s (expected: %s)", paramName, path, FILE_PATH_PATTERN);
78          return path;
79      }
80  
81      public static boolean isValidFilePath(String path) {
82          requireNonNull(path, "path");
83          return !path.isEmpty() && path.charAt(0) == '/' &&
84                 FILE_PATH_PATTERN.matcher(path).matches();
85      }
86  
87      public static String validateJsonFilePath(String path, String paramName) {
88          requireNonNull(path, paramName);
89          checkArgument(isValidJsonFilePath(path),
90                        "%s: %s (expected: %s)", paramName, path, JSON_FILE_PATH_PATTERN);
91          return path;
92      }
93  
94      public static boolean isValidJsonFilePath(String path) {
95          requireNonNull(path, "path");
96          return !path.isEmpty() && path.charAt(0) == '/' &&
97                 JSON_FILE_PATH_PATTERN.matcher(path).matches();
98      }
99  
100     public static String validateJsonPath(String jsonPath, String paramName) {
101         requireNonNull(jsonPath, paramName);
102         checkArgument(isValidJsonPath(jsonPath),
103                       "%s: %s (expected: a valid JSON path)", paramName, jsonPath);
104         return jsonPath;
105     }
106 
107     public static boolean isValidJsonPath(String jsonPath) {
108         try {
109             JsonPath.compile(jsonPath);
110             return true;
111         } catch (Exception e) {
112             return false;
113         }
114     }
115 
116     public static String validateDirPath(String path, String paramName) {
117         requireNonNull(path, paramName);
118         checkArgument(isValidDirPath(path),
119                       "%s: %s (expected: %s)", paramName, path, DIR_PATH_PATTERN);
120         return path;
121     }
122 
123     public static boolean isValidDirPath(String path) {
124         return isValidDirPath(path, false);
125     }
126 
127     public static boolean isValidDirPath(String path, boolean mustEndWithSlash) {
128         requireNonNull(path);
129         if (mustEndWithSlash && !path.endsWith("/")) {
130             return false;
131         }
132         return !path.isEmpty() && path.charAt(0) == '/' &&
133                DIR_PATH_PATTERN.matcher(path).matches();
134     }
135 
136     public static String validatePathPattern(String pathPattern, String paramName) {
137         requireNonNull(pathPattern, paramName);
138         checkArgument(isValidPathPattern(pathPattern),
139                       "%s: %s (expected: %s)", paramName, pathPattern, PATH_PATTERN_PATTERN);
140         return pathPattern;
141     }
142 
143     public static boolean isValidPathPattern(String pathPattern) {
144         requireNonNull(pathPattern, "pathPattern");
145         return PATH_PATTERN_PATTERN.matcher(pathPattern).matches();
146     }
147 
148     public static String validateProjectName(String projectName, String paramName) {
149         requireNonNull(projectName, paramName);
150         checkArgument(isValidProjectName(projectName),
151                       "%s: %s (expected: %s)", paramName, projectName, PROJECT_AND_REPO_NAME_PATTERN);
152         return projectName;
153     }
154 
155     public static boolean isValidProjectName(String projectName) {
156         requireNonNull(projectName, "projectName");
157         return PROJECT_AND_REPO_NAME_PATTERN.matcher(projectName).matches();
158     }
159 
160     public static String validateRepositoryName(String repoName, String paramName) {
161         requireNonNull(repoName, paramName);
162         checkArgument(isValidRepositoryName(repoName),
163                       "%s: %s (expected: %s)", paramName, repoName, PROJECT_AND_REPO_NAME_PATTERN);
164         return repoName;
165     }
166 
167     public static boolean isValidRepositoryName(String repoName) {
168         requireNonNull(repoName, "repoName");
169         return PROJECT_AND_REPO_NAME_PATTERN.matcher(repoName).matches();
170     }
171 
172     public static String validateEmailAddress(String emailAddr, String paramName) {
173         requireNonNull(emailAddr, paramName);
174         checkArgument(isValidEmailAddress(emailAddr),
175                       "%s: %s (expected: a valid e-mail address)", paramName, emailAddr);
176         return emailAddr;
177     }
178 
179     public static boolean isValidEmailAddress(String emailAddr) {
180         requireNonNull(emailAddr, "emailAddr");
181         if (EMAIL_PATTERN.matcher(emailAddr).matches()) {
182             return true;
183         }
184         // Try to check whether the domain part is IP address format.
185         final Matcher m = GENERAL_EMAIL_PATTERN.matcher(emailAddr);
186         if (m.matches()) {
187             final String domainPart = m.group(1);
188             return isValidIpV4Address(domainPart) ||
189                    isValidIpV6Address(domainPart);
190         }
191         return false;
192     }
193 
194     public static String toEmailAddress(String emailAddr, String paramName) {
195         requireNonNull(emailAddr, paramName);
196         if (isValidEmailAddress(emailAddr)) {
197             return emailAddr;
198         }
199         return emailAddr + "@localhost.localdomain";
200     }
201 
202     public static String emailToUsername(String emailAddr, String paramName) {
203         validateEmailAddress(emailAddr, paramName);
204         return emailAddr.substring(0, emailAddr.indexOf('@'));
205     }
206 
207     public static List<String> stringToLines(String str) {
208         final BufferedReader reader = new BufferedReader(new StringReader(str));
209         final List<String> lines = new ArrayList<>(128);
210         try {
211             String line;
212             while ((line = reader.readLine()) != null) {
213                 lines.add(line);
214             }
215         } catch (IOException ignored) {
216             // Should never happen.
217         }
218         return lines;
219     }
220 
221     /**
222      * Returns the simplified name of the type of the specified object.
223      */
224     public static String simpleTypeName(Object obj) {
225         if (obj == null) {
226             return "null";
227         }
228 
229         return simpleTypeName(obj.getClass(), false);
230     }
231 
232     /**
233      * Returns the simplified name of the specified type.
234      */
235     public static String simpleTypeName(Class<?> clazz) {
236         return simpleTypeName(clazz, false);
237     }
238 
239     /**
240      * Returns the simplified and (optionally) decapitalized name of the specified type.
241      */
242     public static String simpleTypeName(Class<?> clazz, boolean decapitalize) {
243         if (clazz == null) {
244             return "null";
245         }
246 
247         String className = clazz.getName();
248         final int lastDotIdx = className.lastIndexOf('.');
249         if (lastDotIdx >= 0) {
250             className = className.substring(lastDotIdx + 1);
251         }
252 
253         if (!decapitalize) {
254             return className;
255         }
256 
257         final StringBuilder buf = new StringBuilder(className.length());
258         boolean lowercase = true;
259         for (int i = 0; i < className.length(); i++) {
260             final char c1 = className.charAt(i);
261             final char c2;
262             if (lowercase) {
263                 c2 = Character.toLowerCase(c1);
264                 if (c1 == c2) {
265                     lowercase = false;
266                 }
267             } else {
268                 c2 = c1;
269             }
270             buf.append(c2);
271         }
272 
273         return buf.toString();
274     }
275 
276     /**
277      * Casts an object unsafely. Used when you want to suppress the unchecked type warnings.
278      */
279     @SuppressWarnings("unchecked")
280     public static <T> T unsafeCast(Object o) {
281         return (T) o;
282     }
283 
284     /**
285      * Makes sure the specified {@code values} and all its elements are not {@code null}.
286      */
287     public static <T> Iterable<T> requireNonNullElements(Iterable<T> values, String name) {
288         requireNonNull(values, name);
289 
290         int i = 0;
291         for (T v : values) {
292             if (v == null) {
293                 throw new NullPointerException(name + '[' + i + ']');
294             }
295             i++;
296         }
297 
298         return values;
299     }
300 
301     private static boolean isValidIpV4Address(String ip) {
302         return isValidIpV4Address(ip, 0, ip.length());
303     }
304 
305     @SuppressWarnings("DuplicateBooleanBranch")
306     private static boolean isValidIpV4Address(String ip, int from, int toExcluded) {
307         final int len = toExcluded - from;
308         int i;
309         return len <= 15 && len >= 7 &&
310                (i = ip.indexOf('.', from + 1)) > 0 && isValidIpV4Word(ip, from, i) &&
311                (i = ip.indexOf('.', from = i + 2)) > 0 && isValidIpV4Word(ip, from - 1, i) &&
312                (i = ip.indexOf('.', from = i + 2)) > 0 && isValidIpV4Word(ip, from - 1, i) &&
313                isValidIpV4Word(ip, i + 1, toExcluded);
314     }
315 
316     private static boolean isValidIpV4Word(CharSequence word, int from, int toExclusive) {
317         final int len = toExclusive - from;
318         final char c0;
319         final char c1;
320         final char c2;
321         if (len < 1 || len > 3 || (c0 = word.charAt(from)) < '0') {
322             return false;
323         }
324         if (len == 3) {
325             return (c1 = word.charAt(from + 1)) >= '0' &&
326                    (c2 = word.charAt(from + 2)) >= '0' &&
327                    (c0 <= '1' && c1 <= '9' && c2 <= '9' ||
328                     c0 == '2' && c1 <= '5' && (c2 <= '5' || c1 < '5' && c2 <= '9'));
329         }
330         return c0 <= '9' && (len == 1 || isValidNumericChar(word.charAt(from + 1)));
331     }
332 
333     private static boolean isValidIpV6Address(String ip) {
334         int end = ip.length();
335         if (end < 2) {
336             return false;
337         }
338 
339         // strip "[]"
340         int start;
341         char c = ip.charAt(0);
342         if (c == '[') {
343             end--;
344             if (ip.charAt(end) != ']') {
345                 // must have a close ]
346                 return false;
347             }
348             start = 1;
349             c = ip.charAt(1);
350         } else {
351             start = 0;
352         }
353 
354         int colons;
355         int compressBegin;
356         if (c == ':') {
357             // an IPv6 address can start with "::" or with a number
358             if (ip.charAt(start + 1) != ':') {
359                 return false;
360             }
361             colons = 2;
362             compressBegin = start;
363             start += 2;
364         } else {
365             colons = 0;
366             compressBegin = -1;
367         }
368 
369         int wordLen = 0;
370         loop:
371         for (int i = start; i < end; i++) {
372             c = ip.charAt(i);
373             if (isValidHexChar(c)) {
374                 if (wordLen < 4) {
375                     wordLen++;
376                     continue;
377                 }
378                 return false;
379             }
380 
381             switch (c) {
382                 case ':':
383                     if (colons > 7) {
384                         return false;
385                     }
386                     if (ip.charAt(i - 1) == ':') {
387                         if (compressBegin >= 0) {
388                             return false;
389                         }
390                         compressBegin = i - 1;
391                     } else {
392                         wordLen = 0;
393                     }
394                     colons++;
395                     break;
396                 case '.':
397                     // case for the last 32-bits represented as IPv4 x:x:x:x:x:x:d.d.d.d
398 
399                     // check a normal case (6 single colons)
400                     if (compressBegin < 0 && colons != 6 ||
401                         // a special case ::1:2:3:4:5:d.d.d.d allows 7 colons with an
402                         // IPv4 ending, otherwise 7 :'s is bad
403                         colons == 7 && compressBegin >= start || colons > 7) {
404                         return false;
405                     }
406 
407                     // Verify this address is of the correct structure to contain an IPv4 address.
408                     // It must be IPv4-Mapped or IPv4-Compatible
409                     // (see https://tools.ietf.org/html/rfc4291#section-2.5.5).
410                     final int ipv4Start = i - wordLen;
411                     int j = ipv4Start - 2; // index of character before the previous ':'.
412                     if (isValidIPv4MappedChar(ip.charAt(j))) {
413                         if (!isValidIPv4MappedChar(ip.charAt(j - 1)) ||
414                             !isValidIPv4MappedChar(ip.charAt(j - 2)) ||
415                             !isValidIPv4MappedChar(ip.charAt(j - 3))) {
416                             return false;
417                         }
418                         j -= 5;
419                     }
420 
421                     for (; j >= start; --j) {
422                         final char tmpChar = ip.charAt(j);
423                         if (tmpChar != '0' && tmpChar != ':') {
424                             return false;
425                         }
426                     }
427 
428                     // 7 - is minimum IPv4 address length
429                     int ipv4End = ip.indexOf('%', ipv4Start + 7);
430                     if (ipv4End < 0) {
431                         ipv4End = end;
432                     }
433                     return isValidIpV4Address(ip, ipv4Start, ipv4End);
434                 case '%':
435                     // strip the interface name/index after the percent sign
436                     end = i;
437                     break loop;
438                 default:
439                     return false;
440             }
441         }
442 
443         // normal case without compression
444         if (compressBegin < 0) {
445             return colons == 7 && wordLen > 0;
446         }
447 
448         return compressBegin + 2 == end ||
449                // 8 colons is valid only if compression in start or end
450                wordLen > 0 && (colons < 8 || compressBegin <= start);
451     }
452 
453     private static boolean isValidNumericChar(char c) {
454         return c >= '0' && c <= '9';
455     }
456 
457     private static boolean isValidIPv4MappedChar(char c) {
458         return c == 'f' || c == 'F';
459     }
460 
461     private static boolean isValidHexChar(char c) {
462         return c >= '0' && c <= '9' || c >= 'A' && c <= 'F' || c >= 'a' && c <= 'f';
463     }
464 
465     /**
466      * Deletes the specified {@code directory} recursively.
467      */
468     public static void deleteFileTree(File directory) throws IOException {
469         if (directory.exists()) {
470             Files.walkFileTree(directory.toPath(), DeletingFileVisitor.INSTANCE);
471         }
472     }
473 
474     private Util() {}
475 }