Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature Request: ArchRule for Comparable HashMap keys #769

Open
MahatmaFatalError opened this issue Jan 8, 2022 · 2 comments
Open

Feature Request: ArchRule for Comparable HashMap keys #769

MahatmaFatalError opened this issue Jan 8, 2022 · 2 comments

Comments

@MahatmaFatalError
Copy link

Reference: https://stackoverflow.com/questions/70507647/how-to-assert-hashmap-keys-to-be-comparable-with-archunit/70509141

VU#903934 (Hash table implementations vulnerable to algorithmic complexity attacks)
Fix with JEP180 in Java8: if too many hash codes map to the same bucket in the map, the list of entries can be changed into a balanced binary tree, sorted first by hash code and then by each key’s compareTo method, as long as the keys are Comparable.

IMHO it makes sense to provide such a rule in the standard ruleset so that others can benefit from it easily.

	@ArchTag("DoS-Protection")
	@ArchTest
	static final ArchRule fields_of_type_HashMap_should_have_Comparable_key = fields().that()
			.haveRawType(Map.class)
			.should(haveComparableFirstTypeParameter());

	@ArchTag("DoS-Protection")
	@ArchTest
	static final ArchRule code_units_should_have_parameters_of_type_Map_with_Comparable_key = codeUnits()
			.should(new ArchCondition<JavaCodeUnit>("have parameters of type Map with `Comparable` key") {
				@Override
				public void check(final JavaCodeUnit javaCodeUnit, final ConditionEvents events) {
					javaCodeUnit.getParameters().forEach(parameter -> {
						if (parameter.getRawType().isEquivalentTo(Map.class)) {
							haveComparableFirstTypeParameter().check(parameter, events);
						}
					});
				}
			});

	@ArchTag("DoS-Protection")
	@ArchTest
	static final ArchRule methods_with_return_type_Map_should_have_return_types_with_Comparable_key = methods().that()
			.haveRawReturnType(Map.class)
			.should(new ArchCondition<JavaMethod>("have return type with `Comparable` key") {
				@Override
				public void check(final JavaMethod method, final ConditionEvents events) {
					class ReturnType implements HasType, HasDescription {
						@Override
						public JavaType getType() {
							return method.getReturnType();
						}

						@Override
						public JavaClass getRawType() {
							return method.getRawReturnType();
						}

						@Override
						public String getDescription() {
							return "Return type <" + this.getType().getName() + "> of " + method.getDescription();
						}
					}
					haveComparableFirstTypeParameter().check(new ReturnType(), events);
				}
			});

	private static <T extends HasType & HasDescription> ArchCondition<T> haveComparableFirstTypeParameter() {
		return new ArchCondition<>("have `Comparable` first type parameter") {
			@Override
			public void check(final T typed, final ConditionEvents events) {
				final JavaType fieldType = typed.getType();
				if (fieldType instanceof JavaParameterizedType) {
					final JavaType keyType = ((JavaParameterizedType) fieldType).getActualTypeArguments().get(0);

					if (keyType instanceof JavaTypeVariable) {
						final boolean isGenericType = true;
						final String message = String.format(
								"%s has a generic first type parameter %s. Comparable check skipped.",
								typed.getDescription(), keyType.getName());
						events.add(new SimpleConditionEvent(typed, isGenericType, message));
					} else {
						final JavaClass erasedType = keyType.toErasure();

						final boolean isComparable = erasedType.getAllRawInterfaces()
								.stream()
								.anyMatch(rawInterface -> rawInterface.isEquivalentTo(Comparable.class));
						final String message = String.format("%s has a first type parameter %s that %s Comparable",
								typed.getDescription(), keyType.getName(), isComparable ? "is" : "is not");
						events.add(new SimpleConditionEvent(typed, isComparable, message));
					}

				} else {
					events.add(SimpleConditionEvent.violated(typed, typed.getDescription() + " is not parameterized"));
				}
			}
		};
	}

However, for a complete check, this #768 feature needs to be implemented first.

@codecholeric
Copy link
Collaborator

Thanks a lot for sharing this 😃 Yes, we could definitely add this to the predefined rules, I just wonder if we should already do this without #768? Or do you think it doesn't provide enough value then?
I kind of think most such relevant errors would be caught by the rule though, because the only maps left are then in a narrow local scope which probably reduces the risk of a targeted attack a lot, don't you think?
If we put it to the library I would make it one rule though and explain the issue behind the rule.

@MahatmaFatalError
Copy link
Author

I kind of think most such relevant errors would be caught by the rule though, because the only maps left are then in a narrow local scope which probably reduces the risk of a targeted attack a lot, don't you think?

Yes, agree.

If we put it to the library I would make it one rule though and explain the issue behind the rule.

Yes, makes sense.

Thanks for taking care 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants