Skip to content

Commit

Permalink
Multiple Filter Subjects and Subject Validation (#821)
Browse files Browse the repository at this point in the history
  • Loading branch information
scottf authored Sep 29, 2023
1 parent 45f0dab commit 78ae172
Show file tree
Hide file tree
Showing 29 changed files with 961 additions and 431 deletions.
93 changes: 48 additions & 45 deletions README.md

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion src/NATS.Client/Exceptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ public sealed class ClientExDetail
public static readonly ClientExDetail JsSubOrderedNotAllowOnQueues = new ClientExDetail(Sub, 90018, "Ordered consumer not allowed on queues.");
public static readonly ClientExDetail JsSubPushCantHaveMaxBatch = new ClientExDetail(Sub, 90019, "Push subscriptions cannot supply max batch.");
public static readonly ClientExDetail JsSubPushCantHaveMaxBytes = new ClientExDetail(Sub, 90020, "Push subscriptions cannot supply max bytes.");
public static readonly ClientExDetail JsSubSubjectNeededToLookupStream = new ClientExDetail(Sub, 90022, "Subject needed to lookup stream. Provide either a subscribe subject or a ConsumerConfiguration filter subject.");

/* Not used in this client. */ // public static readonly ClientExDetail JsSubPushAsyncCantSetPending = new ClientExDetail(Sub, 90021, "Pending limits must be set directly on the dispatcher.");

public static readonly ClientExDetail JsSoDurableMismatch = new ClientExDetail(So, 90101, "Builder durable must match the consumer configuration durable if both are provided.");
Expand All @@ -258,6 +260,7 @@ public sealed class ClientExDetail
public static readonly ClientExDetail JsSoNameMismatch = new ClientExDetail(So, 90110, "Builder name must match the consumer configuration name if both are provided.");
public static readonly ClientExDetail JsSoOrderedMemStorageNotSuppliedOrTrue = new ClientExDetail(So, 90111, "Mem Storage must be true if supplied.");
public static readonly ClientExDetail JsSoOrderedReplicasNotSuppliedOrOne = new ClientExDetail(So, 90112, "Replicas must be 1 if supplied.");
public static readonly ClientExDetail JsSoNameOrDurableRequiredForBind = new ClientExDetail(So, 90113, "Name or Durable required for Bind.");

public static readonly ClientExDetail OsObjectNotFound = new ClientExDetail(Os, 90201, "The object was not found.");
public static readonly ClientExDetail OsObjectIsDeleted = new ClientExDetail(Os, 90202, "The object is deleted.");
Expand All @@ -271,7 +274,8 @@ public sealed class ClientExDetail

public static readonly ClientExDetail JsConsumerCreate290NotAvailable = new ClientExDetail(Con, 90301, "Name field not valid when v2.9.0 consumer create api is not available.");
public static readonly ClientExDetail JsConsumerNameDurableMismatch = new ClientExDetail(Con, 90302, "Name must match durable if both are supplied.");

public static readonly ClientExDetail JsMultipleFilterSubjects210NotAvailable = new ClientExDetail(Con, 90303, "Multiple filter subjects not available until server version 2.10.0.");

private const string Sub = "SUB";
private const string So = "SO";
private const string Os = "OS";
Expand Down
131 changes: 111 additions & 20 deletions src/NATS.Client/Internals/Validator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,30 +45,83 @@ internal static void Required<TKey, TValue>(IDictionary<TKey, TValue> d, string
}
}

internal static string ValidateSubject(string s, bool required)
{
return ValidateSubject(s, "Subject", required, false);
/*
cannot contain spaces \r \n \t
cannot start or end with subject token delimiter .
some things don't allow it to end greater
*/
public static string ValidateSubjectTerm(string subject, string label, bool required)
{
Tuple<bool, string> t = IsValidSubjectTerm(subject, label, required);
if (t.Item1)
{
return t.Item2;
}
throw new ArgumentException(t.Item2);
}

public static string ValidateSubject(string subject, string label, bool required, bool cantEndWithGt) {
if (EmptyAsNull(subject) == null) {

/*
* If is valid, tuple item1 is true and item2 is the subject
* If is not valid, tuple item1 is false and item2 is the error message
*/
internal static Tuple<bool, string> IsValidSubjectTerm(string subject, string label, bool required) {
subject = EmptyAsNull(subject);
if (subject == null) {
if (required) {
throw new ArgumentException($"{label} cannot be null or empty.");
return new Tuple<bool, string>(false, $"{label} cannot be null or empty.");
}
return null;
return new Tuple<bool, string>(true, null);
}
if (subject.EndsWith(".")) {
return new Tuple<bool, string>(false, $"{label} cannot end with '.'");
}

string[] segments = subject.Split('.');
for (int x = 0; x < segments.Length; x++) {
string segment = segments[x];
if (segment.Equals(">")) {
if (cantEndWithGt || x != segments.Length - 1) { // if it can end with gt, gt must be last segment
throw new ArgumentException(label + " cannot contain '>'");
for (int seg = 0; seg < segments.Length; seg++) {
string segment = segments[seg];
int sl = segment.Length;
if (sl == 0) {
if (seg == 0) {
return new Tuple<bool, string>(false, $"{label} cannot start with '.'");
}
return new Tuple<bool, string>(false, $"{label} segment cannot be empty");
}
else {
for (int m = 0; m < sl; m++) {
char c = segment[m];
switch (c) {
case ' ':
case '\r':
case '\n':
case '\t':
return new Tuple<bool, string>(false, $"{label} cannot contain space, tab, carriage return or linefeed character");
case '*':
if (sl != 1) {
return new Tuple<bool, string>(false, $"{label} wildcard improperly placed.");
}
break;
case '>':
if (sl != 1 || seg != segments.Length - 1) {
return new Tuple<bool, string>(false, $"{label} wildcard improperly placed.");
}
break;
}
}
}
else if (!segment.Equals("*") && NotPrintable(segment)) {
throw new ArgumentException(label + " must be printable characters only.");
}
}
return new Tuple<bool, string>(true, subject);
}

public static string ValidateSubject(string s, bool required)
{
return ValidateSubject(s, "Subject", required, false);
}

public static string ValidateSubject(string subject, string label, bool required, bool cantEndWithGt) {
subject = ValidateSubjectTerm(subject, label, required);
if (subject != null && cantEndWithGt && subject.EndsWith(".>")) {
throw new ArgumentException($"{label} last segment cannot be '>'");
}
return subject;
}

Expand Down Expand Up @@ -444,7 +497,7 @@ internal static long ValidateNotNegative(long l, string label) {
// ----------------------------------------------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------------------------------------------

[Obsolete("This property is obsolete. use string.IsNullOrWhiteSpace(string) instead.", false)]
public static bool NullOrEmpty(string s)
{
return string.IsNullOrWhiteSpace(s);
Expand Down Expand Up @@ -604,13 +657,28 @@ public static bool NotPrintableOrHasWildGtDollar(string s) {

public static string EmptyAsNull(string s)
{
return NullOrEmpty(s) ? null : s;
return string.IsNullOrWhiteSpace(s) ? null : s;
}

public static string EmptyOrNullAs(string s, string ifEmpty) {
return NullOrEmpty(s) ? ifEmpty : s;
return string.IsNullOrWhiteSpace(s) ? ifEmpty : s;
}

public static IList<TSource> EmptyAsNull<TSource>(IList<TSource> list)
{
return EmptyOrNull(list) ? null : list;
}

public static bool EmptyOrNull<TSource>(IList<TSource> list)
{
return list == null || list.Count == 0;
}

public static bool EmptyOrNull<TSource>(TSource[] list)
{
return list == null || list.Length == 0;
}

public static bool ZeroOrLtMinus1(long l)
{
return l == 0 || l < -1;
Expand Down Expand Up @@ -663,7 +731,7 @@ public static bool IsSemVer(string s)
return Regex.IsMatch(s, SemVerPattern);
}

public static bool SequenceEqual<TSource>(IList<TSource> l1, IList<TSource> l2, bool nullSecondEqualsEmptyFirst = true)
public static bool SequenceEqual<T>(IList<T> l1, IList<T> l2, bool nullSecondEqualsEmptyFirst = true)
{
if (l1 == null)
{
Expand All @@ -678,6 +746,29 @@ public static bool SequenceEqual<TSource>(IList<TSource> l1, IList<TSource> l2,
return l1.SequenceEqual(l2);
}

// This function tests filter subject equivalency
// It does not care what order and also assumes that there are no duplicates.
// From the server: consumer subject filters cannot overlap [10138]
public static bool ConsumerFilterSubjectsAreEquivalent<T>(IList<T> l1, IList<T> l2)
{
if (l1 == null || l1.Count == 0)
{
return l2 == null || l2.Count == 0;
}

if (l2 == null || l1.Count != l2.Count)
{
return false;
}

foreach (T t in l1) {
if (!l2.Contains(t)) {
return false;
}
}
return true;
}

public static bool DictionariesEqual(IDictionary<string, string> d1, IDictionary<string, string> d2)
{
if (d1 == d2)
Expand Down
1 change: 1 addition & 0 deletions src/NATS.Client/JetStream/ApiConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ internal static class ApiConstants
internal const string External = "external";
internal const string Filter = "filter";
internal const string FilterSubject = "filter_subject";
internal const string FilterSubjects = "filter_subjects";
internal const string FirstSequence = "first_seq";
internal const string FirstTs = "first_ts";
internal const string FlowControl = "flow_control";
Expand Down
Loading

0 comments on commit 78ae172

Please sign in to comment.