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}