Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,22 @@ include("//bazelmod:llvm.MODULE.bazel")

bazel_dep(name = "bazel_skylib", version = "1.9.0")
bazel_dep(name = "platforms", version = "1.1.0")
bazel_dep(name = "rules_cc", version = "0.2.19")
bazel_dep(name = "rules_cc", version = "0.2.21")
bazel_dep(name = "rules_shell", version = "0.8.0")
bazel_dep(name = "abseil-cpp", version = "20250814.2")
bazel_dep(name = "re2", version = "2025-11-05.bcr.1")
bazel_dep(name = "googletest", version = "1.17.0.bcr.2")

# The shared extras API (the RegexBackend plugin interface + PCRE2 registration slot), a local
# module both the xff core and every removable extra depend on - so an extra can be a standalone
# module without a dependency cycle back into the core. It names no heavy deps and is always built.
# See extra_modules/xff_extras_api and TODO.md "Extras architecture v2".
bazel_dep(name = "xff_extras_api", version = "0.0.0")
local_path_override(
module_name = "xff_extras_api",
path = "extra_modules/xff_extras_api",
)

# Composable build extras keep their external deps in their OWN extra_modules/<name>/*.MODULE.bazel
# segment, included here - so the core never names them and removing the include (or the directory)
# drops the extra entirely. PCRE2 (--regextype=PCRE2, #85): only //xff/cli:xff_full links its backend
Expand Down
2 changes: 1 addition & 1 deletion extra_modules/pcre2/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ cc_library(
visibility = ["//xff/cli:__pkg__"],
deps = [
"//xff/license:license_cc",
"//xff/regex:regex_cc",
"@pcre2",
"@xff_extras_api",
],
alwayslink = True,
)
Expand Down
33 changes: 33 additions & 0 deletions extra_modules/xff_extras_api/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# SPDX-FileCopyrightText: Copyright (c) The helly25 authors (helly25.com)
# SPDX-License-Identifier: Apache-2.0
#
# 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.

load("@rules_cc//cc:defs.bzl", "cc_library")

# The shared extras API: the RegexBackend plugin interface + the PCRE2 registration slot. Both the
# xff core (//xff/regex depends on this for the interface; Matcher::Compile(kPcre2) calls
# MakePcre2Backend) and each removable extra (extra_modules/pcre2 implements RegexBackend and
# self-registers via Pcre2Registrar) depend on this module, so an extra never depends back into the
# core. `include_prefix = "xff/regex"` keeps the header's logical path `xff/regex/backend.h`.
cc_library(
name = "xff_extras_api",
srcs = ["backend.cc"],
hdrs = ["backend.h"],
include_prefix = "xff/regex",
visibility = ["//visibility:public"],
deps = [
"@abseil-cpp//absl/status",
"@abseil-cpp//absl/status:statusor",
],
)
28 changes: 28 additions & 0 deletions extra_modules/xff_extras_api/MODULE.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# SPDX-FileCopyrightText: Copyright (c) The helly25 authors (helly25.com)
# SPDX-License-Identifier: Apache-2.0
#
# 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.

# The shared API surface that both the xff core and every removable extra depend on: the
# RegexBackend plugin interface + PCRE2 registration slot (and, later, the license-notice registry).
# It is its own Bazel module - not a core package - so an extra can be a real standalone module
# (bazel_dep on xff_extras_api + the extra's own external deps) without depending back into the xff
# core, which would be a cycle. The core and each extra both `bazel_dep` this; nothing here depends
# on the core. See TODO.md "Extras architecture v2".
module(
name = "xff_extras_api",
version = "0.0.0",
)

bazel_dep(name = "rules_cc", version = "0.2.21")
bazel_dep(name = "abseil-cpp", version = "20250814.2")
56 changes: 56 additions & 0 deletions extra_modules/xff_extras_api/backend.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-FileCopyrightText: Copyright (c) The helly25 authors (helly25.com)
// SPDX-License-Identifier: Apache-2.0
//
// 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.

#include "xff/regex/backend.h"

#include <memory>
#include <string_view>
#include <utility>

#include "absl/status/status.h"
#include "absl/status/statusor.h"

namespace xff::regex {
namespace {

// The process-wide PCRE2 backend factory, empty when no PCRE2 backend is linked. Set once at
// static-init by the real backend's Pcre2Registrar (full build only); a Meyers static so the
// registrar in another translation unit can safely write it during static initialization.
Pcre2Factory& Pcre2FactorySlot() {
static Pcre2Factory slot;
return slot;
}

} // namespace

void RegisterPcre2Backend(Pcre2Factory factory) {
Pcre2FactorySlot() = std::move(factory);
}

bool Pcre2Available() {
return static_cast<bool>(Pcre2FactorySlot());
}

absl::StatusOr<std::unique_ptr<const RegexBackend>> MakePcre2Backend(std::string_view pattern, bool case_insensitive) {
// No factory registered means the real backend was not linked (lean build): a distinct
// Unimplemented state from an InvalidArgument bad pattern, and never a silent fallback to RE2.
const Pcre2Factory& factory = Pcre2FactorySlot();
if (!factory) {
return absl::UnimplementedError("the PCRE2 regex grammar (--regextype=PCRE2) is not built into this binary");
}
return factory(pattern, case_insensitive);
}

} // namespace xff::regex
26 changes: 20 additions & 6 deletions xff/regex/backend.h → extra_modules/xff_extras_api/backend.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ namespace xff::regex {
// is built in. `Matcher` owns a `RegexBackend` behind a `unique_ptr` and forwards each operation, so
// the public API is grammar-agnostic and the concrete engine (and its dependency) stays private.
//
// This is also the seam the future lean/full split gates on: a lean build can ship only the RE2
// backend and have the PCRE2 grammar resolve to a "not built in" error, while a full build links the
// real PCRE2 backend -- neither changes this interface, `Matcher`, or the `-regextype` selection.
// One backend instance per pattern; const after construction, so matching is thread-safe.
// This is the extension seam a build extra implements. It lives in the standalone xff_extras_api
// module (not the xff core), so an extra can implement it from its own module without depending back
// into the core. A lean build ships only the RE2 backend and has the PCRE2 grammar resolve to a "not
// built in" error; a full build links the real PCRE2 backend -- neither changes this interface,
// `Matcher`, or the `-regextype` selection. One backend instance per pattern; const after
// construction, so matching is thread-safe.
class RegexBackend {
public:
RegexBackend() = default;
Expand All @@ -57,13 +59,13 @@ class RegexBackend {

// Compiles `pattern` into a PCRE2-backed RegexBackend (case-folding when `case_insensitive`), or an
// InvalidArgument error for a pattern PCRE2 rejects. The real PCRE2 backend -- built only into the
// full binary, from its own removable target under extra_modules/ -- provides one of these.
// full binary, from its own removable module under extra_modules/ -- provides one of these.
using Pcre2Factory =
std::function<absl::StatusOr<std::unique_ptr<const RegexBackend>>(std::string_view pattern, bool case_insensitive)>;

// Registers the process-wide PCRE2 backend factory. Called once, at static-init, from the real
// backend's translation unit; linkage is presence -- a lean build links no such unit, so nothing
// registers and the PCRE2 grammar reports "not built in" (see Matcher::Compile / Pcre2Available).
// registers and the PCRE2 grammar reports "not built in" (see MakePcre2Backend / Pcre2Available).
// Static-init only; not thread-safe (the matter is resolved before the walk starts).
void RegisterPcre2Backend(Pcre2Factory factory);

Expand All @@ -74,6 +76,18 @@ struct Pcre2Registrar {
explicit Pcre2Registrar(Pcre2Factory factory) { RegisterPcre2Backend(std::move(factory)); }
};

// Whether the PCRE2 grammar (kPcre2) is available in this binary, i.e. the real PCRE2 backend is
// linked and self-registered. False in the lean build (RE2 only), true in the full build. Drives
// the help "regex grammars" presence line and the Compile(kPcre2) availability check; -regextype=PCRE2
// on a binary where this is false is a clean error, never a silent RE2 fallback.
bool Pcre2Available();

// Compiles `pattern` through the registered PCRE2 factory (case-folding when `case_insensitive`).
// Returns Unimplemented when no PCRE2 backend is built in (lean build), or the factory's
// InvalidArgument for a bad pattern. Matcher::Compile(kPcre2) calls this, so the xff core reaches the
// PCRE2 seam without a direct dependency on PCRE2 or on the real backend's translation unit.
absl::StatusOr<std::unique_ptr<const RegexBackend>> MakePcre2Backend(std::string_view pattern, bool case_insensitive);

} // namespace xff::regex

#endif // XFF_REGEX_BACKEND_H_
12 changes: 5 additions & 7 deletions xff/regex/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,13 @@
load("@rules_cc//cc:defs.bzl", "cc_library", "cc_test")

# Regular-expression matching for find's -regex/-iregex, backed by RE2 (+ EXACT/FNMATCH/GLOB core
# engines, and PCRE2 when the build extra is linked). `backend.h` is the extension seam an extra
# backend implements (extra_modules/pcre2), so it is an exported header and the target is visible to
# //extra_modules too - not just //xff.
# engines, and PCRE2 when the build extra is linked). The RegexBackend interface + PCRE2 registration
# API (`xff/regex/backend.h`) now live in the standalone @xff_extras_api module - the seam an extra
# backend implements without depending back into the core - and are re-exported through regex.h.
cc_library(
name = "regex_cc",
srcs = ["regex.cc"],
hdrs = [
"backend.h", # the RegexBackend interface + PCRE2 registration API (for extra backends)
"regex.h",
],
hdrs = ["regex.h"],
visibility = [
"//extra_modules:__subpackages__",
"//xff:__subpackages__",
Expand All @@ -36,6 +33,7 @@ cc_library(
"@abseil-cpp//absl/status:statusor",
"@abseil-cpp//absl/strings",
"@re2",
"@xff_extras_api",
],
)

Expand Down
29 changes: 5 additions & 24 deletions xff/regex/regex.cc
Original file line number Diff line number Diff line change
Expand Up @@ -207,24 +207,8 @@ class FnmatchBackend final : public RegexBackend {
int flags_;
};

// The process-wide PCRE2 backend factory, empty when no PCRE2 backend is linked. Set once at
// static-init by the real backend's Pcre2Registrar (full build only); a Meyers static so the
// registrar in another TU can safely write it during static initialization.
Pcre2Factory& Pcre2FactorySlot() {
static Pcre2Factory slot;
return slot;
}

} // namespace

void RegisterPcre2Backend(Pcre2Factory factory) {
Pcre2FactorySlot() = std::move(factory);
}

bool Pcre2Available() {
return static_cast<bool>(Pcre2FactorySlot());
}

absl::StatusOr<Matcher> Matcher::Compile(std::string_view pattern, bool case_insensitive, Grammar grammar) {
// Shared RE2 compilation: kRe2 uses the pattern verbatim, kGlob its glob-to-RE2 translation. A
// lambda in this member function reaches Matcher's private constructor.
Expand All @@ -251,14 +235,11 @@ absl::StatusOr<Matcher> Matcher::Compile(std::string_view pattern, bool case_ins
// GlobToRegex escapes all input, so the result is valid RE2 (the error path is unreachable).
return compile_re2(glob::GlobToRegex(pattern));
case Grammar::kPcre2: {
// PCRE2 is a build-time extra: the real backend self-registers a factory (full build only).
// When none is registered (lean build) the grammar is not available -- a distinct Unimplemented
// state from an InvalidArgument bad pattern, and never a silent fallback to RE2.
const Pcre2Factory& factory = Pcre2FactorySlot();
if (!factory) {
return absl::UnimplementedError("the PCRE2 regex grammar (--regextype=PCRE2) is not built into this binary");
}
absl::StatusOr<std::unique_ptr<const RegexBackend>> backend = factory(pattern, case_insensitive);
// PCRE2 is a build-time extra: the real backend (extra_modules/pcre2) self-registers a factory
// in the xff_extras_api slot. MakePcre2Backend invokes it, or returns Unimplemented when no
// PCRE2 backend is linked (lean build) -- a distinct state from an InvalidArgument bad pattern,
// and never a silent fallback to RE2.
absl::StatusOr<std::unique_ptr<const RegexBackend>> backend = MakePcre2Backend(pattern, case_insensitive);
if (!backend.ok()) {
return backend.status();
}
Expand Down
3 changes: 1 addition & 2 deletions xff/regex/regex.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include <vector>

#include "absl/status/statusor.h"
#include "xff/regex/backend.h" // RegexBackend, the PCRE2 registration API, and Pcre2Available()

namespace xff::regex {

Expand All @@ -40,8 +41,6 @@ namespace xff::regex {
// and kFnmatch need no real compilation, so Compile(...) never fails for them.
enum class Grammar { kRe2, kExact, kFnmatch, kGlob, kPcre2 };

class RegexBackend; // the concrete engine (backend.h); a Matcher owns one behind a unique_ptr

// A compiled regular expression. -regex matches the whole string (FullMatch); -rxc / -grep match
// anywhere (PartialMatch / FindFirst). The grammar (RE2 default, or PCRE2) is chosen at Compile and
// the engine held behind a RegexBackend, so this API is grammar-agnostic. Move-only; const after
Expand Down
Loading