// Copyright 2012-2026 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // Adapted from server/client.go (header utility functions) in the NATS server Go source. using System.Text; namespace ZB.MOM.NatsNet.Server; /// /// Wire-level NATS message header constants. /// public static class NatsHeaderConstants { /// NATS header status line: "NATS/1.0\r\n". Mirrors Go hdrLine. public const string HdrLine = "NATS/1.0\r\n"; /// Empty header block with blank line terminator. Mirrors Go emptyHdrLine. public const string EmptyHdrLine = "NATS/1.0\r\n\r\n"; // JetStream expected-sequence headers (defined in server/stream.go, used by header utilities). public const string JsExpectedStream = "Nats-Expected-Stream"; public const string JsExpectedLastSeq = "Nats-Expected-Last-Sequence"; public const string JsExpectedLastSubjSeq = "Nats-Expected-Last-Subject-Sequence"; public const string JsExpectedLastSubjSeqSubj = "Nats-Expected-Last-Subject-Sequence-Subject"; public const string JsExpectedLastMsgId = "Nats-Expected-Last-Msg-Id"; // Other commonly used headers. public const string JsMsgId = "Nats-Msg-Id"; public const string JsMsgRollup = "Nats-Rollup"; public const string JsMsgSize = "Nats-Msg-Size"; public const string JsResponseType = "Nats-Response-Type"; public const string JsMessageTtl = "Nats-TTL"; public const string JsMarkerReason = "Nats-Marker-Reason"; public const string JsMessageIncr = "Nats-Incr"; public const string JsBatchId = "Nats-Batch-Id"; public const string JsBatchSeq = "Nats-Batch-Sequence"; public const string JsBatchCommit = "Nats-Batch-Commit"; // Scheduling headers. public const string JsSchedulePattern = "Nats-Schedule"; public const string JsScheduleTtl = "Nats-Schedule-TTL"; public const string JsScheduleTarget = "Nats-Schedule-Target"; public const string JsScheduleSource = "Nats-Schedule-Source"; public const string JsScheduler = "Nats-Scheduler"; public const string JsScheduleNext = "Nats-Schedule-Next"; public const string JsScheduleNextPurge = "purge"; // Rollup values. public const string JsMsgRollupSubject = "sub"; public const string JsMsgRollupAll = "all"; // Marker reasons. public const string JsMarkerReasonMaxAge = "MaxAge"; public const string JsMarkerReasonPurge = "Purge"; public const string JsMarkerReasonRemove = "Remove"; } /// /// Low-level NATS message header manipulation utilities. /// Mirrors the package-level functions in server/client.go: /// genHeader, removeHeaderIfPresent, removeHeaderIfPrefixPresent, /// getHeader, sliceHeader, getHeaderKeyIndex, setHeader. /// public static class NatsMessageHeaders { private static readonly byte[] CrLfBytes = "\r\n"u8.ToArray(); // ------------------------------------------------------------------------- // genHeader (feature 506) // ------------------------------------------------------------------------- /// /// Generates a header buffer by appending key: value\r\n to an existing header, /// or starting a fresh NATS/1.0\r\n block if is empty/null. /// Mirrors Go genHeader. /// /// Existing header bytes, or null to start fresh. /// Header key. /// Header value. public static byte[] GenHeader(byte[]? hdr, string key, string value) { var sb = new StringBuilder(); // Strip trailing CRLF from existing header to reopen for appending, // or start fresh with the header status line. const int LenCrLf = 2; if (hdr is { Length: > LenCrLf }) { // Write all but the trailing "\r\n" sb.Append(Encoding.ASCII.GetString(hdr, 0, hdr.Length - LenCrLf)); } else { sb.Append(NatsHeaderConstants.HdrLine); } // Append "key: value\r\n\r\n" (HTTP header format). sb.Append(key); sb.Append(": "); sb.Append(value); sb.Append("\r\n\r\n"); return Encoding.ASCII.GetBytes(sb.ToString()); } // ------------------------------------------------------------------------- // removeHeaderIfPresent (feature 504) // ------------------------------------------------------------------------- /// /// Removes the first occurrence of header from . /// Returns null if the result would be an empty header block. /// Mirrors Go removeHeaderIfPresent. /// public static byte[]? RemoveHeaderIfPresent(byte[] hdr, string key) { int start = GetHeaderKeyIndex(key, hdr); // Key must exist and be preceded by '\n' (not at position 0). if (start < 1 || hdr[start - 1] != '\n') return hdr; int index = start + key.Length; if (index >= hdr.Length || hdr[index] != ':') return hdr; // Find CRLF following this header line. int crlfIdx = IndexOfCrLf(hdr, start); if (crlfIdx < 0) return hdr; // Remove from 'start' through end of CRLF. int removeEnd = start + crlfIdx + 2; // +2 for "\r\n" var result = new byte[hdr.Length - (removeEnd - start)]; Buffer.BlockCopy(hdr, 0, result, 0, start); Buffer.BlockCopy(hdr, removeEnd, result, start, hdr.Length - removeEnd); // If nothing meaningful remains, return null. if (result.Length <= NatsHeaderConstants.EmptyHdrLine.Length) return null; return result; } // ------------------------------------------------------------------------- // removeHeaderIfPrefixPresent (feature 505) // ------------------------------------------------------------------------- /// /// Removes all headers whose names start with . /// Returns null if the result would be an empty header block. /// Mirrors Go removeHeaderIfPrefixPresent. /// public static byte[]? RemoveHeaderIfPrefixPresent(byte[] hdr, string prefix) { var prefixBytes = Encoding.ASCII.GetBytes(prefix); var working = hdr.ToList(); // work on a list for easy splicing int index = 0; while (index < working.Count) { // Look for prefix starting at current index. int found = IndexOf(working, prefixBytes, index); if (found < 0) break; // Must be preceded by '\n'. if (found < 1 || working[found - 1] != '\n') break; // Find CRLF after this prefix's key:value line. int crlfIdx = IndexOfCrLf(working, found + prefix.Length); if (crlfIdx < 0) break; int removeEnd = found + prefix.Length + crlfIdx + 2; working.RemoveRange(found, removeEnd - found); // Don't advance index — there may be more headers at same position. if (working.Count <= NatsHeaderConstants.EmptyHdrLine.Length) return null; } return working.ToArray(); } // ------------------------------------------------------------------------- // getHeaderKeyIndex (feature 510) // ------------------------------------------------------------------------- /// /// Returns the byte offset of in , /// or -1 if not found. /// The key must be preceded by \r\n and followed by :. /// Mirrors Go getHeaderKeyIndex. /// public static int GetHeaderKeyIndex(string key, byte[] hdr) { if (hdr.Length == 0) return -1; var bkey = Encoding.ASCII.GetBytes(key); int keyLen = bkey.Length; int hdrLen = hdr.Length; int offset = 0; while (true) { int index = IndexOf(hdr, bkey, offset); // Need index >= 2 (room for preceding \r\n) and enough space for trailing colon. if (index < 2) return -1; // Preceded by \r\n ? if (hdr[index - 1] != '\n' || hdr[index - 2] != '\r') { offset = index + keyLen; continue; } // Immediately followed by ':' ? if (index + keyLen >= hdrLen) return -1; if (hdr[index + keyLen] != ':') { offset = index + keyLen; continue; } return index; } } // ------------------------------------------------------------------------- // sliceHeader (feature 509) // ------------------------------------------------------------------------- /// /// Returns a slice of containing the value of , /// or null if not found. /// The returned slice shares memory with . /// Mirrors Go sliceHeader. /// public static ReadOnlyMemory? SliceHeader(string key, byte[] hdr) { if (hdr.Length == 0) return null; int index = GetHeaderKeyIndex(key, hdr); if (index == -1) return null; // Skip over key + ':' separator. index += key.Length + 1; int hdrLen = hdr.Length; // Skip leading whitespace. while (index < hdrLen && hdr[index] == ' ') index++; int start = index; // Collect until CRLF. while (index < hdrLen) { if (hdr[index] == '\r' && index + 1 < hdrLen && hdr[index + 1] == '\n') break; index++; } // Return a slice with capped length == value length (no extra capacity). return new ReadOnlyMemory(hdr, start, index - start); } // ------------------------------------------------------------------------- // getHeader (feature 508) // ------------------------------------------------------------------------- /// /// Returns a copy of the value for the header named , /// or null if not found. /// Mirrors Go getHeader. /// public static byte[]? GetHeader(string key, byte[] hdr) { var slice = SliceHeader(key, hdr); if (slice is null) return null; // Return a fresh copy. return slice.Value.ToArray(); } // ------------------------------------------------------------------------- // setHeader (feature 511) // ------------------------------------------------------------------------- /// /// Replaces the value of the first existing header in /// , or appends a new header if the key is absent. /// Returns a new buffer when the new value is larger; modifies in-place otherwise. /// Mirrors Go setHeader. /// public static byte[] SetHeader(string key, string val, byte[] hdr) { int start = GetHeaderKeyIndex(key, hdr); if (start >= 0) { int valStart = start + key.Length + 1; // skip past ':' int hdrLen = hdr.Length; // Preserve a single leading space if present. if (valStart < hdrLen && hdr[valStart] == ' ') valStart++; // Find the CR before the CRLF. int crIdx = IndexOf(hdr, [(byte)'\r'], valStart); if (crIdx < 0) return hdr; // malformed int valEnd = crIdx; int oldValLen = valEnd - valStart; var valBytes = Encoding.ASCII.GetBytes(val); int extra = valBytes.Length - oldValLen; if (extra > 0) { // New value is larger — must allocate a new buffer. var newHdr = new byte[hdrLen + extra]; Buffer.BlockCopy(hdr, 0, newHdr, 0, valStart); Buffer.BlockCopy(valBytes, 0, newHdr, valStart, valBytes.Length); Buffer.BlockCopy(hdr, valEnd, newHdr, valStart + valBytes.Length, hdrLen - valEnd); return newHdr; } // Write in place (new value fits). int n = valBytes.Length; Buffer.BlockCopy(valBytes, 0, hdr, valStart, n); // Shift remainder left. Buffer.BlockCopy(hdr, valEnd, hdr, valStart + n, hdrLen - valEnd); return hdr[..(valStart + n + hdrLen - valEnd)]; } // Key not present — append. bool hasTrailingCrLf = hdr.Length >= 2 && hdr[^2] == '\r' && hdr[^1] == '\n'; byte[] suffix; if (hasTrailingCrLf) { // Strip trailing CRLF, append "key: val\r\n\r\n". suffix = Encoding.ASCII.GetBytes($"{key}: {val}\r\n"); var result = new byte[hdr.Length - 2 + suffix.Length + 2]; Buffer.BlockCopy(hdr, 0, result, 0, hdr.Length - 2); Buffer.BlockCopy(suffix, 0, result, hdr.Length - 2, suffix.Length); result[^2] = (byte)'\r'; result[^1] = (byte)'\n'; return result; } suffix = Encoding.ASCII.GetBytes($"{key}: {val}\r\n"); var newBuf = new byte[hdr.Length + suffix.Length]; Buffer.BlockCopy(hdr, 0, newBuf, 0, hdr.Length); Buffer.BlockCopy(suffix, 0, newBuf, hdr.Length, suffix.Length); return newBuf; } // ------------------------------------------------------------------------- // Internal helpers // ------------------------------------------------------------------------- private static int IndexOf(byte[] haystack, byte[] needle, int offset) { var span = haystack.AsSpan(offset); int idx = span.IndexOf(needle); return idx < 0 ? -1 : offset + idx; } private static int IndexOf(List haystack, byte[] needle, int offset) { for (int i = offset; i <= haystack.Count - needle.Length; i++) { bool match = true; for (int j = 0; j < needle.Length; j++) { if (haystack[i + j] != needle[j]) { match = false; break; } } if (match) return i; } return -1; } /// Returns the offset of the first \r\n in at or after . private static int IndexOfCrLf(byte[] hdr, int offset) { var span = hdr.AsSpan(offset); int idx = span.IndexOf(CrLfBytes); return idx; // relative to offset } private static int IndexOfCrLf(List hdr, int offset) { for (int i = offset; i < hdr.Count - 1; i++) { if (hdr[i] == '\r' && hdr[i + 1] == '\n') return i - offset; } return -1; } }