View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.eclipse.aether.internal.impl.filter.prefixes;
20  
21  import java.io.BufferedReader;
22  import java.io.IOException;
23  import java.nio.charset.StandardCharsets;
24  import java.nio.file.Files;
25  import java.nio.file.NoSuchFileException;
26  import java.nio.file.Path;
27  import java.util.ArrayList;
28  import java.util.Collections;
29  import java.util.List;
30  
31  import org.eclipse.aether.repository.RemoteRepository;
32  
33  import static java.util.Objects.requireNonNull;
34  
35  /**
36   * Prefixes source and parser.
37   * <p>
38   * This class is "clean room" reimplementation of
39   * <a href="https://github.com/sonatype/nexus-public/blob/daf1e9c2844282132063f1d8bad914c93efa3d0e/components/nexus-core/src/main/java/org/sonatype/nexus/proxy/maven/routing/internal/TextFilePrefixSourceMarshaller.java">original class</a>.
40   *
41   * @since 2.0.11
42   */
43  public interface PrefixesSource {
44      /**
45       * The origin repository of this source.
46       */
47      RemoteRepository origin();
48  
49      /**
50       * The file path (ie local repository or user provided one) this source got entries from.
51       */
52      Path path();
53  
54      /**
55       * Message worth logging if {@link #valid()} returns {@code false}.
56       */
57      String message();
58  
59      /**
60       * Returns {@code true} if source is valid and contains valid entries.
61       */
62      boolean valid();
63  
64      /**
65       * The prefix entries.
66       */
67      List<String> entries();
68  
69      /**
70       * Creates {@link PrefixesSource} out of passed in parameters, never returns {@code null}. The returned
71       * source should be checked for {@link #valid()} and use only if it returns {@code true}.
72       * <p>
73       * This method is "forgiving" to all kind of IO problems while reading (file not found, etc.) and will never
74       * throw {@link IOException} as prefix file processing should not interrupt main flow due which prefix file
75       * processing is happening in the first place. Ideally, user is notified at least by logging if any problem happens.
76       */
77      static PrefixesSource of(RemoteRepository origin, Path path) {
78          requireNonNull(origin, "origin is null");
79          requireNonNull(path, "path is null");
80          return new Parser(origin, path).parse();
81      }
82  
83      final class Parser {
84          private static final String PREFIX_MAGIC = "## repository-prefixes/2.0";
85          private static final String PREFIX_LEGACY_MAGIC = "# Prefix file generated by Sonatype Nexus";
86          private static final String PREFIX_UNSUPPORTED = "@ unsupported";
87          private static final int MAX_ENTRIES = 100_000;
88          private static final int MAX_LINE_LENGTH = 250;
89  
90          private final RemoteRepository origin;
91          private final Path path;
92  
93          private Parser(RemoteRepository origin, Path path) {
94              this.origin = origin;
95              this.path = path;
96          }
97  
98          private PrefixesSource parse() {
99              try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
100                 ArrayList<String> entries = new ArrayList<>();
101                 String line = reader.readLine();
102                 if (!PREFIX_MAGIC.equals(line)
103                         && !PREFIX_LEGACY_MAGIC.equals(line)
104                         && !PREFIX_UNSUPPORTED.equals(line)) {
105                     return invalid(origin, path, "No expected magic in file");
106                 }
107                 while (line != null) {
108                     line = line.trim();
109                     if (PREFIX_UNSUPPORTED.equals(line)) {
110                         // abort; if file contains this line anywhere is unsupported
111                         return invalid(origin, path, "Declares itself unsupported");
112                     }
113                     if (!line.startsWith("#") && !line.isEmpty()) {
114                         // entry length
115                         if (line.length() > MAX_LINE_LENGTH) {
116                             return invalid(origin, path, "Contains too long line");
117                         }
118                         // entry should be ASCII subset
119                         if (!line.chars().allMatch(c -> c < 128)) {
120                             return invalid(origin, path, "Contains non-ASCII characters");
121                         }
122                         // entry should be actually a bit less than ASCII, filtering most certain characters
123                         if (line.contains(":")
124                                 || line.contains("<")
125                                 || line.contains(">")
126                                 || line.contains("\\")
127                                 || line.contains("//")) {
128                             return invalid(origin, path, "Contains forbidden characters");
129                         }
130 
131                         // strip leading dot if needed (ie manually crafted file using UN*X find command)
132                         while (line.startsWith(".")) {
133                             line = line.substring(1);
134                         }
135                         entries.add(line);
136                     }
137                     line = reader.readLine();
138 
139                     // dump big files
140                     if (entries.size() > MAX_ENTRIES) {
141                         return invalid(origin, path, "Contains too many entries");
142                     }
143                 }
144                 return new Impl(origin, path, "OK", true, Collections.unmodifiableList(entries));
145             } catch (NoSuchFileException e) {
146                 return invalid(origin, path, "No such file");
147             } catch (IOException e) {
148                 return invalid(origin, path, "Could not read the file: " + e.getMessage());
149             }
150         }
151 
152         private static PrefixesSource invalid(RemoteRepository origin, Path path, String message) {
153             return new Impl(origin, path, message, false, Collections.emptyList());
154         }
155 
156         private static class Impl implements PrefixesSource {
157             private final RemoteRepository origin;
158             private final Path path;
159             private final String message;
160             private final boolean valid;
161             private final List<String> entries;
162 
163             private Impl(RemoteRepository origin, Path path, String message, boolean valid, List<String> entries) {
164                 this.origin = requireNonNull(origin);
165                 this.path = requireNonNull(path);
166                 this.message = message;
167                 this.valid = valid;
168                 this.entries = entries;
169             }
170 
171             @Override
172             public RemoteRepository origin() {
173                 return origin;
174             }
175 
176             @Override
177             public Path path() {
178                 return path;
179             }
180 
181             @Override
182             public String message() {
183                 return message;
184             }
185 
186             @Override
187             public boolean valid() {
188                 return valid;
189             }
190 
191             @Override
192             public List<String> entries() {
193                 return entries;
194             }
195         }
196     }
197 }