<commit_before>/*
 * Copyright 2000-2013 JetBrains s.r.o.
 *
 * 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 org.jetbrains.idea.svn.commandLine;

import com.intellij.execution.process.ProcessOutputTypes;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.LineHandlerHelper;
import com.intellij.openapi.vcs.LineProcessEventListener;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.util.EventDispatcher;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.svn.AuthenticationCallback;
import org.jetbrains.idea.svn.Util;
import org.jetbrains.idea.svn.config.SvnBindException;

import java.io.File;
import java.util.Iterator;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Created with IntelliJ IDEA.
 * User: Irina.Chernushina
 * Date: 1/25/12
 * Time: 4:05 PM
 *
 * honestly stolen from GitLineHandler
 */
public class SvnLineCommand extends SvnCommand {
  public static final String AUTHENTICATION_REALM = "Authentication realm:";
  public static final String CERTIFICATE_ERROR = "Error validating server certificate for";
  public static final String PASSPHRASE_FOR = "Passphrase for";
  public static final String CLIENT_CERTIFICATE_FILENAME = "Client certificate filename:";
  /**
   * the partial line from stdout stream
   */
  private final StringBuilder myStdoutLine = new StringBuilder();
  /**
   * the partial line from stderr stream
   */
  private final StringBuilder myStderrLine = new StringBuilder();
  private final EventDispatcher<LineProcessEventListener> myLineListeners;
  private final AtomicReference<Integer> myExitCode;
  private final StringBuffer myErr;

  public SvnLineCommand(File workingDirectory, @NotNull SvnCommandName commandName, @NotNull @NonNls String exePath) {
    this(workingDirectory, commandName, exePath, null);
  }

  public SvnLineCommand(File workingDirectory, @NotNull SvnCommandName commandName, @NotNull @NonNls String exePath, File configDir) {
    super(workingDirectory, commandName, exePath, configDir);
    myLineListeners = EventDispatcher.create(LineProcessEventListener.class);
    myExitCode = new AtomicReference<Integer>();
    myErr = new StringBuffer();
  }

  @Override
  protected void processTerminated(int exitCode) {
    // force newline
    if (myStdoutLine.length() != 0) {
      onTextAvailable("\n\r", ProcessOutputTypes.STDOUT);
    }
    else if (myStderrLine.length() != 0) {
      onTextAvailable("\n\r", ProcessOutputTypes.STDERR);
    }
  }

  public static void runAndWaitProcessErrorsIntoExceptions(final String exePath, final File firstFile, SvnCommandName commandName,
                                                           final LineCommandListener listener, @Nullable AuthenticationCallback authenticationCallback, final String... parameters) throws SvnBindException {
    File base = firstFile.isDirectory() ? firstFile : firstFile.getParentFile();
    base = Util.correctUpToExistingParent(base);

    listener.baseDirectory(base);

    File configDir = null;
    while (true) {
      final SvnLineCommand command = runCommand(exePath, commandName, listener, base, configDir, parameters);
      if (command.myErr.length() > 0) {
        final String errText = command.myErr.toString().trim();
        if (authenticationCallback != null) {
          final AuthCallbackCase callback = createCallback(errText, authenticationCallback, base);
          if (callback != null) {
            cleanup(exePath, commandName, base);
            if (callback.getCredentials(errText)) {
              if (authenticationCallback.getSpecialConfigDir() != null) {
                configDir = authenticationCallback.getSpecialConfigDir();
              }
              continue;
            }
          }
        }
        throw new SvnBindException(errText);
      }
      final Integer exitCode = command.myExitCode.get();
      if (exitCode != 0) {
        throw new SvnBindException("Svn process exited with error code: " + exitCode);
      }
      return;
    }
    //ok
  }

  private static AuthCallbackCase createCallback(final String errText, final AuthenticationCallback callback, final File base) {
    if (errText.startsWith(CERTIFICATE_ERROR)) {
      return new CertificateCallbackCase(callback, base);
    }
    if (errText.startsWith(AUTHENTICATION_REALM)) {
      return new CredentialsCallback(callback, base);
    }
    if (errText.startsWith(PASSPHRASE_FOR)) {
      return new PassphraseCallback(callback, base);
    }
    return null;
  }

  private static class CredentialsCallback extends AuthCallbackCase {
    protected CredentialsCallback(AuthenticationCallback callback, File base) {
      super(callback, base);
    }

    @Override
    boolean getCredentials(String errText) throws SvnBindException {
      final String realm = cutFirstLine(errText).substring(AUTHENTICATION_REALM.length()).trim();
      final boolean isPassword = StringUtil.containsIgnoreCase(errText, "password");
      if (myTried) {
        myAuthenticationCallback.clearPassiveCredentials(realm, myBase, isPassword);
      }
      myTried = true;
      if (myAuthenticationCallback.authenticateFor(realm, myBase, myAuthenticationCallback.getSpecialConfigDir() != null, isPassword)) {
        return true;
      }
      throw new SvnBindException("Authentication canceled for realm: " + realm);
    }
  }

  private static String cutFirstLine(final String text) {
    final int idx = text.indexOf('\n');
    if (idx == -1) return text;
    return text.substring(0, idx);
  }

  private static class CertificateCallbackCase extends AuthCallbackCase {
    private CertificateCallbackCase(AuthenticationCallback callback, File base) {
      super(callback, base);
    }

    @Override
    public boolean getCredentials(final String errText) throws SvnBindException {
      String realm = cutFirstLine(errText).substring(CERTIFICATE_ERROR.length());
      final int idx1 = realm.indexOf('\'');
      if (idx1 == -1) {
        throw new SvnBindException("Can not detect authentication realm name: " + errText);
      }
      final int idx2 = realm.indexOf('\'', idx1 + 1);
      if (idx2== -1) {
        throw new SvnBindException("Can not detect authentication realm name: " + errText);
      }
      realm = realm.substring(idx1 + 1, idx2);
      if (! myTried && myAuthenticationCallback.acceptSSLServerCertificate(myBase, realm)) {
        myTried = true;
        return true;
      }
      throw new SvnBindException("Server SSL certificate rejected");
    }
  }

  private static abstract class AuthCallbackCase {
    protected boolean myTried = false;
    protected final AuthenticationCallback myAuthenticationCallback;
    protected final File myBase;

    protected AuthCallbackCase(AuthenticationCallback callback, final File base) {
      myAuthenticationCallback = callback;
      myBase = base;
    }

    abstract boolean getCredentials(final String errText) throws SvnBindException;
  }

  private static void cleanup(String exePath, SvnCommandName commandName, File base) throws SvnBindException {
    File wcRoot = Util.getWcRoot(base);
    if (wcRoot == null) throw new SvnBindException("Can not find working copy root for: " + base.getPath());

    //cleanup -> check command type
    if (commandName.isWriteable()) {
      final SvnSimpleCommand command = new SvnSimpleCommand(wcRoot, SvnCommandName.cleanup, exePath);
      try {
        command.run();
      }
      catch (VcsException e) {
        throw new SvnBindException(e);
      }
    }
  }

  /*svn: E170001: Commit failed (details follow):
  svn: E170001: Unable to connect to a repository at URL 'htt../svn/secondRepo/local2/trunk/mod2/src/com/test/gggGA'
  svn: E170001: OPTIONS of 'htt.../svn/secondRepo/local2/trunk/mod2/src/com/test/gggGA': authorization failed: Could not authenticate to server: rejected Basic challenge (ht)*/
  private final static String ourAuthFailed = "authorization failed";
  private final static String ourAuthFailed2 = "Could not authenticate to server";

  private static boolean isAuthenticationFailed(String s) {
    return s.trim().startsWith(AUTHENTICATION_REALM);
    //return s.contains(ourAuthFailed) && s.contains(ourAuthFailed2);
  }

  private static SvnLineCommand runCommand(String exePath,
                                           SvnCommandName commandName,
                                           final LineCommandListener listener,
                                           File base, File configDir,
                                           String... parameters) throws SvnBindException {
    final SvnLineCommand command = new SvnLineCommand(base, commandName, exePath, configDir) {
      @Override
      protected void onTextAvailable(String text, Key outputType) {
        int myErrCnt = 0;

        // we won't stop right now if got "authentication realm" -> since we want to get "password" string (that would mean password is expected
        // or certificate maybe is expected
        // but for certificate passphrase we get just "Passphrase for ..." - one line without line feed

        // for client certificate (when no path in servers file) we get
        // Authentication realm: <text>
        // Client certificate filename:
        if (ProcessOutputTypes.STDERR.equals(outputType)) {
          if (text.trim().startsWith(PASSPHRASE_FOR) || myErrCnt >= 2) {
            destroyProcess();
          }
        }
        super.onTextAvailable(text, outputType);
      }
    };

    //command.addParameters("--non-interactive");
    command.addParameters(parameters);
    final AtomicReference<Throwable> exceptionRef = new AtomicReference<Throwable>();
    // several threads
    command.addListener(new LineProcessEventListener() {
      @Override
      public void onLineAvailable(String line, Key outputType) {<commit_msg>we can sometimes get "Authentication realm .." and "Passphrase for..." lines in reverse order on Unix (!) so only check for reply that have 1 line (certificate passphrase), or wait for at least 2 lines of err text<commit_after>/*
 * Copyright 2000-2013 JetBrains s.r.o.
 *
 * 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 org.jetbrains.idea.svn.commandLine;

import com.intellij.execution.process.ProcessOutputTypes;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.LineHandlerHelper;
import com.intellij.openapi.vcs.LineProcessEventListener;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.util.EventDispatcher;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.svn.AuthenticationCallback;
import org.jetbrains.idea.svn.Util;
import org.jetbrains.idea.svn.config.SvnBindException;

import java.io.File;
import java.util.Iterator;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Created with IntelliJ IDEA.
 * User: Irina.Chernushina
 * Date: 1/25/12
 * Time: 4:05 PM
 *
 * honestly stolen from GitLineHandler
 */
public class SvnLineCommand extends SvnCommand {
  public static final String AUTHENTICATION_REALM = "Authentication realm:";
  public static final String CERTIFICATE_ERROR = "Error validating server certificate for";
  public static final String PASSPHRASE_FOR = "Passphrase for";
  public static final String CLIENT_CERTIFICATE_FILENAME = "Client certificate filename:";
  /**
   * the partial line from stdout stream
   */
  private final StringBuilder myStdoutLine = new StringBuilder();
  /**
   * the partial line from stderr stream
   */
  private final StringBuilder myStderrLine = new StringBuilder();
  private final EventDispatcher<LineProcessEventListener> myLineListeners;
  private final AtomicReference<Integer> myExitCode;
  private final StringBuffer myErr;

  public SvnLineCommand(File workingDirectory, @NotNull SvnCommandName commandName, @NotNull @NonNls String exePath) {
    this(workingDirectory, commandName, exePath, null);
  }

  public SvnLineCommand(File workingDirectory, @NotNull SvnCommandName commandName, @NotNull @NonNls String exePath, File configDir) {
    super(workingDirectory, commandName, exePath, configDir);
    myLineListeners = EventDispatcher.create(LineProcessEventListener.class);
    myExitCode = new AtomicReference<Integer>();
    myErr = new StringBuffer();
  }

  @Override
  protected void processTerminated(int exitCode) {
    // force newline
    if (myStdoutLine.length() != 0) {
      onTextAvailable("\n\r", ProcessOutputTypes.STDOUT);
    }
    else if (myStderrLine.length() != 0) {
      onTextAvailable("\n\r", ProcessOutputTypes.STDERR);
    }
  }

  public static void runAndWaitProcessErrorsIntoExceptions(final String exePath, final File firstFile, SvnCommandName commandName,
                                                           final LineCommandListener listener, @Nullable AuthenticationCallback authenticationCallback, final String... parameters) throws SvnBindException {
    File base = firstFile.isDirectory() ? firstFile : firstFile.getParentFile();
    base = Util.correctUpToExistingParent(base);

    listener.baseDirectory(base);

    File configDir = null;
    while (true) {
      final SvnLineCommand command = runCommand(exePath, commandName, listener, base, configDir, parameters);
      if (command.myErr.length() > 0) {
        final String errText = command.myErr.toString().trim();
        if (authenticationCallback != null) {
          final AuthCallbackCase callback = createCallback(errText, authenticationCallback, base);
          if (callback != null) {
            cleanup(exePath, commandName, base);
            if (callback.getCredentials(errText)) {
              if (authenticationCallback.getSpecialConfigDir() != null) {
                configDir = authenticationCallback.getSpecialConfigDir();
              }
              continue;
            }
          }
        }
        throw new SvnBindException(errText);
      }
      final Integer exitCode = command.myExitCode.get();
      if (exitCode != 0) {
        throw new SvnBindException("Svn process exited with error code: " + exitCode);
      }
      return;
    }
    //ok
  }

  private static AuthCallbackCase createCallback(final String errText, final AuthenticationCallback callback, final File base) {
    if (errText.startsWith(CERTIFICATE_ERROR)) {
      return new CertificateCallbackCase(callback, base);
    }
    if (errText.startsWith(AUTHENTICATION_REALM)) {
      return new CredentialsCallback(callback, base);
    }
    if (errText.startsWith(PASSPHRASE_FOR)) {
      return new PassphraseCallback(callback, base);
    }
    return null;
  }

  private static class CredentialsCallback extends AuthCallbackCase {
    protected CredentialsCallback(AuthenticationCallback callback, File base) {
      super(callback, base);
    }

    @Override
    boolean getCredentials(String errText) throws SvnBindException {
      final String realm = cutFirstLine(errText).substring(AUTHENTICATION_REALM.length()).trim();
      final boolean isPassword = StringUtil.containsIgnoreCase(errText, "password");
      if (myTried) {
        myAuthenticationCallback.clearPassiveCredentials(realm, myBase, isPassword);
      }
      myTried = true;
      if (myAuthenticationCallback.authenticateFor(realm, myBase, myAuthenticationCallback.getSpecialConfigDir() != null, isPassword)) {
        return true;
      }
      throw new SvnBindException("Authentication canceled for realm: " + realm);
    }
  }

  private static String cutFirstLine(final String text) {
    final int idx = text.indexOf('\n');
    if (idx == -1) return text;
    return text.substring(0, idx);
  }

  private static class CertificateCallbackCase extends AuthCallbackCase {
    private CertificateCallbackCase(AuthenticationCallback callback, File base) {
      super(callback, base);
    }

    @Override
    public boolean getCredentials(final String errText) throws SvnBindException {
      String realm = cutFirstLine(errText).substring(CERTIFICATE_ERROR.length());
      final int idx1 = realm.indexOf('\'');
      if (idx1 == -1) {
        throw new SvnBindException("Can not detect authentication realm name: " + errText);
      }
      final int idx2 = realm.indexOf('\'', idx1 + 1);
      if (idx2== -1) {
        throw new SvnBindException("Can not detect authentication realm name: " + errText);
      }
      realm = realm.substring(idx1 + 1, idx2);
      if (! myTried && myAuthenticationCallback.acceptSSLServerCertificate(myBase, realm)) {
        myTried = true;
        return true;
      }
      throw new SvnBindException("Server SSL certificate rejected");
    }
  }

  private static abstract class AuthCallbackCase {
    protected boolean myTried = false;
    protected final AuthenticationCallback myAuthenticationCallback;
    protected final File myBase;

    protected AuthCallbackCase(AuthenticationCallback callback, final File base) {
      myAuthenticationCallback = callback;
      myBase = base;
    }

    abstract boolean getCredentials(final String errText) throws SvnBindException;
  }

  private static void cleanup(String exePath, SvnCommandName commandName, File base) throws SvnBindException {
    File wcRoot = Util.getWcRoot(base);
    if (wcRoot == null) throw new SvnBindException("Can not find working copy root for: " + base.getPath());

    //cleanup -> check command type
    if (commandName.isWriteable()) {
      final SvnSimpleCommand command = new SvnSimpleCommand(wcRoot, SvnCommandName.cleanup, exePath);
      try {
        command.run();
      }
      catch (VcsException e) {
        throw new SvnBindException(e);
      }
    }
  }

  /*svn: E170001: Commit failed (details follow):
  svn: E170001: Unable to connect to a repository at URL 'htt../svn/secondRepo/local2/trunk/mod2/src/com/test/gggGA'
  svn: E170001: OPTIONS of 'htt.../svn/secondRepo/local2/trunk/mod2/src/com/test/gggGA': authorization failed: Could not authenticate to server: rejected Basic challenge (ht)*/
  private final static String ourAuthFailed = "authorization failed";
  private final static String ourAuthFailed2 = "Could not authenticate to server";

  private static boolean isAuthenticationFailed(String s) {
    return s.trim().startsWith(AUTHENTICATION_REALM);
    //return s.contains(ourAuthFailed) && s.contains(ourAuthFailed2);
  }

  private static SvnLineCommand runCommand(String exePath,
                                           SvnCommandName commandName,
                                           final LineCommandListener listener,
                                           File base, File configDir,
                                           String... parameters) throws SvnBindException {
    final SvnLineCommand command = new SvnLineCommand(base, commandName, exePath, configDir) {
      int myErrCnt = 0;

      @Override
      protected void onTextAvailable(String text, Key outputType) {

        // we won't stop right now if got "authentication realm" -> since we want to get "password" string (that would mean password is expected
        // or certificate maybe is expected
        // but for certificate passphrase we get just "Passphrase for ..." - one line without line feed

        // for client certificate (when no path in servers file) we get
        // Authentication realm: <text>
        // Client certificate filename:
        if (ProcessOutputTypes.STDERR.equals(outputType)) {
          ++ myErrCnt;
          if (text.trim().startsWith(PASSPHRASE_FOR) || myErrCnt >= 2) {
            destroyProcess();
          }
        }
        super.onTextAvailable(text, outputType);
      }
    };

    //command.addParameters("--non-interactive");
    command.addParameters(parameters);
    final AtomicReference<Throwable> exceptionRef = new AtomicReference<Throwable>();
    // several threads
    command.addListener(new LineProcessEventListener() {
      @Override
      public void onLineAvailable(String line, Key outputType) {
{'fraction_non_alphanumeric': 0.08841234010534237, 'fraction_numerical': 0.006019563581640331, 'mean_word_length': 3.3504500818330607, 'pattern_counts': {'":': 0, '<': 15, '<?xml version=': 0, '>': 23, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 20, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2773598', 'n_tokens_mistral': 5782, 'n_tokens_neox': 5601, 'n_words': 1630}