blob: 8f6de4c95dc07d54c00f84ff12eea911f99d089f [file] [log] [blame]
// Copyright 2020 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.lib.authandtls;
import static com.google.common.base.Predicates.not;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.auto.value.AutoValue;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.authandtls.Netrc.Credential;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
/**
* A parser used to parse .netrc content.
*
* @see <a href="https://man.cx/netrc(4)">netrc − file for ftp remote login data</a>
* @see <a
* href="https://github.com/bazelbuild/bazel/blob/master/tools/build_defs/repo/utils.bzl#L203-L204">Starlark
* netrc parser</a>
*/
public class NetrcParser {
private static final String MACHINE = "machine";
private static final String MACDEF = "macdef";
private static final String DEFAULT = "default";
private static final String LOGIN = "login";
private static final String PASSWORD = "password";
private static final String ACCOUNT = "account";
interface Token {}
@AutoValue
abstract static class ItemToken implements Token {
public static ItemToken create(String item) {
return new AutoValue_NetrcParser_ItemToken(item);
}
abstract String item();
}
@AutoValue
abstract static class NewlineToken implements Token {
public static NewlineToken create() {
return new AutoValue_NetrcParser_NewlineToken();
}
}
@AutoValue
abstract static class CommentToken implements Token {
public static CommentToken create() {
return new AutoValue_NetrcParser_CommentToken();
}
}
private static class TokenStream implements Closeable {
private final BufferedReader bufferedReader;
private final Queue<Token> tokens = new ArrayDeque<>();
TokenStream(InputStream inputStream) throws IOException {
bufferedReader = new BufferedReader(new InputStreamReader(inputStream, UTF_8));
processLine();
}
@Override
public void close() throws IOException {
bufferedReader.close();
}
private void processLine() throws IOException {
String line = bufferedReader.readLine();
if (line == null) {
return;
}
// Comments start with #
if (line.startsWith("#")) {
tokens.add(CommentToken.create());
} else {
Arrays.stream(line.split("\\s+"))
.filter(not(Strings::isNullOrEmpty))
.map(ItemToken::create)
.forEach(tokens::add);
}
tokens.add(NewlineToken.create());
}
public boolean hasNext() {
return !tokens.isEmpty();
}
public Token next() throws IOException {
Token token = tokens.poll();
if (tokens.isEmpty()) {
processLine();
}
return token;
}
public Token peek() {
return tokens.peek();
}
}
private NetrcParser() {}
public static Netrc parseAndClose(InputStream inputStream) throws IOException {
try (TokenStream tokenStream = new TokenStream(inputStream)) {
return parse(tokenStream);
}
}
private static Netrc parse(TokenStream tokenStream) throws IOException {
Credential defaultCredential = null;
Map<String, Credential> credentialMap = new HashMap<>();
boolean done = false;
while (!done && tokenStream.hasNext()) {
Token token = tokenStream.next();
if (token instanceof ItemToken itemToken) {
String item = itemToken.item();
switch (item) {
case MACHINE -> {
String machine = nextItem(tokenStream);
Credential credential = parseCredentialForMachine(tokenStream, machine);
credentialMap.put(machine, credential);
}
case MACDEF -> skipMacdef(tokenStream);
case DEFAULT -> {
defaultCredential = parseCredentialForMachine(tokenStream, DEFAULT);
// There can be only one default token, and it must be after all machine tokens.
done = true;
}
default ->
throw new IOException(
String.format(
"Unexpected token: %s (expecting %s, %s or %s)",
item, MACHINE, MACDEF, DEFAULT));
}
}
}
return Netrc.create(defaultCredential, ImmutableMap.copyOf(credentialMap));
}
private static String nextItem(TokenStream tokenStream) throws IOException {
while (tokenStream.hasNext()) {
Token token = tokenStream.next();
if (token instanceof ItemToken itemToken) {
return itemToken.item();
}
}
throw new IOException("Unexpected EOF");
}
/** Parse credentials for a given machine from token stream. */
private static Credential parseCredentialForMachine(TokenStream tokenStream, String machine)
throws IOException {
Credential.Builder builder = Credential.builder(machine);
boolean done = false;
while (!done && tokenStream.hasNext()) {
// Peek rather than taking next token since we probably won't process it
Token token = tokenStream.peek();
if (token instanceof ItemToken itemToken) {
String item = itemToken.item();
switch (item) {
case LOGIN -> {
tokenStream.next();
builder.setLogin(nextItem(tokenStream));
}
case PASSWORD -> {
tokenStream.next();
builder.setPassword(nextItem(tokenStream));
}
case ACCOUNT -> {
tokenStream.next();
builder.setAccount(nextItem(tokenStream));
}
case MACHINE, MACDEF, DEFAULT -> done = true;
default ->
throw new IOException(
String.format(
"Unexpected item: %s (expecting %s, %s, %s, %s, %s or %s)",
item, LOGIN, PASSWORD, ACCOUNT, MACHINE, MACDEF, DEFAULT));
}
} else {
tokenStream.next();
}
}
return builder.build();
}
/** Skip macdef section since we don't need that data currently. */
private static void skipMacdef(TokenStream tokenStream) throws IOException {
int numNewlines = 0;
while (tokenStream.hasNext()) {
Token token = tokenStream.next();
if (token instanceof NewlineToken) {
++numNewlines;
} else {
numNewlines = 0;
}
if (numNewlines >= 2) {
break;
}
}
}
}