PushCertificateIdent.java

  1. /*
  2.  * Copyright (C) 2015, Google Inc. and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */

  10. package org.eclipse.jgit.transport;

  11. import static java.nio.charset.StandardCharsets.UTF_8;
  12. import static org.eclipse.jgit.util.RawParseUtils.lastIndexOfTrim;

  13. import java.text.SimpleDateFormat;
  14. import java.util.Date;
  15. import java.util.Locale;
  16. import java.util.TimeZone;

  17. import org.eclipse.jgit.lib.PersonIdent;
  18. import org.eclipse.jgit.util.MutableInteger;
  19. import org.eclipse.jgit.util.RawParseUtils;

  20. /**
  21.  * Identity in a push certificate.
  22.  * <p>
  23.  * This is similar to a {@link org.eclipse.jgit.lib.PersonIdent} in that it
  24.  * contains a name, timestamp, and timezone offset, but differs in the following
  25.  * ways:
  26.  * <ul>
  27.  * <li>It is always parsed from a UTF-8 string, rather than a raw commit
  28.  * buffer.</li>
  29.  * <li>It is not guaranteed to contain a name and email portion, since any UTF-8
  30.  * string is a valid OpenPGP User ID (RFC4880 5.1.1). The raw User ID is always
  31.  * available as {@link #getUserId()}, but {@link #getEmailAddress()} may return
  32.  * null.</li>
  33.  * <li>The raw text from which the identity was parsed is available with
  34.  * {@link #getRaw()}. This is necessary for losslessly reconstructing the signed
  35.  * push certificate payload.</li>
  36.  * <li>
  37.  * </ul>
  38.  *
  39.  * @since 4.1
  40.  */
  41. public class PushCertificateIdent {
  42.     /**
  43.      * Parse an identity from a string.
  44.      * <p>
  45.      * Spaces are trimmed when parsing the timestamp and timezone offset, with
  46.      * one exception. The timestamp must be preceded by a single space, and the
  47.      * rest of the string prior to that space (including any additional
  48.      * whitespace) is treated as the OpenPGP User ID.
  49.      * <p>
  50.      * If either the timestamp or timezone offsets are missing, mimics
  51.      * {@link RawParseUtils#parsePersonIdent(String)} behavior and sets them
  52.      * both to zero.
  53.      *
  54.      * @param str
  55.      *            string to parse.
  56.      * @return a {@link org.eclipse.jgit.transport.PushCertificateIdent} object.
  57.      */
  58.     public static PushCertificateIdent parse(String str) {
  59.         MutableInteger p = new MutableInteger();
  60.         byte[] raw = str.getBytes(UTF_8);
  61.         int tzBegin = raw.length - 1;
  62.         tzBegin = lastIndexOfTrim(raw, ' ', tzBegin);
  63.         if (tzBegin < 0 || raw[tzBegin] != ' ') {
  64.             return new PushCertificateIdent(str, str, 0, 0);
  65.         }
  66.         int whenBegin = tzBegin++;
  67.         int tz = RawParseUtils.parseTimeZoneOffset(raw, tzBegin, p);
  68.         boolean hasTz = p.value != tzBegin;

  69.         whenBegin = lastIndexOfTrim(raw, ' ', whenBegin);
  70.         if (whenBegin < 0 || raw[whenBegin] != ' ') {
  71.             return new PushCertificateIdent(str, str, 0, 0);
  72.         }
  73.         int idEnd = whenBegin++;
  74.         long when = RawParseUtils.parseLongBase10(raw, whenBegin, p);
  75.         boolean hasWhen = p.value != whenBegin;

  76.         if (hasTz && hasWhen) {
  77.             idEnd = whenBegin - 1;
  78.         } else {
  79.             // If either tz or when are non-numeric, mimic parsePersonIdent behavior and
  80.             // set them both to zero.
  81.             tz = 0;
  82.             when = 0;
  83.             if (hasTz && !hasWhen) {
  84.                 // Only one trailing numeric field; assume User ID ends before this
  85.                 // field, but discard its value.
  86.                 idEnd = tzBegin - 1;
  87.             } else {
  88.                 // No trailing numeric fields; User ID is whole raw value.
  89.                 idEnd = raw.length;
  90.             }
  91.         }
  92.         String id = new String(raw, 0, idEnd, UTF_8);

  93.         return new PushCertificateIdent(str, id, when * 1000L, tz);
  94.     }

  95.     private final String raw;
  96.     private final String userId;
  97.     private final long when;
  98.     private final int tzOffset;

  99.     /**
  100.      * Construct a new identity from an OpenPGP User ID.
  101.      *
  102.      * @param userId
  103.      *            OpenPGP User ID; any UTF-8 string.
  104.      * @param when
  105.      *            local time.
  106.      * @param tzOffset
  107.      *            timezone offset; see {@link #getTimeZoneOffset()}.
  108.      */
  109.     public PushCertificateIdent(String userId, long when, int tzOffset) {
  110.         this.userId = userId;
  111.         this.when = when;
  112.         this.tzOffset = tzOffset;
  113.         StringBuilder sb = new StringBuilder(userId).append(' ').append(when / 1000)
  114.                 .append(' ');
  115.         PersonIdent.appendTimezone(sb, tzOffset);
  116.         raw = sb.toString();
  117.     }

  118.     private PushCertificateIdent(String raw, String userId, long when,
  119.             int tzOffset) {
  120.         this.raw = raw;
  121.         this.userId = userId;
  122.         this.when = when;
  123.         this.tzOffset = tzOffset;
  124.     }

  125.     /**
  126.      * Get the raw string from which this identity was parsed.
  127.      * <p>
  128.      * If the string was constructed manually, a suitable canonical string is
  129.      * returned.
  130.      * <p>
  131.      * For the purposes of bytewise comparisons with other OpenPGP IDs, the string
  132.      * must be encoded as UTF-8.
  133.      *
  134.      * @return the raw string.
  135.      */
  136.     public String getRaw() {
  137.         return raw;
  138.     }

  139.     /**
  140.      * Get the OpenPGP User ID, which may be any string.
  141.      *
  142.      * @return the OpenPGP User ID, which may be any string.
  143.      */
  144.     public String getUserId() {
  145.         return userId;
  146.     }

  147.     /**
  148.      * Get the name portion of the User ID.
  149.      *
  150.      * @return the name portion of the User ID. If no email address would be
  151.      *         parsed by {@link #getEmailAddress()}, returns the full User ID
  152.      *         with spaces trimmed.
  153.      */
  154.     public String getName() {
  155.         int nameEnd = userId.indexOf('<');
  156.         if (nameEnd < 0 || userId.indexOf('>', nameEnd) < 0) {
  157.             nameEnd = userId.length();
  158.         }
  159.         nameEnd--;
  160.         while (nameEnd >= 0 && userId.charAt(nameEnd) == ' ') {
  161.             nameEnd--;
  162.         }
  163.         int nameBegin = 0;
  164.         while (nameBegin < nameEnd && userId.charAt(nameBegin) == ' ') {
  165.             nameBegin++;
  166.         }
  167.         return userId.substring(nameBegin, nameEnd + 1);
  168.     }

  169.     /**
  170.      * Get the email portion of the User ID
  171.      *
  172.      * @return the email portion of the User ID, if one was successfully parsed
  173.      *         from {@link #getUserId()}, or null.
  174.      */
  175.     public String getEmailAddress() {
  176.         int emailBegin = userId.indexOf('<');
  177.         if (emailBegin < 0) {
  178.             return null;
  179.         }
  180.         int emailEnd = userId.indexOf('>', emailBegin);
  181.         if (emailEnd < 0) {
  182.             return null;
  183.         }
  184.         return userId.substring(emailBegin + 1, emailEnd);
  185.     }

  186.     /**
  187.      * Get the timestamp of the identity.
  188.      *
  189.      * @return the timestamp of the identity.
  190.      */
  191.     public Date getWhen() {
  192.         return new Date(when);
  193.     }

  194.     /**
  195.      * Get this person's declared time zone
  196.      *
  197.      * @return this person's declared time zone; null if the timezone is
  198.      *         unknown.
  199.      */
  200.     public TimeZone getTimeZone() {
  201.         return PersonIdent.getTimeZone(tzOffset);
  202.     }

  203.     /**
  204.      * Get this person's declared time zone as minutes east of UTC.
  205.      *
  206.      * @return this person's declared time zone as minutes east of UTC. If the
  207.      *         timezone is to the west of UTC it is negative.
  208.      */
  209.     public int getTimeZoneOffset() {
  210.         return tzOffset;
  211.     }

  212.     /** {@inheritDoc} */
  213.     @Override
  214.     public boolean equals(Object o) {
  215.         return (o instanceof PushCertificateIdent)
  216.             && raw.equals(((PushCertificateIdent) o).raw);
  217.     }

  218.     /** {@inheritDoc} */
  219.     @Override
  220.     public int hashCode() {
  221.         return raw.hashCode();
  222.     }

  223.     /** {@inheritDoc} */
  224.     @SuppressWarnings("nls")
  225.     @Override
  226.     public String toString() {
  227.         SimpleDateFormat fmt;
  228.         fmt = new SimpleDateFormat("EEE MMM d HH:mm:ss yyyy Z", Locale.US);
  229.         fmt.setTimeZone(getTimeZone());
  230.         return getClass().getSimpleName()
  231.             + "[raw=\"" + raw + "\","
  232.             + " userId=\"" + userId + "\","
  233.             + " " + fmt.format(Long.valueOf(when)) + "]";
  234.     }
  235. }