001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.eclipse.aether.internal.impl.filter.prefixes;
020
021import java.io.BufferedReader;
022import java.io.IOException;
023import java.nio.charset.StandardCharsets;
024import java.nio.file.Files;
025import java.nio.file.NoSuchFileException;
026import java.nio.file.Path;
027import java.util.ArrayList;
028import java.util.Collections;
029import java.util.List;
030
031import org.eclipse.aether.repository.RemoteRepository;
032
033import static java.util.Objects.requireNonNull;
034
035/**
036 * Prefixes source and parser.
037 * <p>
038 * This class is "clean room" reimplementation of
039 * <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>.
040 *
041 * @since 2.0.11
042 */
043public interface PrefixesSource {
044    /**
045     * The origin repository of this source.
046     */
047    RemoteRepository origin();
048
049    /**
050     * The file path (ie local repository or user provided one) this source got entries from.
051     */
052    Path path();
053
054    /**
055     * Message worth logging if {@link #valid()} returns {@code false}.
056     */
057    String message();
058
059    /**
060     * Returns {@code true} if source is valid and contains valid entries.
061     */
062    boolean valid();
063
064    /**
065     * The prefix entries.
066     */
067    List<String> entries();
068
069    /**
070     * Creates {@link PrefixesSource} out of passed in parameters, never returns {@code null}. The returned
071     * source should be checked for {@link #valid()} and use only if it returns {@code true}.
072     * <p>
073     * This method is "forgiving" to all kind of IO problems while reading (file not found, etc.) and will never
074     * throw {@link IOException} as prefix file processing should not interrupt main flow due which prefix file
075     * processing is happening in the first place. Ideally, user is notified at least by logging if any problem happens.
076     */
077    static PrefixesSource of(RemoteRepository origin, Path path) {
078        requireNonNull(origin, "origin is null");
079        requireNonNull(path, "path is null");
080        return new Parser(origin, path).parse();
081    }
082
083    final class Parser {
084        private static final String PREFIX_MAGIC = "## repository-prefixes/2.0";
085        private static final String PREFIX_LEGACY_MAGIC = "# Prefix file generated by Sonatype Nexus";
086        private static final String PREFIX_UNSUPPORTED = "@ unsupported";
087        private static final int MAX_ENTRIES = 100_000;
088        private static final int MAX_LINE_LENGTH = 250;
089
090        private final RemoteRepository origin;
091        private final Path path;
092
093        private Parser(RemoteRepository origin, Path path) {
094            this.origin = origin;
095            this.path = path;
096        }
097
098        private PrefixesSource parse() {
099            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}