/*
 * Copyright (C) 2008-2010, Google Inc.
 * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
 * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Distribution License v. 1.0 which is available at
 * https://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

package org.eclipse.jgit.transport;

import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_AGENT;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

import org.eclipse.jgit.errors.InvalidObjectIdException;
import org.eclipse.jgit.errors.NoRemoteRepositoryException;
import org.eclipse.jgit.errors.PackProtocolException;
import org.eclipse.jgit.errors.RemoteRepositoryException;
import org.eclipse.jgit.errors.TransportException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectIdRef;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.SymbolicRef;
import org.eclipse.jgit.util.io.InterruptTimer;
import org.eclipse.jgit.util.io.TimeoutInputStream;
import org.eclipse.jgit.util.io.TimeoutOutputStream;

Base helper class for pack-based operations implementations. Provides partial implementation of pack-protocol - refs advertising and capabilities support, and some other helper methods.
See Also:
/** * Base helper class for pack-based operations implementations. Provides partial * implementation of pack-protocol - refs advertising and capabilities support, * and some other helper methods. * * @see BasePackFetchConnection * @see BasePackPushConnection */
abstract class BasePackConnection extends BaseConnection { protected static final String CAPABILITY_SYMREF_PREFIX = "symref="; //$NON-NLS-1$
The repository this transport fetches into, or pushes out of.
/** The repository this transport fetches into, or pushes out of. */
protected final Repository local;
Remote repository location.
/** Remote repository location. */
protected final URIish uri;
A transport connected to uri.
/** A transport connected to {@link #uri}. */
protected final Transport transport;
Low-level input stream, if a timeout was configured.
/** Low-level input stream, if a timeout was configured. */
protected TimeoutInputStream timeoutIn;
Low-level output stream, if a timeout was configured.
/** Low-level output stream, if a timeout was configured. */
protected TimeoutOutputStream timeoutOut;
Timer to manage timeoutIn and timeoutOut.
/** Timer to manage {@link #timeoutIn} and {@link #timeoutOut}. */
private InterruptTimer myTimer;
Input stream reading from the remote.
/** Input stream reading from the remote. */
protected InputStream in;
Output stream sending to the remote.
/** Output stream sending to the remote. */
protected OutputStream out;
Packet line decoder around in.
/** Packet line decoder around {@link #in}. */
protected PacketLineIn pckIn;
Packet line encoder around out.
/** Packet line encoder around {@link #out}. */
protected PacketLineOut pckOut;
Send PacketLineOut.end() before closing out?
/** Send {@link PacketLineOut#end()} before closing {@link #out}? */
protected boolean outNeedsEnd;
True if this is a stateless RPC connection.
/** True if this is a stateless RPC connection. */
protected boolean statelessRPC;
Capability tokens advertised by the remote side.
/** Capability tokens advertised by the remote side. */
private final Set<String> remoteCapablities = new HashSet<>();
Extra objects the remote has, but which aren't offered as refs.
/** Extra objects the remote has, but which aren't offered as refs. */
protected final Set<ObjectId> additionalHaves = new HashSet<>(); BasePackConnection(PackTransport packTransport) { transport = (Transport) packTransport; local = transport.local; uri = transport.uri; }
Configure this connection with the directional pipes.
Params:
  • myIn – input stream to receive data from the peer. Caller must ensure the input is buffered, otherwise read performance may suffer.
  • myOut – output stream to transmit data to the peer. Caller must ensure the output is buffered, otherwise write performance may suffer.
/** * Configure this connection with the directional pipes. * * @param myIn * input stream to receive data from the peer. Caller must ensure * the input is buffered, otherwise read performance may suffer. * @param myOut * output stream to transmit data to the peer. Caller must ensure * the output is buffered, otherwise write performance may * suffer. */
protected final void init(InputStream myIn, OutputStream myOut) { final int timeout = transport.getTimeout(); if (timeout > 0) { final Thread caller = Thread.currentThread(); if (myTimer == null) { myTimer = new InterruptTimer(caller.getName() + "-Timer"); //$NON-NLS-1$ } timeoutIn = new TimeoutInputStream(myIn, myTimer); timeoutOut = new TimeoutOutputStream(myOut, myTimer); timeoutIn.setTimeout(timeout * 1000); timeoutOut.setTimeout(timeout * 1000); myIn = timeoutIn; myOut = timeoutOut; } in = myIn; out = myOut; pckIn = new PacketLineIn(in); pckOut = new PacketLineOut(out); outNeedsEnd = true; }
Reads the advertised references through the initialized stream.

Subclass implementations may call this method only after setting up the input and output streams with init(InputStream, OutputStream).

If any errors occur, this connection is automatically closed by invoking close() and the exception is wrapped (if necessary) and thrown as a TransportException.

Throws:
/** * Reads the advertised references through the initialized stream. * <p> * Subclass implementations may call this method only after setting up the * input and output streams with {@link #init(InputStream, OutputStream)}. * <p> * If any errors occur, this connection is automatically closed by invoking * {@link #close()} and the exception is wrapped (if necessary) and thrown * as a {@link org.eclipse.jgit.errors.TransportException}. * * @throws org.eclipse.jgit.errors.TransportException * the reference list could not be scanned. */
protected void readAdvertisedRefs() throws TransportException { try { readAdvertisedRefsImpl(); } catch (TransportException err) { close(); throw err; } catch (IOException | RuntimeException err) { close(); throw new TransportException(err.getMessage(), err); } } private void readAdvertisedRefsImpl() throws IOException { final LinkedHashMap<String, Ref> avail = new LinkedHashMap<>(); for (;;) { String line; try { line = pckIn.readString(); } catch (EOFException eof) { if (avail.isEmpty()) throw noRepository(); throw eof; } if (PacketLineIn.isEnd(line)) break; if (line.startsWith("ERR ")) { //$NON-NLS-1$ // This is a customized remote service error. // Users should be informed about it. throw new RemoteRepositoryException(uri, line.substring(4)); } if (avail.isEmpty()) { final int nul = line.indexOf('\0'); if (nul >= 0) { // The first line (if any) may contain "hidden" // capability values after a NUL byte. remoteCapablities.addAll( Arrays.asList(line.substring(nul + 1).split(" "))); //$NON-NLS-1$ line = line.substring(0, nul); } } // Expecting to get a line in the form "sha1 refname" if (line.length() < 41 || line.charAt(40) != ' ') { throw invalidRefAdvertisementLine(line); } String name = line.substring(41, line.length()); if (avail.isEmpty() && name.equals("capabilities^{}")) { //$NON-NLS-1$ // special line from git-receive-pack to show // capabilities when there are no refs to advertise continue; } final ObjectId id; try { id = ObjectId.fromString(line.substring(0, 40)); } catch (InvalidObjectIdException e) { PackProtocolException ppe = invalidRefAdvertisementLine(line); ppe.initCause(e); throw ppe; } if (name.equals(".have")) { //$NON-NLS-1$ additionalHaves.add(id); } else if (name.endsWith("^{}")) { //$NON-NLS-1$ name = name.substring(0, name.length() - 3); final Ref prior = avail.get(name); if (prior == null) throw new PackProtocolException(uri, MessageFormat.format( JGitText.get().advertisementCameBefore, name, name)); if (prior.getPeeledObjectId() != null) throw duplicateAdvertisement(name + "^{}"); //$NON-NLS-1$ avail.put(name, new ObjectIdRef.PeeledTag( Ref.Storage.NETWORK, name, prior.getObjectId(), id)); } else { final Ref prior = avail.put(name, new ObjectIdRef.PeeledNonTag( Ref.Storage.NETWORK, name, id)); if (prior != null) throw duplicateAdvertisement(name); } } updateWithSymRefs(avail, extractSymRefsFromCapabilities(remoteCapablities)); available(avail); }
Finds values in the given capabilities of the form:
symref=source:target
And returns a Map of source->target entries.
Params:
  • capabilities – the capabilities lines
Throws:
Returns:a Map of the symref entries from capabilities
/** * Finds values in the given capabilities of the form: * * <pre> * symref=<em>source</em>:<em>target</em> * </pre> * * And returns a Map of source->target entries. * * @param capabilities * the capabilities lines * @return a Map of the symref entries from capabilities * @throws NullPointerException * if capabilities, or any entry in it, is null */
static Map<String, String> extractSymRefsFromCapabilities(Collection<String> capabilities) { final Map<String, String> symRefs = new LinkedHashMap<>(); for (String option : capabilities) { if (option.startsWith(CAPABILITY_SYMREF_PREFIX)) { String[] symRef = option .substring(CAPABILITY_SYMREF_PREFIX.length()) .split(":", 2); //$NON-NLS-1$ if (symRef.length == 2) { symRefs.put(symRef[0], symRef[1]); } } } return symRefs; }
Updates the given refMap with SymbolicRefs defined by the given symRefs.

For each entry, symRef, in symRefs, whose value is a key in refMap, adds a new entry to refMap with that same key and value of a new SymbolicRef with source=symRef.key and target=refMap.get(symRef.value), then removes that entry from symRefs.

If refMap already contains an entry for symRef.key, it is replaced.

For example, given:

refMap.put("refs/heads/main", ref);
symRefs.put("HEAD", "refs/heads/main");
then:
updateWithSymRefs(refMap, symRefs);
has the effect of:
refMap.put("HEAD",
		new SymbolicRef("HEAD", refMap.get(symRefs.remove("HEAD"))))

Any entry in symRefs whose value is not a key in refMap is ignored. Any circular symRefs are ignored.

Upon completion, symRefs will contain only any unresolvable entries.

Params:
  • refMap – a non-null, modifiable, Map to update, and the provider of symref targets.
  • symRefs – a non-null, modifiable, Map of symrefs.
Throws:
/** * Updates the given refMap with {@link SymbolicRef}s defined by the given * symRefs. * <p> * For each entry, symRef, in symRefs, whose value is a key in refMap, adds * a new entry to refMap with that same key and value of a new * {@link SymbolicRef} with source=symRef.key and * target=refMap.get(symRef.value), then removes that entry from symRefs. * <p> * If refMap already contains an entry for symRef.key, it is replaced. * </p> * </p> * <p> * For example, given: * </p> * * <pre> * refMap.put("refs/heads/main", ref); * symRefs.put("HEAD", "refs/heads/main"); * </pre> * * then: * * <pre> * updateWithSymRefs(refMap, symRefs); * </pre> * * has the <em>effect</em> of: * * <pre> * refMap.put("HEAD", * new SymbolicRef("HEAD", refMap.get(symRefs.remove("HEAD")))) * </pre> * <p> * Any entry in symRefs whose value is not a key in refMap is ignored. Any * circular symRefs are ignored. * </p> * <p> * Upon completion, symRefs will contain only any unresolvable entries. * </p> * * @param refMap * a non-null, modifiable, Map to update, and the provider of * symref targets. * @param symRefs * a non-null, modifiable, Map of symrefs. * @throws NullPointerException * if refMap or symRefs is null */
static void updateWithSymRefs(Map<String, Ref> refMap, Map<String, String> symRefs) { boolean haveNewRefMapEntries = !refMap.isEmpty(); while (!symRefs.isEmpty() && haveNewRefMapEntries) { haveNewRefMapEntries = false; final Iterator<Map.Entry<String, String>> iterator = symRefs.entrySet().iterator(); while (iterator.hasNext()) { final Map.Entry<String, String> symRef = iterator.next(); if (!symRefs.containsKey(symRef.getValue())) { // defer forward reference final Ref r = refMap.get(symRef.getValue()); if (r != null) { refMap.put(symRef.getKey(), new SymbolicRef(symRef.getKey(), r)); haveNewRefMapEntries = true; iterator.remove(); } } } } }
Create an exception to indicate problems finding a remote repository. The caller is expected to throw the returned exception. Subclasses may override this method to provide better diagnostics.
Returns:a TransportException saying a repository cannot be found and possibly why.
/** * Create an exception to indicate problems finding a remote repository. The * caller is expected to throw the returned exception. * * Subclasses may override this method to provide better diagnostics. * * @return a TransportException saying a repository cannot be found and * possibly why. */
protected TransportException noRepository() { return new NoRemoteRepositoryException(uri, JGitText.get().notFound); }
Whether this option is supported
Params:
  • option – option string
Returns:whether this option is supported
/** * Whether this option is supported * * @param option * option string * @return whether this option is supported */
protected boolean isCapableOf(String option) { return remoteCapablities.contains(option); }
Request capability
Params:
  • b – buffer
  • option – option we want
Returns:true if the requested option is supported
/** * Request capability * * @param b * buffer * @param option * option we want * @return {@code true} if the requested option is supported */
protected boolean wantCapability(StringBuilder b, String option) { if (!isCapableOf(option)) return false; b.append(' '); b.append(option); return true; }
Add user agent capability
Params:
/** * Add user agent capability * * @param b * a {@link java.lang.StringBuilder} object. */
protected void addUserAgentCapability(StringBuilder b) { String a = UserAgent.get(); if (a != null && UserAgent.hasAgent(remoteCapablities)) { b.append(' ').append(OPTION_AGENT).append('=').append(a); } }
{@inheritDoc}
/** {@inheritDoc} */
@Override public String getPeerUserAgent() { return UserAgent.getAgent(remoteCapablities, super.getPeerUserAgent()); } private PackProtocolException duplicateAdvertisement(String name) { return new PackProtocolException(uri, MessageFormat.format(JGitText.get().duplicateAdvertisementsOf, name)); } private PackProtocolException invalidRefAdvertisementLine(String line) { return new PackProtocolException(uri, MessageFormat.format(JGitText.get().invalidRefAdvertisementLine, line)); }
{@inheritDoc}
/** {@inheritDoc} */
@Override public void close() { if (out != null) { try { if (outNeedsEnd) { outNeedsEnd = false; pckOut.end(); } out.close(); } catch (IOException err) { // Ignore any close errors. } finally { out = null; pckOut = null; } } if (in != null) { try { in.close(); } catch (IOException err) { // Ignore any close errors. } finally { in = null; pckIn = null; } } if (myTimer != null) { try { myTimer.terminate(); } finally { myTimer = null; timeoutIn = null; timeoutOut = null; } } }
Tell the peer we are disconnecting, if it cares to know.
/** * Tell the peer we are disconnecting, if it cares to know. */
protected void endOut() { if (outNeedsEnd && out != null) { try { outNeedsEnd = false; pckOut.end(); } catch (IOException e) { try { out.close(); } catch (IOException err) { // Ignore any close errors. } finally { out = null; pckOut = null; } } } } }