/*
* Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved.
* This source file is part of the Cangjie project, licensed under Apache-2.0
* with Runtime Library Exception.
*
* See https://cangjie-lang.cn/pages/LICENSE for license information.
*/
/**
* @file The file declares the Regex class.
*/
package std.regex
import std.collection.*
import std.sync.*
func release(cString: CString) {
unsafe {
if (!cString.isNull()) {
LibC.free(cString)
}
}
}
func release(pcre2md: Pcre2MatchDataPtr) {
unsafe {
CJ_REGEX_FreeMatchData(pcre2md)
}
}
/**
* Iterator for lazyFindAll() of Regex
*/
class FindIterator <: Iterator<MatchData> {
var offset: Int64 = 0
private let regex: Regex
private let input: String
private let inputCString: CString
private let group: Bool
private let pcre2md: Pcre2MatchDataPtr
init(regex: Regex, input: String, group!: Bool = false) {
this.regex = regex
this.input = input
this.inputCString = unsafe { LibC.mallocCString(input) }
if (this.inputCString.isNull()) {
throw RegexException("Failed to mallocCString.")
}
this.pcre2md = unsafe { CJ_REGEX_CreateMatchData(regex.re) }
if (pcre2md.isNull()) {
release(inputCString)
throw RegexException("Create match_data for pattern `${regex.pattern}` failed.")
}
this.group = group
}
~init() {
release(inputCString)
release(pcre2md)
}
public func next(): Option<MatchData> {
let result = regex.find(input, inputCString, pcre2md, offset, group: group)
if (let Some(md) <- result) {
if (offset == md.matchPosition().end) {
offset = md.matchPosition().end + 1
} else {
offset = md.matchPosition().end
}
}
return result
}
}
/**
* Regular Expression.
* Description: Used to retrieve and replace text that conforms to a certain pattern.
*/
public class Regex {
// refers to `pcre2_code*` in PCRE2, and indicates the compiled pattern
let re: Pcre2CodePtr
var pattern: String
var flags: Array<RegexFlag>
private var namedGroups: Option<HashMap<String, Int64>> = None
public init(pattern: String, flags: Array<RegexFlag>) {
this.pattern = pattern
this.flags = flags
let options: UInt32 = RegexFlag.getValue(flags)
unsafe {
let patternCString = LibC.mallocCString(pattern)
if (patternCString.isNull()) {
throw RegexException("Failed to mallocCString.")
}
let resultCPointer: CPointer<CompileResult> = CJ_REGEX_Compile(patternCString, options)
LibC.free(patternCString)
if (resultCPointer.isNull()) {
throw RegexException("Compilation for pattern `${pattern}` failed.")
}
let result: CompileResult = resultCPointer.read()
LibC.free(resultCPointer)
if (result.re.isNull()) {
let errorMsgCString = CJ_REGEX_GetErrorMsg(result.errorCode)
if (errorMsgCString.isNull()) {
throw RegexException("Compilation for pattern `${pattern}` failed at offset ${result.errorOffset}.")
}
try {
let errorMsg = errorMsgCString.toString()
throw RegexException(
"Compilation for pattern `${pattern}` failed at offset ${result.errorOffset}: ${errorMsg}.")
} finally {
LibC.free(errorMsgCString)
}
}
this.re = result.re
keepAlive(this)
}
}
@Deprecated[message: "Use member funtion `public Regex(let pattern: String, flags: Array<RegexFlag>)` instead."]
public init(pattern: String, option: RegexOption) {
this(pattern, option.toFlags())
}
~init() {
unsafe {
CJ_REGEX_FreeCode(this.re)
}
}
@Deprecated[message: "`Matcher` has been marked as deprecated."]
public func matcher(input: String): Matcher {
return Matcher(this, input)
}
private func createInputCString(input: String): CString {
unsafe {
let inputCString = LibC.mallocCString(input)
if (inputCString.isNull()) {
throw RegexException("Failed to mallocCString.")
}
return inputCString
}
}
private func createMatchData(inputCString: CString): Pcre2MatchDataPtr {
unsafe {
let pcre2md = CJ_REGEX_CreateMatchData(this.re)
if (pcre2md.isNull()) {
release(inputCString)
throw RegexException("Create match_data for pattern `${pattern}` failed.")
}
return pcre2md
}
}
private func throwIfMatchError(result: Int64): Unit {
if (result == PCRE2_ERROR_NOMEMORY) {
throw RegexException("Failed to create match context for pattern `${pattern}`.")
}
if (result == PCRE2_ERROR_MATCHLIMIT) {
throw RegexException("Match limit exceeded for pattern `${pattern}`.")
}
if (result == PCRE2_ERROR_RECURSIONLIMIT) {
throw RegexException("Recursion limit exceeded for pattern `${pattern}`.")
}
}
/**
* Attempts to check whether input matches the pattern.
* @param input The character sequence to be matched.
* @return Bool indicates the check result
*/
public func matches(input: String): Bool {
unsafe {
let inputCString = createInputCString(input)
let pcre2md = createMatchData(inputCString)
try {
let rc = CJ_REGEX_Match(this.re, inputCString, UIntNative(input.size), UIntNative(0), pcre2md)
throwIfMatchError(rc)
keepAlive(this)
return rc >= 0
} finally {
release(inputCString)
release(pcre2md)
}
}
}
/**
* lazy getter of namedGroups, which only depending on the compiled pattern.
* So this method can be invoked as soon as `Regex` is initialized.
* return Map<String, Int64> indicates the mapping from group name to group index
*/
public func getNamedGroups(): Map<String, Int64> {
if (let Some(map) <- namedGroups) {
return map
}
unsafe {
let nameTableInfoCPointer: CPointer<NameTableInfo> = CJ_REGEX_GetNameTableInfo(this.re)
keepAlive(this)
if (nameTableInfoCPointer.isNull()) {
throw RegexException("GetNameTableInfo failed.")
}
let nameTableInfo = nameTableInfoCPointer.read()
LibC.free(nameTableInfoCPointer)
let nameCount = Int64(nameTableInfo.nameCount)
let nameEntrySize = Int64(nameTableInfo.nameEntrySize)
// nameTable cannot be `free()` because it's not created by `malloc()`
let nameTable = nameTableInfo.nameTable
let map = HashMap<String, Int64>(nameCount)
if (nameTable.isNull()) {
return map
}
var entryIndex = 0
for (_ in 0..nameCount) {
let high = Int64(nameTable.read(entryIndex + 0))
let low = Int64(nameTable.read(entryIndex + 1))
let index = (high << 8) | low
let nameRawData = Array<UInt8>(nameEntrySize - 2, repeat: 0)
for (i in 2..nameEntrySize) {
nameRawData[i - 2] = nameTable.read(entryIndex + i)
}
let name = String.fromUtf8Unchecked(nameRawData).trimEnd(r'\0')
map[name] = index
entryIndex += nameEntrySize
}
namedGroups = map
return map
}
}
/**
* find the group index by group name
*/
private func nameToIndex(name: String): Int64 {
let map = getNamedGroups()
return map.get(name) ?? throw IllegalArgumentException("Capture group `${name}` not found.")
}
/**
* Attempts to find the first match of the input sequence from the beginning.
* @param input The the input sequence.
* @param group Whether enable capture group or not. Disabled by default.
* @return Option<MatchData> indicates the match.
*/
public func find(input: String, group!: Bool = false): Option<MatchData> {
let inputCString = createInputCString(input)
let pcre2md = createMatchData(inputCString)
let result: Option<MatchData>
try {
result = find(input, inputCString, pcre2md, 0, group: group)
} finally {
release(inputCString)
release(pcre2md)
}
return result
}
/**
* Method of `internal` version with parameters `inputCString` & `offset`
* @param inputCString indicates cached CString of input sequenece
* @param offset indicates the offset of `pcre2_match()`, which used by `FindIterator` and `Matcher`
*/
func find(input: String, inputCString: CString, pcre2md: Pcre2MatchDataPtr, offset: Int64, group!: Bool = false): Option<MatchData> {
unsafe {
var md: Option<MatchData> = None
let rc = CJ_REGEX_Match(this.re, inputCString, UIntNative(input.size), UIntNative(offset), pcre2md)
throwIfMatchError(rc)
if (rc < 0) {
return md
}
// ovector cannot be `free()` because it's not created by `malloc`
let ovector: CPointer<UIntNative> = CJ_REGEX_GetOvector(pcre2md)
if (ovector.isNull()) {
throw RegexException("Process matched data failed.")
}
let positions: Array<Position>
if (group) {
positions = Array<Position>(rc, {i => Position(ovector.read(2 * i), ovector.read(2 * i + 1))})
md = MatchData(input, positions, rc, nameToIndex: nameToIndex)
} else {
positions = [Position(ovector.read(0), ovector.read(1))]
md = MatchData(input, positions, rc)
}
keepAlive(this)
return md
}
}
func allCount(input: String, begin: Int64, end: Int64): Int64 {
unsafe {
let inputCString = createInputCString(input)
let pcre2md = createMatchData(inputCString)
try {
let count = CJ_REGEX_Count(this.re, inputCString, UIntNative(input.size), UIntNative(begin),
UIntNative(end), pcre2md)
throwIfMatchError(count)
keepAlive(this)
return count
} finally {
release(inputCString)
release(pcre2md)
}
}
}
/**
* Find all matches of the input sequence from the beginning.
* @param input The the input sequence.
* @param group Whether enable capture group or not. Disabled by default.
* @return Array<MatchData> indicates the matches.
*/
public func findAll(input: String, group!: Bool = false): Array<MatchData> {
let list = ArrayList<MatchData>()
unsafe {
let inputCString = createInputCString(input)
let pcre2md = createMatchData(inputCString)
try {
var offset = 0
while (true) {
let rc = CJ_REGEX_Match(this.re, inputCString, UIntNative(input.size), UIntNative(offset), pcre2md)
throwIfMatchError(rc)
if (rc < 0) {
break
}
// ovector cannot be `free()` because it's not created by `malloc`
let ovector: CPointer<UIntNative> = CJ_REGEX_GetOvector(pcre2md)
if (ovector.isNull()) {
throw RegexException("Processing matched data failed.")
}
let positions: Array<Position>
if (group) {
positions = Array<Position>(rc, {i => Position(ovector.read(2 * i), ovector.read(2 * i + 1))})
list.add(MatchData(input, positions, rc, nameToIndex: nameToIndex))
} else {
positions = [Position(ovector.read(0), ovector.read(1))]
list.add(MatchData(input, positions, rc))
}
if (offset == positions[0].end) {
offset = positions[0].end + 1
} else {
offset = positions[0].end
}
}
keepAlive(this)
return if (list.size == list.capacity) {
list.getRawArray()
} else {
list.toArray()
}
} finally {
release(inputCString)
release(pcre2md)
}
}
}
/**
* Get an iterator for finding all matches of the input sequence from the beginning.
* @param input The the input sequence.
* @param group Whether enable capture group or not. Disabled by default.
* @return Iterator<MatchData> indicates the iterator.
*/
public func lazyFindAll(input: String, group!: Bool = false): Iterator<MatchData> {
return FindIterator(this, input, group: group)
}
/**
* Replaces the first subsequence of the input sequence that matches the
* pattern with the given replacement string from the beginning.
*
* @param input The the input sequence.
* @param replacement The character sequence to be replaced.
* @return The character that has been replaced.
*/
public func replace(input: String, replacement: String): String {
return replace(input, replacement, 0)
}
/**
* Replaces the first subsequence of the input sequence that matches the
* pattern with the given replacement string from index.
*
* @param input The the input sequence.
* @param replacement The character sequence to be replaced.
* @param index The the input sequence that matches from index.
* @return The character that has been replaced.
*
* @throws IndexOutOfBoundsException if index is less than 0 or index is greater than or equal to input.size.
*/
public func replace(input: String, replacement: String, index: Int64): String {
if (index < 0 || index >= input.size) {
throw IndexOutOfBoundsException("Invalid index: ${index} against size: ${input.size}.")
}
let sb = if (input.size == 0) {
StringBuilder()
} else {
StringBuilder(input.size)
}
var offset = 0
let inputCString = createInputCString(input)
let pcre2md = createMatchData(inputCString)
try {
if (let Some(md) <- find(input, inputCString, pcre2md, index, group: false)) {
let position = md.matchPosition()
sb.append(input[offset..position.start])
sb.append(replacement)
offset = position.end
}
sb.append(input[offset..])
return sb.toString()
} finally {
release(inputCString)
release(pcre2md)
}
}
/**
* Replaces all subsequence of the input sequence that matches the
* pattern with the given replacement string from the beginning.
*
* @param input The the input sequence.
* @param replacement The character sequence to be replaced.
* @return The character that has been replaced.
*/
public func replaceAll(input: String, replacement: String): String {
replaceAll(input, replacement, -1)
}
/**
* Replaces all subsequence of the input sequence that matches the
* pattern with the given replacement string from the beginning.
*
* if `limit` is greater than zero then it will replace the string at most `limit` times
* if `limit` is non-positive, it will replace the string as many times as possible
* if `limit` is zero, return the string
*
* @param input The the input sequence.
* @param replacement The character sequence to be replaced.
* @param limit The replacement limit.
* @return The character that has been replaced.
*/
public func replaceAll(input: String, replacement: String, limit: Int64): String {
if (limit == 0) {
return input
}
let sb = if (input.size == 0) {
StringBuilder()
} else {
StringBuilder(input.size)
}
var offset = 0
var count = 0
for (md in lazyFindAll(input, group: false)) {
if (limit > 0 && count >= limit) {
break
}
let position = md.matchPosition()
sb.append(input[offset..position.start])
sb.append(replacement)
offset = position.end
count += 1
}
sb.append(input[offset..])
return sb.toString()
}
/**
* Split the input sequence by all subsequence that matches the pattern as the delimiters.
*
* @param input The the input sequence.
* @return The array of strings computed by splitting the input
* around matches of this pattern
*/
public func split(input: String): Array<String> {
let mds = findAll(input, group: false)
let list = ArrayList<String>(mds.size + 1)
var offset = 0
for (i in 0..mds.size) {
let position = mds[i].matchPosition()
list.add(input[offset..position.start])
offset = position.end
}
list.add(input[offset..])
return unsafe { list.getRawArray() }
}
/**
* Split the input sequence by all subsequence that matches the pattern as the delimiters
*
* if limit n is greater than zero then it will separate the string at most n times
* if limit n is non-positive, it will separate the string as many times as possible
*
* @param input The the input sequence.
* @param limit The limit of split
* @return The array of strings computed by splitting the input
* around matches of this pattern
*/
public func split(input: String, limit: Int64): Array<String> {
if (limit <= 0) {
return split(input)
}
let list = ArrayList<String>(limit)
var offset = 0
var count = 0
for (md in lazyFindAll(input, group: false)) {
if (count >= limit - 1) {
break
}
let position = md.matchPosition()
list.add(input[offset..position.start])
offset = position.end
count += 1
}
list.add(input[offset..])
return if (list.size == list.capacity) {
unsafe { list.getRawArray() }
} else {
list.toArray()
}
}
public func string(): String {
return this.pattern
}
}
let _phantom = Object()
let _rf = AtomicReference<Object>(_phantom)
func keepAlive(o: Object) {
_rf.store(o)
_rf.store(_phantom)
}