View Javadoc
1   /*
2    * Copyright (C) 2019, Google LLC. 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  
11  package org.eclipse.jgit.transport;
12  
13  import static java.math.BigInteger.ZERO;
14  import static java.util.Objects.requireNonNull;
15  import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
16  import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT;
17  import static org.eclipse.jgit.lib.Constants.OBJ_TAG;
18  import static org.eclipse.jgit.lib.Constants.OBJ_TREE;
19  import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_FILTER;
20  
21  import java.math.BigInteger;
22  import java.text.MessageFormat;
23  
24  import org.eclipse.jgit.annotations.Nullable;
25  import org.eclipse.jgit.errors.PackProtocolException;
26  import org.eclipse.jgit.internal.JGitText;
27  
28  /**
29   * Represents either a filter specified in a protocol "filter" line, or a
30   * placeholder to indicate no filtering.
31   *
32   * @since 5.4
33   */
34  public final class FilterSpec {
35  
36  	/** Immutable bit-set representation of a set of Git object types. */
37  	static class ObjectTypes {
38  		static ObjectTypes ALL = allow(OBJ_BLOB, OBJ_TREE, OBJ_COMMIT, OBJ_TAG);
39  
40  		private final BigInteger val;
41  
42  		private ObjectTypes(BigInteger val) {
43  			this.val = requireNonNull(val);
44  		}
45  
46  		static ObjectTypes allow(int... types) {
47  			BigInteger bits = ZERO;
48  			for (int type : types) {
49  				bits = bits.setBit(type);
50  			}
51  			return new ObjectTypes(bits);
52  		}
53  
54  		boolean contains(int type) {
55  			return val.testBit(type);
56  		}
57  
58  		/** {@inheritDoc} */
59  		@Override
60  		public boolean equals(Object obj) {
61  			if (!(obj instanceof ObjectTypes)) {
62  				return false;
63  			}
64  
65  			ObjectTypes other = (ObjectTypes) obj;
66  			return other.val.equals(val);
67  		}
68  
69  		/** {@inheritDoc} */
70  		@Override
71  		public int hashCode() {
72  			return val.hashCode();
73  		}
74  	}
75  
76  	private final ObjectTypes types;
77  
78  	private final long blobLimit;
79  
80  	private final long treeDepthLimit;
81  
82  	private FilterSpec(ObjectTypes types, long blobLimit, long treeDepthLimit) {
83  		this.types = requireNonNull(types);
84  		this.blobLimit = blobLimit;
85  		this.treeDepthLimit = treeDepthLimit;
86  	}
87  
88  	/**
89  	 * Process the content of "filter" line from the protocol. It has a shape
90  	 * like:
91  	 *
92  	 * <ul>
93  	 *   <li>"blob:none"
94  	 *   <li>"blob:limit=N", with N &gt;= 0
95  	 *   <li>"tree:DEPTH", with DEPTH &gt;= 0
96  	 * </ul>
97  	 *
98  	 * @param filterLine
99  	 *            the content of the "filter" line in the protocol
100 	 * @return a FilterSpec representing the given filter
101 	 * @throws PackProtocolException
102 	 *             invalid filter because due to unrecognized format or
103 	 *             negative/non-numeric filter.
104 	 */
105 	public static FilterSpec fromFilterLine(String filterLine)
106 			throws PackProtocolException {
107 		if (filterLine.equals("blob:none")) { //$NON-NLS-1$
108 			return FilterSpec.withObjectTypes(
109 					ObjectTypes.allow(OBJ_TREE, OBJ_COMMIT, OBJ_TAG));
110 		} else if (filterLine.startsWith("blob:limit=")) { //$NON-NLS-1$
111 			long blobLimit = -1;
112 			try {
113 				blobLimit = Long
114 						.parseLong(filterLine.substring("blob:limit=".length())); //$NON-NLS-1$
115 			} catch (NumberFormatException e) {
116 				// Do not change blobLimit so that we throw a
117 				// PackProtocolException later.
118 			}
119 			if (blobLimit >= 0) {
120 				return FilterSpec.withBlobLimit(blobLimit);
121 			}
122 		} else if (filterLine.startsWith("tree:")) { //$NON-NLS-1$
123 			long treeDepthLimit = -1;
124 			try {
125 				treeDepthLimit = Long
126 						.parseLong(filterLine.substring("tree:".length())); //$NON-NLS-1$
127 			} catch (NumberFormatException e) {
128 				// Do not change blobLimit so that we throw a
129 				// PackProtocolException later.
130 			}
131 			if (treeDepthLimit >= 0) {
132 				return FilterSpec.withTreeDepthLimit(treeDepthLimit);
133 			}
134 		}
135 
136 		// Did not match any known filter format.
137 		throw new PackProtocolException(
138 				MessageFormat.format(JGitText.get().invalidFilter, filterLine));
139 	}
140 
141 	/**
142 	 * @param types
143 	 *            set of permitted object types, for use in "blob:none" and
144 	 *            "object:none" filters
145 	 * @return a filter spec which restricts to objects of the specified types
146 	 */
147 	static FilterSpec withObjectTypes(ObjectTypes types) {
148 		return new FilterSpec(types, -1, -1);
149 	}
150 
151 	/**
152 	 * @param blobLimit
153 	 *            the blob limit in a "blob:[limit]" filter line
154 	 * @return a filter spec which filters blobs above a certain size
155 	 */
156 	static FilterSpec withBlobLimit(long blobLimit) {
157 		if (blobLimit < 0) {
158 			throw new IllegalArgumentException(
159 					"blobLimit cannot be negative: " + blobLimit); //$NON-NLS-1$
160 		}
161 		return new FilterSpec(ObjectTypes.ALL, blobLimit, -1);
162 	}
163 
164 	/**
165 	 * @param treeDepthLimit
166 	 *            the tree depth limit in a "tree:[depth]" filter line
167 	 * @return a filter spec which filters blobs and trees beyond a certain tree
168 	 *         depth
169 	 */
170 	static FilterSpec withTreeDepthLimit(long treeDepthLimit) {
171 		if (treeDepthLimit < 0) {
172 			throw new IllegalArgumentException(
173 					"treeDepthLimit cannot be negative: " + treeDepthLimit); //$NON-NLS-1$
174 		}
175 		return new FilterSpec(ObjectTypes.ALL, -1, treeDepthLimit);
176 	}
177 
178 	/**
179 	 * A placeholder that indicates no filtering.
180 	 */
181 	public static final FilterSpecec.html#FilterSpec">FilterSpec NO_FILTER = new FilterSpec(ObjectTypes.ALL, -1, -1);
182 
183 	/**
184 	 * @param type
185 	 *            a Git object type, such as
186 	 *            {@link org.eclipse.jgit.lib.Constants#OBJ_BLOB}
187 	 * @return whether this filter allows objects of the specified type
188 	 *
189 	 * @since 5.9
190 	 */
191 	public boolean allowsType(int type) {
192 		return types.contains(type);
193 	}
194 
195 	/**
196 	 * @return -1 if this filter does not filter blobs based on size, or a
197 	 *         non-negative integer representing the max size of blobs to allow
198 	 */
199 	public long getBlobLimit() {
200 		return blobLimit;
201 	}
202 
203 	/**
204 	 * @return -1 if this filter does not filter blobs and trees based on depth,
205 	 *         or a non-negative integer representing the max tree depth of
206 	 *         blobs and trees to fetch
207 	 */
208 	public long getTreeDepthLimit() {
209 		return treeDepthLimit;
210 	}
211 
212 	/**
213 	 * @return true if this filter doesn't filter out anything
214 	 */
215 	public boolean isNoOp() {
216 		return types.equals(ObjectTypes.ALL) && blobLimit == -1 && treeDepthLimit == -1;
217 	}
218 
219 	/**
220 	 * @return the filter line which describes this spec, e.g. "filter blob:limit=42"
221 	 */
222 	@Nullable
223 	public String filterLine() {
224 		if (isNoOp()) {
225 			return null;
226 		} else if (types.equals(ObjectTypes.allow(OBJ_TREE, OBJ_COMMIT, OBJ_TAG)) &&
227 					blobLimit == -1 && treeDepthLimit == -1) {
228 			return OPTION_FILTER + " blob:none"; //$NON-NLS-1$
229 		} else if (types.equals(ObjectTypes.ALL) && blobLimit >= 0 && treeDepthLimit == -1) {
230 			return OPTION_FILTER + " blob:limit=" + blobLimit; //$NON-NLS-1$
231 		} else if (types.equals(ObjectTypes.ALL) && blobLimit == -1 && treeDepthLimit >= 0) {
232 			return OPTION_FILTER + " tree:" + treeDepthLimit; //$NON-NLS-1$
233 		} else {
234 			throw new IllegalStateException();
235 		}
236 	}
237 }