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 org.eclipse.jgit.lib.PersonIdent} in that it 62 * contains a name, timestamp, and timezone offset, but differs in the following 63 * 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 always 69 * available as {@link #getUserId()}, but {@link #getEmailAddress()} may return 70 * null.</li> 71 * <li>The raw text from which the identity was parsed is available with 72 * {@link #getRaw()}. This is necessary for losslessly reconstructing the signed 73 * push 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 84 * one exception. The timestamp must be preceded by a single space, and the 85 * rest of the string prior to that space (including any additional 86 * whitespace) is treated as the OpenPGP User ID. 87 * <p> 88 * If either the timestamp or timezone offsets are missing, mimics 89 * {@link RawParseUtils#parsePersonIdent(String)} behavior and sets them 90 * both to zero. 91 * 92 * @param str 93 * string to parse. 94 * @return a {@link org.eclipse.jgit.transport.PushCertificateIdent} object. 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 /** 186 * Get the OpenPGP User ID, which may be any string. 187 * 188 * @return the OpenPGP User ID, which may be any string. 189 */ 190 public String getUserId() { 191 return userId; 192 } 193 194 /** 195 * Get the name portion of the User ID. 196 * 197 * @return the name portion of the User ID. If no email address would be 198 * parsed by {@link #getEmailAddress()}, returns the full User ID 199 * with spaces trimmed. 200 */ 201 public String getName() { 202 int nameEnd = userId.indexOf('<'); 203 if (nameEnd < 0 || userId.indexOf('>', nameEnd) < 0) { 204 nameEnd = userId.length(); 205 } 206 nameEnd--; 207 while (nameEnd >= 0 && userId.charAt(nameEnd) == ' ') { 208 nameEnd--; 209 } 210 int nameBegin = 0; 211 while (nameBegin < nameEnd && userId.charAt(nameBegin) == ' ') { 212 nameBegin++; 213 } 214 return userId.substring(nameBegin, nameEnd + 1); 215 } 216 217 /** 218 * Get the email portion of the User ID 219 * 220 * @return the email portion of the User ID, if one was successfully parsed 221 * from {@link #getUserId()}, or null. 222 */ 223 public String getEmailAddress() { 224 int emailBegin = userId.indexOf('<'); 225 if (emailBegin < 0) { 226 return null; 227 } 228 int emailEnd = userId.indexOf('>', emailBegin); 229 if (emailEnd < 0) { 230 return null; 231 } 232 return userId.substring(emailBegin + 1, emailEnd); 233 } 234 235 /** 236 * Get the timestamp of the identity. 237 * 238 * @return the timestamp of the identity. 239 */ 240 public Date getWhen() { 241 return new Date(when); 242 } 243 244 /** 245 * Get this person's declared time zone 246 * 247 * @return this person's declared time zone; null if the timezone is 248 * unknown. 249 */ 250 public TimeZone getTimeZone() { 251 return PersonIdent.getTimeZone(tzOffset); 252 } 253 254 /** 255 * Get this person's declared time zone as minutes east of UTC. 256 * 257 * @return this person's declared time zone as minutes east of UTC. If the 258 * timezone is to the west of UTC it is negative. 259 */ 260 public int getTimeZoneOffset() { 261 return tzOffset; 262 } 263 264 /** {@inheritDoc} */ 265 @Override 266 public boolean equals(Object o) { 267 return (o instanceof PushCertificateIdent) 268 && raw.equals(((PushCertificateIdent) o).raw); 269 } 270 271 /** {@inheritDoc} */ 272 @Override 273 public int hashCode() { 274 return raw.hashCode(); 275 } 276 277 /** {@inheritDoc} */ 278 @SuppressWarnings("nls") 279 @Override 280 public String toString() { 281 SimpleDateFormat fmt; 282 fmt = new SimpleDateFormat("EEE MMM d HH:mm:ss yyyy Z", Locale.US); 283 fmt.setTimeZone(getTimeZone()); 284 return getClass().getSimpleName() 285 + "[raw=\"" + raw + "\"," 286 + " userId=\"" + userId + "\"," 287 + " " + fmt.format(Long.valueOf(when)) + "]"; 288 } 289 }