1 /* 2 * Copyright (C) 2015, Google Inc. 3 * and other copyright owners as documented in the project's IP log. 4 * 5 * This program and the accompanying materials are made available 6 * under the terms of the Eclipse Distribution License v1.0 which 7 * accompanies this distribution, is reproduced below, and is 8 * available at http://www.eclipse.org/org/documents/edl-v10.php 9 * 10 * All rights reserved. 11 * 12 * Redistribution and use in source and binary forms, with or 13 * without modification, are permitted provided that the following 14 * conditions are met: 15 * 16 * - Redistributions of source code must retain the above copyright 17 * notice, this list of conditions and the following disclaimer. 18 * 19 * - Redistributions in binary form must reproduce the above 20 * copyright notice, this list of conditions and the following 21 * disclaimer in the documentation and/or other materials provided 22 * with the distribution. 23 * 24 * - Neither the name of the Eclipse Foundation, Inc. nor the 25 * names of its contributors may be used to endorse or promote 26 * products derived from this software without specific prior 27 * written permission. 28 * 29 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 30 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 31 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 32 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 33 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 34 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 35 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 36 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 37 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 38 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 39 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 40 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 41 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 42 */ 43 44 package org.eclipse.jgit.transport; 45 46 import static java.nio.charset.StandardCharsets.UTF_8; 47 48 import static org.eclipse.jgit.util.RawParseUtils.lastIndexOfTrim; 49 50 import java.text.SimpleDateFormat; 51 import java.util.Date; 52 import java.util.Locale; 53 import java.util.TimeZone; 54 55 import org.eclipse.jgit.lib.PersonIdent; 56 import org.eclipse.jgit.util.MutableInteger; 57 import org.eclipse.jgit.util.RawParseUtils; 58 59 /** 60 * Identity in a push certificate. 61 * <p> 62 * This is similar to a {@link PersonIdent} in that it contains a name, 63 * timestamp, and timezone offset, but differs in the following ways: 64 * <ul> 65 * <li>It is always parsed from a UTF-8 string, rather than a raw commit 66 * buffer.</li> 67 * <li>It is not guaranteed to contain a name and email portion, since any UTF-8 68 * string is a valid OpenPGP User ID (RFC4880 5.1.1). The raw User ID is 69 * always available as {@link #getUserId()}, but {@link #getEmailAddress()} 70 * may return null.</li> 71 * <li>The raw text from which the identity was parsed is available with {@link 72 * #getRaw()}. This is necessary for losslessly reconstructing the signed push 73 * certificate payload.</li> 74 * <li> 75 * </ul> 76 * 77 * @since 4.1 78 */ 79 public class PushCertificateIdent { 80 /** 81 * Parse an identity from a string. 82 * <p> 83 * Spaces are trimmed when parsing the timestamp and timezone offset, with one 84 * exception. The timestamp must be preceded by a single space, and the rest 85 * of the string prior to that space (including any additional whitespace) is 86 * treated as the OpenPGP User ID. 87 * <p> 88 * If either the timestamp or timezone offsets are missing, mimics {@link 89 * RawParseUtils#parsePersonIdent(String)} behavior and sets them both to 90 * zero. 91 * 92 * @param str 93 * string to parse. 94 * @return identity, never null. 95 */ 96 public static PushCertificateIdent parse(String str) { 97 MutableInteger p = new MutableInteger(); 98 byte[] raw = str.getBytes(UTF_8); 99 int tzBegin = raw.length - 1; 100 tzBegin = lastIndexOfTrim(raw, ' ', tzBegin); 101 if (tzBegin < 0 || raw[tzBegin] != ' ') { 102 return new PushCertificateIdent(str, str, 0, 0); 103 } 104 int whenBegin = tzBegin++; 105 int tz = RawParseUtils.parseTimeZoneOffset(raw, tzBegin, p); 106 boolean hasTz = p.value != tzBegin; 107 108 whenBegin = lastIndexOfTrim(raw, ' ', whenBegin); 109 if (whenBegin < 0 || raw[whenBegin] != ' ') { 110 return new PushCertificateIdent(str, str, 0, 0); 111 } 112 int idEnd = whenBegin++; 113 long when = RawParseUtils.parseLongBase10(raw, whenBegin, p); 114 boolean hasWhen = p.value != whenBegin; 115 116 if (hasTz && hasWhen) { 117 idEnd = whenBegin - 1; 118 } else { 119 // If either tz or when are non-numeric, mimic parsePersonIdent behavior and 120 // set them both to zero. 121 tz = 0; 122 when = 0; 123 if (hasTz && !hasWhen) { 124 // Only one trailing numeric field; assume User ID ends before this 125 // field, but discard its value. 126 idEnd = tzBegin - 1; 127 } else { 128 // No trailing numeric fields; User ID is whole raw value. 129 idEnd = raw.length; 130 } 131 } 132 String id = new String(raw, 0, idEnd, UTF_8); 133 134 return new PushCertificateIdent(str, id, when * 1000L, tz); 135 } 136 137 private final String raw; 138 private final String userId; 139 private final long when; 140 private final int tzOffset; 141 142 /** 143 * Construct a new identity from an OpenPGP User ID. 144 * 145 * @param userId 146 * OpenPGP User ID; any UTF-8 string. 147 * @param when 148 * local time. 149 * @param tzOffset 150 * timezone offset; see {@link #getTimeZoneOffset()}. 151 */ 152 public PushCertificateIdent(String userId, long when, int tzOffset) { 153 this.userId = userId; 154 this.when = when; 155 this.tzOffset = tzOffset; 156 StringBuilder sb = new StringBuilder(userId).append(' ').append(when / 1000) 157 .append(' '); 158 PersonIdent.appendTimezone(sb, tzOffset); 159 raw = sb.toString(); 160 } 161 162 private PushCertificateIdent(String raw, String userId, long when, 163 int tzOffset) { 164 this.raw = raw; 165 this.userId = userId; 166 this.when = when; 167 this.tzOffset = tzOffset; 168 } 169 170 /** 171 * Get the raw string from which this identity was parsed. 172 * <p> 173 * If the string was constructed manually, a suitable canonical string is 174 * returned. 175 * <p> 176 * For the purposes of bytewise comparisons with other OpenPGP IDs, the string 177 * must be encoded as UTF-8. 178 * 179 * @return the raw string. 180 */ 181 public String getRaw() { 182 return raw; 183 } 184 185 /** @return the OpenPGP User ID, which may be any string. */ 186 public String getUserId() { 187 return userId; 188 } 189 190 /** 191 * @return the name portion of the User ID. If no email address would be 192 * parsed by {@link #getEmailAddress()}, returns the full User ID with 193 * spaces trimmed. 194 */ 195 public String getName() { 196 int nameEnd = userId.indexOf('<'); 197 if (nameEnd < 0 || userId.indexOf('>', nameEnd) < 0) { 198 nameEnd = userId.length(); 199 } 200 nameEnd--; 201 while (nameEnd >= 0 && userId.charAt(nameEnd) == ' ') { 202 nameEnd--; 203 } 204 int nameBegin = 0; 205 while (nameBegin < nameEnd && userId.charAt(nameBegin) == ' ') { 206 nameBegin++; 207 } 208 return userId.substring(nameBegin, nameEnd + 1); 209 } 210 211 /** 212 * @return the email portion of the User ID, if one was successfully parsed 213 * from {@link #getUserId()}, or null. 214 */ 215 public String getEmailAddress() { 216 int emailBegin = userId.indexOf('<'); 217 if (emailBegin < 0) { 218 return null; 219 } 220 int emailEnd = userId.indexOf('>', emailBegin); 221 if (emailEnd < 0) { 222 return null; 223 } 224 return userId.substring(emailBegin + 1, emailEnd); 225 } 226 227 /** @return the timestamp of the identity. */ 228 public Date getWhen() { 229 return new Date(when); 230 } 231 232 /** 233 * @return this person's declared time zone; null if the timezone is unknown. 234 */ 235 public TimeZone getTimeZone() { 236 return PersonIdent.getTimeZone(tzOffset); 237 } 238 239 /** 240 * @return this person's declared time zone as minutes east of UTC. If the 241 * timezone is to the west of UTC it is negative. 242 */ 243 public int getTimeZoneOffset() { 244 return tzOffset; 245 } 246 247 @Override 248 public boolean equals(Object o) { 249 return (o instanceof PushCertificateIdent) 250 && raw.equals(((PushCertificateIdent) o).raw); 251 } 252 253 @Override 254 public int hashCode() { 255 return raw.hashCode(); 256 } 257 258 @SuppressWarnings("nls") 259 @Override 260 public String toString() { 261 SimpleDateFormat fmt; 262 fmt = new SimpleDateFormat("EEE MMM d HH:mm:ss yyyy Z", Locale.US); 263 fmt.setTimeZone(getTimeZone()); 264 return getClass().getSimpleName() 265 + "[raw=\"" + raw + "\"," 266 + " userId=\"" + userId + "\"," 267 + " " + fmt.format(Long.valueOf(when)) + "]"; 268 } 269 }