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 import static org.eclipse.jgit.util.RawParseUtils.lastIndexOfTrim; 48 49 import java.text.SimpleDateFormat; 50 import java.util.Date; 51 import java.util.Locale; 52 import java.util.TimeZone; 53 54 import org.eclipse.jgit.lib.PersonIdent; 55 import org.eclipse.jgit.util.MutableInteger; 56 import org.eclipse.jgit.util.RawParseUtils; 57 58 /** 59 * Identity in a push certificate. 60 * <p> 61 * This is similar to a {@link PersonIdent} in that it contains a name, 62 * timestamp, and timezone offset, but differs in the following ways: 63 * <ul> 64 * <li>It is always parsed from a UTF-8 string, rather than a raw commit 65 * buffer.</li> 66 * <li>It is not guaranteed to contain a name and email portion, since any UTF-8 67 * string is a valid OpenPGP User ID (RFC4880 5.1.1). The raw User ID is 68 * always available as {@link #getUserId()}, but {@link #getEmailAddress()} 69 * may return null.</li> 70 * <li>The raw text from which the identity was parsed is available with {@link 71 * #getRaw()}. This is necessary for losslessly reconstructing the signed push 72 * certificate payload.</li> 73 * <li> 74 * </ul> 75 * 76 * @since 4.1 77 */ 78 public class PushCertificateIdent { 79 /** 80 * Parse an identity from a string. 81 * <p> 82 * Spaces are trimmed when parsing the timestamp and timezone offset, with one 83 * exception. The timestamp must be preceded by a single space, and the rest 84 * of the string prior to that space (including any additional whitespace) is 85 * treated as the OpenPGP User ID. 86 * <p> 87 * If either the timestamp or timezone offsets are missing, mimics {@link 88 * RawParseUtils#parsePersonIdent(String)} behavior and sets them both to 89 * zero. 90 * 91 * @param str 92 * string to parse. 93 * @return identity, never null. 94 */ 95 public static PushCertificateIdent parse(String str) { 96 MutableInteger p = new MutableInteger(); 97 byte[] raw = str.getBytes(UTF_8); 98 int tzBegin = raw.length - 1; 99 tzBegin = lastIndexOfTrim(raw, ' ', tzBegin); 100 if (tzBegin < 0 || raw[tzBegin] != ' ') { 101 return new PushCertificateIdent(str, str, 0, 0); 102 } 103 int whenBegin = tzBegin++; 104 int tz = RawParseUtils.parseTimeZoneOffset(raw, tzBegin, p); 105 boolean hasTz = p.value != tzBegin; 106 107 whenBegin = lastIndexOfTrim(raw, ' ', whenBegin); 108 if (whenBegin < 0 || raw[whenBegin] != ' ') { 109 return new PushCertificateIdent(str, str, 0, 0); 110 } 111 int idEnd = whenBegin++; 112 long when = RawParseUtils.parseLongBase10(raw, whenBegin, p); 113 boolean hasWhen = p.value != whenBegin; 114 115 if (hasTz && hasWhen) { 116 idEnd = whenBegin - 1; 117 } else { 118 // If either tz or when are non-numeric, mimic parsePersonIdent behavior and 119 // set them both to zero. 120 tz = 0; 121 when = 0; 122 if (hasTz && !hasWhen) { 123 // Only one trailing numeric field; assume User ID ends before this 124 // field, but discard its value. 125 idEnd = tzBegin - 1; 126 } else { 127 // No trailing numeric fields; User ID is whole raw value. 128 idEnd = raw.length; 129 } 130 } 131 String id = new String(raw, 0, idEnd, UTF_8); 132 133 return new PushCertificateIdent(str, id, when * 1000L, tz); 134 } 135 136 private final String raw; 137 private final String userId; 138 private final long when; 139 private final int tzOffset; 140 141 /** 142 * Construct a new identity from an OpenPGP User ID. 143 * 144 * @param userId 145 * OpenPGP User ID; any UTF-8 string. 146 * @param when 147 * local time. 148 * @param tzOffset 149 * timezone offset; see {@link #getTimeZoneOffset()}. 150 */ 151 public PushCertificateIdent(String userId, long when, int tzOffset) { 152 this.userId = userId; 153 this.when = when; 154 this.tzOffset = tzOffset; 155 StringBuilder sb = new StringBuilder(userId).append(' ').append(when / 1000) 156 .append(' '); 157 PersonIdent.appendTimezone(sb, tzOffset); 158 raw = sb.toString(); 159 } 160 161 private PushCertificateIdent(String raw, String userId, long when, 162 int tzOffset) { 163 this.raw = raw; 164 this.userId = userId; 165 this.when = when; 166 this.tzOffset = tzOffset; 167 } 168 169 /** 170 * Get the raw string from which this identity was parsed. 171 * <p> 172 * If the string was constructed manually, a suitable canonical string is 173 * returned. 174 * <p> 175 * For the purposes of bytewise comparisons with other OpenPGP IDs, the string 176 * must be encoded as UTF-8. 177 * 178 * @return the raw string. 179 */ 180 public String getRaw() { 181 return raw; 182 } 183 184 /** @return the OpenPGP User ID, which may be any string. */ 185 public String getUserId() { 186 return userId; 187 } 188 189 /** 190 * @return the name portion of the User ID. If no email address would be 191 * parsed by {@link #getEmailAddress()}, returns the full User ID with 192 * spaces trimmed. 193 */ 194 public String getName() { 195 int nameEnd = userId.indexOf('<'); 196 if (nameEnd < 0 || userId.indexOf('>', nameEnd) < 0) { 197 nameEnd = userId.length(); 198 } 199 nameEnd--; 200 while (nameEnd >= 0 && userId.charAt(nameEnd) == ' ') { 201 nameEnd--; 202 } 203 int nameBegin = 0; 204 while (nameBegin < nameEnd && userId.charAt(nameBegin) == ' ') { 205 nameBegin++; 206 } 207 return userId.substring(nameBegin, nameEnd + 1); 208 } 209 210 /** 211 * @return the email portion of the User ID, if one was successfully parsed 212 * from {@link #getUserId()}, or null. 213 */ 214 public String getEmailAddress() { 215 int emailBegin = userId.indexOf('<'); 216 if (emailBegin < 0) { 217 return null; 218 } 219 int emailEnd = userId.indexOf('>', emailBegin); 220 if (emailEnd < 0) { 221 return null; 222 } 223 return userId.substring(emailBegin + 1, emailEnd); 224 } 225 226 /** @return the timestamp of the identity. */ 227 public Date getWhen() { 228 return new Date(when); 229 } 230 231 /** 232 * @return this person's declared time zone; null if the timezone is unknown. 233 */ 234 public TimeZone getTimeZone() { 235 return PersonIdent.getTimeZone(tzOffset); 236 } 237 238 /** 239 * @return this person's declared time zone as minutes east of UTC. If the 240 * timezone is to the west of UTC it is negative. 241 */ 242 public int getTimeZoneOffset() { 243 return tzOffset; 244 } 245 246 @Override 247 public boolean equals(Object o) { 248 return (o instanceof PushCertificateIdent) 249 && raw.equals(((PushCertificateIdent) o).raw); 250 } 251 252 @Override 253 public int hashCode() { 254 return raw.hashCode(); 255 } 256 257 @SuppressWarnings("nls") 258 @Override 259 public String toString() { 260 SimpleDateFormat fmt; 261 fmt = new SimpleDateFormat("EEE MMM d HH:mm:ss yyyy Z", Locale.US); 262 fmt.setTimeZone(getTimeZone()); 263 return getClass().getSimpleName() 264 + "[raw=\"" + raw + "\"," 265 + " userId=\"" + userId + "\"," 266 + " " + fmt.format(Long.valueOf(when)) + "]"; 267 } 268 }