]> git.ipfire.org Git - thirdparty/squid.git/commitdiff
Moved regcomp(3)-specific RegexPattern code inside RegexPattern (#972)
authorAlex Rousskov <rousskov@measurement-factory.com>
Fri, 4 Feb 2022 14:25:58 +0000 (14:25 +0000)
committerSquid Anubis <squid-anubis@squid-cache.org>
Fri, 4 Feb 2022 14:33:31 +0000 (14:33 +0000)
Also moved non-ACL regex configuration parsing code inside ConfigParser.
It is possible to move ACL (data) regex configuration parsing code as
well, but that move is a lot more complex due to regex-pattern-joining
optimizations, and there are no immediate plans to support
non-regcomp(3) regexes in ACL data. We may do that move later as we get
more experience with non-regcomp(3) regexes and decide to join them too.

These moves clean up existing regex-using code and allow adding support
for non-regcomp(3) regexes (e.g., regexes based on C++11 <regex>)
without adjusting ConfigParser::regex() callers. Such support would also
require "virtualizing" RegexPattern. To avoid increasing complexity and
hurting performance, that (simpler) step should be done only if we
decide to actually add support for non-regcomp(3) regexes.

The above changes allowed us to improve RegexData error reporting: Squid
no longer reports REs _optimization_ failure when it is an individual RE
that is broken (and reported as such). Squid still ignores the fact that
broken REs can be "optimized" into a completely "different" valid
combined RE: We do not compile individual REs unless optimization fails.

Also simplified and polished affected code.

src/ConfigParser.cc
src/ConfigParser.h
src/RefreshPattern.h
src/acl/RegexData.cc
src/base/RegexPattern.cc
src/base/RegexPattern.h
src/base/forward.h
src/cache_cf.cc
src/refresh.cc

index 5ecd4c5444d0cc080879b02e959c4f7b217758b2..58ee069c78143c53e40b343b2c77b7d4ab3e4a47 100644 (file)
@@ -9,6 +9,7 @@
 #include "squid.h"
 #include "acl/Gadgets.h"
 #include "base/Here.h"
+#include "base/RegexPattern.h"
 #include "cache_cf.h"
 #include "ConfigParser.h"
 #include "Debug.h"
@@ -477,17 +478,29 @@ ConfigParser::RegexStrtokFile()
     return token;
 }
 
-char *
-ConfigParser::RegexPattern()
+std::unique_ptr<RegexPattern>
+ConfigParser::regex(const char *expectedRegexDescription)
 {
-    if (ConfigParser::RecognizeQuotedValues) {
-        debugs(3, DBG_CRITICAL, "FATAL: Can not read regex expression while configuration_includes_quoted_values is enabled");
-        self_destruct();
-    }
+    if (RecognizeQuotedValues)
+        throw TextException("Cannot read regex expression while configuration_includes_quoted_values is enabled", Here());
+
+    SBuf pattern;
+    int flags = REG_EXTENDED | REG_NOSUB;
+
     ConfigParser::RecognizeQuotedPair_ = true;
-    char * token = NextToken();
+    const auto flagOrPattern = token(expectedRegexDescription);
+    if (flagOrPattern.cmp("-i") == 0) {
+        flags |= REG_ICASE;
+        pattern = token(expectedRegexDescription);
+    } else if (flagOrPattern.cmp("+i") == 0) {
+        flags &= ~REG_ICASE;
+        pattern = token(expectedRegexDescription);
+    } else {
+        pattern = flagOrPattern;
+    }
     ConfigParser::RecognizeQuotedPair_ = false;
-    return token;
+
+    return std::unique_ptr<RegexPattern>(new RegexPattern(pattern, flags));
 }
 
 char *
index 7c4d7bfe86585b7a089a16570ac8fda3b67634d3..ff1fc004e106796129719c5c41778f855463e1ef 100644 (file)
 #define SQUID_CONFIGPARSER_H
 
 #include "acl/forward.h"
+#include "base/forward.h"
 #include "sbuf/forward.h"
 #include "SquidString.h"
 
+#include <memory>
 #include <queue>
 #include <stack>
 #include <string>
@@ -71,6 +73,9 @@ public:
     /// parses an [if [!]<acl>...] construct
     Acl::Tree *optionalAclList();
 
+    /// extracts and returns a regex (including any optional flags)
+    std::unique_ptr<RegexPattern> regex(const char *expectedRegexDescription);
+
     static void ParseUShort(unsigned short *var);
     static void ParseBool(bool *var);
     static const char *QuoteString(const String &var);
@@ -98,12 +103,6 @@ public:
      */
     static char *RegexStrtokFile();
 
-    /**
-     * Parse the next token as a regex pattern. The regex patterns are non quoted
-     * tokens.
-     */
-    static char *RegexPattern();
-
     /**
      * Parse the next token with support for quoted values enabled even if
      * the configuration_includes_quoted_values is set to off
index 2aa5c24085919e27c04e3deeb6b2e5df57b86507..39caa3796d62e5d18948cd5f4f5f54c7c4c47bbf 100644 (file)
@@ -11,6 +11,8 @@
 
 #include "base/RegexPattern.h"
 
+#include <memory>
+
 /// a representation of a refresh pattern.
 class RefreshPattern
 {
@@ -26,11 +28,15 @@ public:
      */
 #define REFRESH_DEFAULT_MAX static_cast<time_t>(259200)
 
-    RefreshPattern(const char *aPattern, const decltype(RegexPattern::flags) &reFlags) :
-        pattern(reFlags, aPattern),
+    using RegexPointer = std::unique_ptr<RegexPattern>;
+
+    // If given a regex, becomes its owner, creating an explicit refresh_pattern
+    // rule. Otherwise, creates an implicit/default refresh_pattern rule.
+    explicit RefreshPattern(RegexPointer aRegex):
         min(0), pct(0.20), max(REFRESH_DEFAULT_MAX),
         next(NULL),
-        max_stale(0)
+        max_stale(0),
+        regex_(std::move(aRegex))
     {
         memset(&flags, 0, sizeof(flags));
     }
@@ -42,9 +48,7 @@ public:
             delete t;
         }
     }
-    // ~RefreshPattern() default destructor is fine
 
-    RegexPattern pattern;
     time_t min;
     double pct;
     time_t max;
@@ -72,7 +76,28 @@ public:
         uint64_t matchCount;
         // TODO: some stats to indicate how useful/less the flags are would be nice.
     } stats;
+
+    /// reports configuration excluding trailing options
+    void printHead(std::ostream &) const;
+
+    /// reports the configured pattern or a fake pattern of the implicit rule
+    void printPattern(std::ostream &os) const;
+
+    // TODO: Refactor external refresh_pattern rules iterators to make private.
+    /// configured regex; do not use except when iterating configured rules
+    const RegexPattern &regex() const;
+
+private:
+    /// configured regex or, for the implicit refresh_pattern rule, nil
+    RegexPointer regex_;
 };
 
+inline std::ostream &
+operator <<(std::ostream &os, const RefreshPattern &r)
+{
+    r.printHead(os);
+    return os;
+}
+
 #endif /* SQUID_REFRESHPATTERN_H_ */
 
index c70564682bdf40a797a0adf94f2f17e136999be1..0d0f949ec5fed2c6494a98b3309cf67d7b51ad20 100644 (file)
@@ -24,6 +24,7 @@
 #include "Debug.h"
 #include "sbuf/Algorithms.h"
 #include "sbuf/List.h"
+#include "sbuf/Stream.h"
 
 Acl::BooleanOptionValue ACLRegexData::CaseInsensitive_;
 
@@ -51,7 +52,7 @@ ACLRegexData::match(char const *word)
     // walk the list of patterns to see if one matches
     for (auto &i : data) {
         if (i.match(word)) {
-            debugs(28, 2, '\'' << i.c_str() << "' found in '" << word << '\'');
+            debugs(28, 2, '\'' << i << "' found in '" << word << '\'');
             // TODO: old code also popped the pattern to second place of the list
             // in order to reduce patterns search times.
             return 1;
@@ -64,25 +65,15 @@ ACLRegexData::match(char const *word)
 SBufList
 ACLRegexData::dump() const
 {
-    SBufList sl;
-    int flags = REG_EXTENDED | REG_NOSUB;
+    SBufStream os;
 
-    // walk and dump the list
-    // keeping the flags values consistent
-    for (auto &i : data) {
-        if (i.flags != flags) {
-            if ((i.flags&REG_ICASE) != 0) {
-                sl.emplace_back("-i");
-            } else {
-                sl.emplace_back("+i");
-            }
-            flags = i.flags;
-        }
-
-        sl.emplace_back(i.c_str());
+    const RegexPattern *previous = nullptr;
+    for (const auto &i: data) {
+        i.print(os, previous); // skip flags implied by the previous entry
+        previous = &i;
     }
 
-    return sl;
+    return SBufList(1, os.buf());
 }
 
 static const char *
@@ -113,45 +104,28 @@ removeUnnecessaryWildcards(char * t)
     return t;
 }
 
-static bool
-compileRE(std::list<RegexPattern> &curlist, const char * RE, int flags)
+static void
+compileRE(std::list<RegexPattern> &curlist, const SBuf &RE, int flags)
 {
-    if (RE == NULL || *RE == '\0')
-        return curlist.empty(); // XXX: old code did this. It looks wrong.
-
-    regex_t comp;
-    if (int errcode = regcomp(&comp, RE, flags)) {
-        char errbuf[256];
-        regerror(errcode, &comp, errbuf, sizeof errbuf);
-        debugs(28, DBG_CRITICAL, cfg_filename << " line " << config_lineno << ": " << config_input_line);
-        debugs(28, DBG_CRITICAL, "ERROR: invalid regular expression: '" << RE << "': " << errbuf);
-        return false;
-    }
-    debugs(28, 2, "compiled '" << RE << "' with flags " << flags);
-
-    curlist.emplace_back(flags, RE);
-    curlist.back().regex = comp;
-
-    return true;
+    curlist.emplace_back(RE, flags);
 }
 
-static bool
-compileRE(std::list<RegexPattern> &curlist, const SBufList &RE, int flags)
+static void
+compileREs(std::list<RegexPattern> &curlist, const SBufList &RE, int flags)
 {
-    if (RE.empty())
-        return curlist.empty(); // XXX: old code did this. It looks wrong.
+    assert(!RE.empty());
     SBuf regexp;
     static const SBuf openparen("("), closeparen(")"), separator(")|(");
     JoinContainerIntoSBuf(regexp, RE.begin(), RE.end(), separator, openparen,
                           closeparen);
-    return compileRE(curlist, regexp.c_str(), flags);
+    compileRE(curlist, regexp, flags);
 }
 
 /** Compose and compile one large RE from a set of (small) REs.
  * The ultimate goal is to have only one RE per ACL so that match() is
  * called only once per ACL.
  */
-static int
+static void
 compileOptimisedREs(std::list<RegexPattern> &curlist, const SBufList &sl, const int flagsAtLineStart)
 {
     std::list<RegexPattern> newlist;
@@ -168,11 +142,12 @@ compileOptimisedREs(std::list<RegexPattern> &curlist, const SBufList &sl, const
                 debugs(28, 2, "optimisation of -i ... -i" );
             } else {
                 debugs(28, 2, "-i" );
-                if (!compileRE(newlist, accumulatedRE, flags))
-                    return 0;
+                if (!accumulatedRE.empty()) {
+                    compileREs(newlist, accumulatedRE, flags);
+                    accumulatedRE.clear();
+                    reSize = 0;
+                }
                 flags |= REG_ICASE;
-                accumulatedRE.clear();
-                reSize = 0;
             }
             continue;
         } else if (configurationLineWord == plus_i) {
@@ -181,11 +156,12 @@ compileOptimisedREs(std::list<RegexPattern> &curlist, const SBufList &sl, const
                 debugs(28, 2, "optimisation of +i ... +i");
             } else {
                 debugs(28, 2, "+i");
-                if (!compileRE(newlist, accumulatedRE, flags))
-                    return 0;
+                if (!accumulatedRE.empty()) {
+                    compileREs(newlist, accumulatedRE, flags);
+                    accumulatedRE.clear();
+                    reSize = 0;
+                }
                 flags &= ~REG_ICASE;
-                accumulatedRE.clear();
-                reSize = 0;
             }
             continue;
         }
@@ -197,19 +173,18 @@ compileOptimisedREs(std::list<RegexPattern> &curlist, const SBufList &sl, const
 
         if (reSize > 1024) { // must be < BUFSIZ everything included
             debugs(28, 2, "buffer full, generating new optimised RE..." );
-            if (!compileRE(newlist, accumulatedRE, flags))
-                return 0;
+            compileREs(newlist, accumulatedRE, flags);
             accumulatedRE.clear();
             reSize = 0;
             continue;    /* do the loop again to add the RE to largeRE */
         }
     }
 
-    if (!compileRE(newlist, accumulatedRE, flags))
-        return 0;
-
-    accumulatedRE.clear();
-    reSize = 0;
+    if (!accumulatedRE.empty()) {
+        compileREs(newlist, accumulatedRE, flags);
+        accumulatedRE.clear();
+        reSize = 0;
+    }
 
     /* all was successful, so put the new list at the tail */
     curlist.splice(curlist.end(), newlist);
@@ -220,8 +195,6 @@ compileOptimisedREs(std::list<RegexPattern> &curlist, const SBufList &sl, const
         debugs(28, (opt_parse_cfg_only?DBG_IMPORTANT:2), "WARNING: there are more than 100 regular expressions. " <<
                "Consider using less REs or use rules without expressions like 'dstdomain'.");
     }
-
-    return 1;
 }
 
 static void
@@ -230,15 +203,20 @@ compileUnoptimisedREs(std::list<RegexPattern> &curlist, const SBufList &sl, cons
     auto flags = flagsAtLineStart;
 
     static const SBuf minus_i("-i"), plus_i("+i");
-    for (auto configurationLineWord : sl) {
+    for (const auto &configurationLineWord: sl) {
         if (configurationLineWord == minus_i) {
             flags |= REG_ICASE;
         } else if (configurationLineWord == plus_i) {
             flags &= ~REG_ICASE;
         } else {
-            if (!compileRE(curlist, configurationLineWord.c_str(), flags))
-                debugs(28, DBG_CRITICAL, "ERROR: Skipping regular expression. "
-                       "Compile failed: '" << configurationLineWord << "'");
+            try {
+                compileRE(curlist, configurationLineWord, flags);
+            } catch (...) {
+                // TODO: Make these configuration failures fatal (by default).
+                debugs(28, DBG_CRITICAL, "ERROR: Skipping regular expression: '" << configurationLineWord << "'" <<
+                       Debug::Extra << "configuration: " << cfg_filename << " line " << config_lineno << ": " << config_input_line <<
+                       Debug::Extra << "regex compilation failure: " << CurrentException);
+            }
         }
     }
 }
@@ -264,9 +242,19 @@ ACLRegexData::parse()
         }
     }
 
-    if (!compileOptimisedREs(data, sl, flagsAtLineStart)) {
-        debugs(28, DBG_IMPORTANT, "WARNING: optimisation of regular expressions failed; using fallback method without optimisation");
+    try {
+        // ignore the danger of merging invalid REs into a valid "optimized" RE
+        compileOptimisedREs(data, sl, flagsAtLineStart);
+    } catch (...) {
         compileUnoptimisedREs(data, sl, flagsAtLineStart);
+        // Delay compileOptimisedREs() failure reporting until we know that
+        // compileUnoptimisedREs() above have succeeded. If
+        // compileUnoptimisedREs() also fails, then the compileOptimisedREs()
+        // exception caught earlier was probably not related to _optimization_
+        // (and we do not want to report the same RE compilation problem twice).
+        debugs(28, DBG_IMPORTANT, "WARNING: Failed to optimize a set of regular expressions; will use them as-is instead;" <<
+               Debug::Extra << "configuration: " << cfg_filename << " line " << config_lineno << ": " << config_input_line <<
+               Debug::Extra << "optimization error: " << CurrentException);
     }
 }
 
index e0ec4cb8ed2c97aae1346b63ac6af667f974c2b8..7a6b8037f259d49a7da245dcc6c762658aeeb467 100644 (file)
@@ -8,38 +8,52 @@
 
 #include "squid.h"
 #include "base/RegexPattern.h"
+#include "base/TextException.h"
+#include "Debug.h"
+#include "sbuf/Stream.h"
+
+#include <iostream>
 #include <utility>
 
-RegexPattern::RegexPattern(int aFlags, const char *aPattern) :
-    flags(aFlags),
-    pattern(xstrdup(aPattern))
+RegexPattern::RegexPattern(const SBuf &aPattern, const int aFlags):
+    pattern(aPattern),
+    flags(aFlags)
 {
-    memset(&regex, 0, sizeof(regex));
-}
+    memset(&regex, 0, sizeof(regex)); // paranoid; POSIX does not require this
+    if (const auto errCode = regcomp(&regex, pattern.c_str(), flags)) {
+        char errBuf[256];
+        // for simplicity, ignore any error message truncation
+        (void)regerror(errCode, &regex, errBuf, sizeof(errBuf));
+        // POSIX examples show no regfree(&regex) after a regcomp() error;
+        // presumably, regcom() frees any allocated memory on failures
+        throw TextException(ToSBuf("POSIX regcomp(3) failure: (", errCode, ") ", errBuf,
+                                   Debug::Extra, "regular expression: ", pattern), Here());
+    }
 
-RegexPattern::RegexPattern(RegexPattern &&o) :
-    flags(std::move(o.flags)),
-    regex(std::move(o.regex)),
-    pattern(std::move(o.pattern))
-{
-    memset(&o.regex, 0, sizeof(o.regex));
-    o.pattern = nullptr;
+    debugs(28, 2, *this);
 }
 
 RegexPattern::~RegexPattern()
 {
-    xfree(pattern);
     regfree(&regex);
 }
 
-RegexPattern &
-RegexPattern::operator =(RegexPattern &&o)
+void
+RegexPattern::print(std::ostream &os, const RegexPattern * const previous) const
 {
-    flags = std::move(o.flags);
-    regex = std::move(o.regex);
-    memset(&o.regex, 0, sizeof(o.regex));
-    pattern = std::move(o.pattern);
-    o.pattern = nullptr;
-    return *this;
+    // report context-dependent explicit options and delimiters
+    if (!previous) {
+        // do not report default settings
+        if (!caseSensitive())
+            os << "-i ";
+    } else {
+        os << ' '; // separate us from the previous value
+
+        // do not report same-as-previous (i.e. inherited) settings
+        if (previous->flags != flags)
+            os << (caseSensitive() ? "+i " : "-i ");
+    }
+
+    os << pattern;
 }
 
index b5b702dc2cf43a71b12d36f5eb864a296cb99337..6f222a7973a8b590dcaf2a3f63b925f06f7a776c 100644 (file)
@@ -11,6 +11,7 @@
 
 #include "compat/GnuRegex.h"
 #include "mem/forward.h"
+#include "sbuf/SBuf.h"
 
 /**
  * A regular expression,
@@ -22,26 +23,41 @@ class RegexPattern
 
 public:
     RegexPattern() = delete;
-    RegexPattern(int aFlags, const char *aPattern);
+    RegexPattern(const SBuf &aPattern, int aFlags);
     ~RegexPattern();
 
-    // regex type varies by library, usually not safe to copy
-    RegexPattern(const RegexPattern &) = delete;
-    RegexPattern &operator =(const RegexPattern &) = delete;
+    RegexPattern(RegexPattern &&) = delete; // no copying of any kind
 
-    RegexPattern(RegexPattern &&);
-    RegexPattern &operator =(RegexPattern &&);
+    /// whether the regex differentiates letter case
+    bool caseSensitive() const { return !(flags & REG_ICASE); }
+
+    /// whether this is an "any single character" regex (".")
+    bool isDot() const { return pattern.length() == 1 && pattern[0] == '.'; }
 
-    const char * c_str() const {return pattern;}
     bool match(const char *str) const {return regexec(&regex,str,0,NULL,0)==0;}
 
-public:
-    int flags;
-    regex_t regex;
+    /// Attempts to reproduce this regex (context-sensitive) configuration.
+    /// If the previous regex is nil, may not report default flags.
+    /// Otherwise, may not report same-as-previous flags (and prepends a space).
+    void print(std::ostream &os, const RegexPattern *previous = nullptr) const;
 
 private:
-    char *pattern;
+    /// a regular expression in the text form, suitable for regcomp(3)
+    SBuf pattern;
+
+    /// bitmask of REG_* flags for regcomp(3)
+    const int flags;
+
+    /// a "compiled pattern buffer" filled by regcomp(3) for regexec(3)
+    regex_t regex;
 };
 
+inline std::ostream &
+operator <<(std::ostream &os, const RegexPattern &rp)
+{
+    rp.print(os);
+    return os;
+}
+
 #endif /* SQUID_SRC_BASE_REGEXPATTERN_H */
 
index 3e95d99e69bf1f7abaef1dea4a94407dd473d932..7a91102724d15bfd41fd5bb23d97c1efc474ae69 100644 (file)
@@ -15,6 +15,7 @@ class CallDialer;
 class CodeContext;
 class ScopedId;
 class BadOptionalAccess;
+class RegexPattern;
 
 template <typename Value> class Optional;
 
index 4250c0c0eb0bb59e6d5333f245632b31edfbaaf0..dfc2d82f187db064a167bc7d33be6b9c18618a0a 100644 (file)
@@ -2713,13 +2713,9 @@ static void
 dump_refreshpattern(StoreEntry * entry, const char *name, RefreshPattern * head)
 {
     while (head != NULL) {
-        storeAppendPrintf(entry, "%s%s %s %d %d%% %d",
-                          name,
-                          head->pattern.flags&REG_ICASE ? " -i" : null_string,
-                          head->pattern.c_str(),
-                          (int) head->min / 60,
-                          (int) (100.0 * head->pct + 0.5),
-                          (int) head->max / 60);
+        PackableStream os(*entry);
+        os << name << ' ';
+        head->printHead(os);
 
         if (head->max_stale >= 0)
             storeAppendPrintf(entry, " max-stale=%d", head->max_stale);
@@ -2761,7 +2757,6 @@ static void
 parse_refreshpattern(RefreshPattern ** head)
 {
     char *token;
-    char *pattern;
     time_t min = 0;
     double pct = 0.0;
     time_t max = 0;
@@ -2781,29 +2776,8 @@ parse_refreshpattern(RefreshPattern ** head)
 
     int i;
     RefreshPattern *t;
-    regex_t comp;
-    int errcode;
-    int flags = REG_EXTENDED | REG_NOSUB;
-
-    if ((token = ConfigParser::RegexPattern()) != NULL) {
-
-        if (strcmp(token, "-i") == 0) {
-            flags |= REG_ICASE;
-            token = ConfigParser::RegexPattern();
-        } else if (strcmp(token, "+i") == 0) {
-            flags &= ~REG_ICASE;
-            token = ConfigParser::RegexPattern();
-        }
 
-    }
-
-    if (token == NULL) {
-        debugs(3, DBG_CRITICAL, "FATAL: refresh_pattern missing the regex pattern parameter");
-        self_destruct();
-        return;
-    }
-
-    pattern = xstrdup(token);
+    auto regex = LegacyParser.regex("refresh_pattern regex");
 
     i = GetInteger();       /* token: min */
 
@@ -2870,22 +2844,12 @@ parse_refreshpattern(RefreshPattern ** head)
                   ) {
             debugs(22, DBG_PARSE_NOTE(2), "UPGRADE: refresh_pattern option '" << token << "' is obsolete. Remove it.");
         } else
-            debugs(22, DBG_CRITICAL, "ERROR: refreshAddToList: Unknown option '" << pattern << "': " << token);
-    }
-
-    if ((errcode = regcomp(&comp, pattern, flags)) != 0) {
-        char errbuf[256];
-        regerror(errcode, &comp, errbuf, sizeof errbuf);
-        debugs(22, DBG_CRITICAL, "" << cfg_filename << " line " << config_lineno << ": " << config_input_line);
-        debugs(22, DBG_CRITICAL, "ERROR: refreshAddToList: Invalid regular expression '" << pattern << "': " << errbuf);
-        xfree(pattern);
-        return;
+            debugs(22, DBG_CRITICAL, "ERROR: Unknown refresh_pattern option: " << token);
     }
 
     pct = pct < 0.0 ? 0.0 : pct;
     max = max < 0 ? 0 : max;
-    t = new RefreshPattern(pattern, flags);
-    t->pattern.regex = comp;
+    t = new RefreshPattern(std::move(regex));
     t->min = min;
     t->pct = pct;
     t->max = max;
@@ -2925,8 +2889,6 @@ parse_refreshpattern(RefreshPattern ** head)
         head = &(*head)->next;
 
     *head = t;
-
-    xfree(pattern);
 }
 
 static void
index 1207646c875ef0b4716eb94b2a03cf4886803055..21c1e37b79e0646e03f1e98e09d04b310c149a02 100644 (file)
@@ -13,6 +13,7 @@
 #endif
 
 #include "squid.h"
+#include "base/PackableStream.h"
 #include "HttpHdrCc.h"
 #include "HttpReply.h"
 #include "HttpRequest.h"
@@ -79,11 +80,10 @@ static struct RefreshCounts {
     int status[STALE_DEFAULT + 1];
 } refreshCounts[rcCount];
 
-static const RefreshPattern *refreshUncompiledPattern(const char *);
 static OBJH refreshStats;
 static int refreshStaleness(const StoreEntry * entry, time_t check_time, const time_t age, const RefreshPattern * R, stale_flags * sf);
 
-static RefreshPattern DefaultRefresh("<none>", 0);
+static RefreshPattern DefaultRefresh(nullptr);
 
 /** Locate the first refresh_pattern rule that matches the given URL by regex.
  *
@@ -94,7 +94,7 @@ refreshLimits(const char *url)
 {
     for (auto R = Config.Refresh; R; R = R->next) {
         ++(R->stats.matchTests);
-        if (R->pattern.match(url)) {
+        if (R->regex().match(url)) {
             ++(R->stats.matchCount);
             return R;
         }
@@ -103,19 +103,12 @@ refreshLimits(const char *url)
     return nullptr;
 }
 
-/** Locate the first refresh_pattern rule that has the given uncompiled regex.
- *
- * \note There is only one reference to this function, below. It always passes "." as the pattern.
- * This function is only ever called if there is no URI. Because a regex match is impossible, Squid
- * forces the "." rule to apply (if it exists)
- *
- * \return A pointer to the refresh_pattern parameters to use, or nullptr if there is no match.
- */
+/// the first explicit refresh_pattern rule that uses a "." regex (or nil)
 static const RefreshPattern *
-refreshUncompiledPattern(const char *pat)
+refreshFirstDotRule()
 {
     for (auto R = Config.Refresh; R; R = R->next) {
-        if (0 == strcmp(R->pattern.c_str(), pat))
+        if (R->regex().isDot())
             return R;
     }
 
@@ -293,13 +286,11 @@ refreshCheck(const StoreEntry * entry, HttpRequest * request, time_t delta)
      *   3. the default "." rule
      */
     // XXX: performance regression. c_str() reallocates
-    const RefreshPattern *R = (uri != nilUri) ? refreshLimits(uri.c_str()) : refreshUncompiledPattern(".");
+    const RefreshPattern *R = (uri != nilUri) ? refreshLimits(uri.c_str()) : refreshFirstDotRule();
     if (NULL == R)
         R = &DefaultRefresh;
 
-    debugs(22, 3, "Matched '" << R->pattern.c_str() << " " <<
-           (int) R->min << " " << (int) (100.0 * R->pct) << "%% " <<
-           (int) R->max << "'");
+    debugs(22, 3, "Matched '" << *R << '\'');
 
     debugs(22, 3, "\tage:\t" << age);
 
@@ -693,12 +684,13 @@ refreshStats(StoreEntry * sentry)
     storeAppendPrintf(sentry, "\nRefresh pattern usage:\n\n");
     storeAppendPrintf(sentry, "  Used      \tChecks    \t%% Matches\tPattern\n");
     for (const RefreshPattern *R = Config.Refresh; R; R = R->next) {
-        storeAppendPrintf(sentry, "  %10" PRIu64 "\t%10" PRIu64 "\t%6.2f\t%s%s\n",
+        storeAppendPrintf(sentry, "  %10" PRIu64 "\t%10" PRIu64 "\t%6.2f\t",
                           R->stats.matchCount,
                           R->stats.matchTests,
-                          xpercent(R->stats.matchCount, R->stats.matchTests),
-                          (R->pattern.flags&REG_ICASE ? "-i " : ""),
-                          R->pattern.c_str());
+                          xpercent(R->stats.matchCount, R->stats.matchTests));
+        PackableStream os(*sentry);
+        R->printPattern(os);
+        os << "\n";
     }
 
     int i;
@@ -727,6 +719,33 @@ refreshStats(StoreEntry * sentry)
         refreshCountsStats(sentry, refreshCounts[i]);
 }
 
+const RegexPattern &
+RefreshPattern::regex() const
+{
+    assert(regex_);
+    return *regex_;
+}
+
+void
+RefreshPattern::printPattern(std::ostream &os) const
+{
+    if (regex_)
+        regex_->print(os, nullptr); // refresh lines do not inherit line flags
+    else
+        os << "<none>";
+}
+
+void
+RefreshPattern::printHead(std::ostream &os) const
+{
+    printPattern(os);
+    os <<
+       // these adjustments are safe: raw values were configured using integers
+       ' ' << intmax_t(min/60) << // to minutes
+       ' ' << intmax_t(100.0 * pct + 0.5) << '%' << // to percentage points
+       ' ' << intmax_t(max/60); // to minutes
+}
+
 static void
 refreshRegisterWithCacheManager(void)
 {