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
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,22 @@ jobs:
timeout-minutes: 30

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v5

- name: Set up JDK
uses: actions/setup-java@v2
uses: actions/setup-java@v5
with:
java-version: 8
distribution: 'temurin'

- uses: burrunan/gradle-cache-action@cbdf4342ff988d143aa7a5aeceedffafb8c74bcf #v1.10
- uses: burrunan/gradle-cache-action@663fbad34e03c8f12b27f4999ac46e3d90f87eca # v3.0
name: Build with Gradle
with:
arguments: build

- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: Test Results Linux
path: '**/test-results/**/*.xml'
48 changes: 48 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Publish to Maven Central

on:
push:
branches:
- main

jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5

- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: 11
distribution: 'temurin'

- name: Publish to Maven Central
run: ./gradlew publish
env:
ORG_GRADLE_PROJECT_NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }}
ORG_GRADLE_PROJECT_NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }}
ORG_GRADLE_PROJECT_SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }}
ORG_GRADLE_PROJECT_SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
ORG_GRADLE_PROJECT_SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}

- name: Check version
id: version
run: |
version=$(grep "generexVersion" gradle.properties | cut -d'=' -f2 | tr -d ' ')
if [[ "$version" != *"SNAPSHOT"* ]]; then
echo "is_release=true" >> $GITHUB_OUTPUT
else
echo "is_release=false" >> $GITHUB_OUTPUT
fi

- name: Notify Central Publisher Portal
if: steps.version.outputs.is_release == 'true'
run: |
token=$(echo -n "${{ secrets.NEXUS_USERNAME }}:${{ secrets.NEXUS_PASSWORD }}" | base64)
curl -X POST \
--max-time 30 \
--fail-with-body \
-H "Authorization: Bearer $token" \
"https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/com.pkware.generex?publishing_type=automatic"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ If you use [Maven](http://maven.apache.org) you can include this library to your
<dependency>
<groupId>com.pkware.generex</groupId>
<artifactId>generex</artifactId>
<version>1.1.0</version>
<version>1.2.0</version>
</dependency>
```

Expand Down
23 changes: 20 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ publishing {
}
repositories {
maven {
name = "MavenCentral"
url = uri(if (version.toString().isReleaseBuild) releaseRepositoryUrl else snapshotRepositoryUrl)
credentials {
username = repositoryUsername
Expand All @@ -81,8 +82,15 @@ publishing {
}

signing {
// Signing credentials are stored locally in the user's global gradle.properties file.
// Signing credentials are stored as secrets in GitHub.
// See https://docs.gradle.org/current/userguide/signing_plugin.html#sec:signatory_credentials for more information.

useInMemoryPgpKeys(
signingKeyId,
signingKey,
signingPassword,
)

sign(publishing.publications["mavenJava"])
}

Expand All @@ -92,13 +100,13 @@ val String.isReleaseBuild
val Project.releaseRepositoryUrl: String
get() = properties.getOrDefault(
"RELEASE_REPOSITORY_URL",
"https://oss.sonatype.org/service/local/staging/deploy/maven2"
"https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2",
).toString()

val Project.snapshotRepositoryUrl: String
get() = properties.getOrDefault(
"SNAPSHOT_REPOSITORY_URL",
"https://oss.sonatype.org/content/repositories/snapshots"
"https://central.sonatype.com/repository/maven-snapshots/",
).toString()

val Project.repositoryUsername: String
Expand All @@ -107,6 +115,15 @@ val Project.repositoryUsername: String
val Project.repositoryPassword: String
get() = properties.getOrDefault("NEXUS_PASSWORD", "").toString()

val Project.signingKeyId: String
get() = properties.getOrDefault("SIGNING_KEY_ID", "").toString()

val Project.signingKey: String
get() = properties.getOrDefault("SIGNING_KEY", "").toString()

val Project.signingPassword: String
get() = properties.getOrDefault("SIGNING_PASSWORD", "").toString()

val Project.pomPackaging: String
get() = properties.getOrDefault("POM_PACKAGING", "jar").toString()

Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
kotlin.code.style=official
generexVersion=1.1.0
generexVersion=1.2.0

POM_ARTIFACT_ID=generex
POM_NAME=Generex
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* limitations under the License.
*/

package com.mifmif.common.regex;
package com.pkware.generex;

import dk.brics.automaton.Automaton;
import dk.brics.automaton.RegExp;
Expand Down Expand Up @@ -60,6 +60,13 @@ public class Generex implements Iterable<String> {
private Node rootNode;
private boolean isTransactionNodeBuilt;

/**
* Determined possible minimum and maximum length of a regex by traversing the
* Automaton tree using depth first search.
*/
private Integer cachedMinLength;
private Integer cachedMaxLength;

/**
* The maximum length a produced string for an infinite regex if {@link #random(int, int)} hasn't been given a max
* length other than {@link Integer#MAX_VALUE}.
Expand Down Expand Up @@ -317,7 +324,9 @@ public String random() {
* See {@link #random(int, int)}
*/
public String random(int minLength) {
return random(minLength, automaton.isFinite() ? Integer.MAX_VALUE : DEFAULT_INFINITE_MAX_LENGTH);
calculateLengthBounds();
int actualMaxLength = isInfinite() ? DEFAULT_INFINITE_MAX_LENGTH : cachedMaxLength;
return random(minLength, actualMaxLength);
}

/**
Expand All @@ -342,8 +351,21 @@ public String random(int minLength) {
* given range. Otherwise, see the {@code minLength} and {@code maxLength} docs.
*/
public String random(int minLength, int maxLength) {
calculateLengthBounds();

// Calculate actual valid range by comparing the regex and the user defined bounds
int actualMinLength = Math.max(minLength, cachedMinLength);
int actualMaxLength = Math.min(maxLength, isInfinite() ? maxLength : cachedMaxLength);

// Pre-select target length uniformly from valid range
int targetLength;
if (actualMinLength > actualMaxLength) {
targetLength = actualMaxLength;
} else {
targetLength = actualMinLength + random.nextInt(actualMaxLength - actualMinLength + 1);
}

String result = prepareRandom("", automaton.getInitialState(), minLength, maxLength);
String result = prepareRandom("", automaton.getInitialState(), minLength, maxLength, targetLength);
// Substring in case a length of 'maxLength + 1' is returned, which is possible if a smaller string can't be produced.
return result.substring(0, Math.min(maxLength, result.length()));
}
Expand All @@ -357,19 +379,26 @@ public String random(int minLength, int maxLength) {
* @param maxLength Maximum wanted length of produced string.
* @return A string built from the accumulation of previous transitions.
*/
private String prepareRandom(String currentMatch, State state, int minLength, int maxLength) {
private String prepareRandom(String currentMatch, State state, int minLength, int maxLength, int targetLength) {

// Return a string of length 'maxLength + 1' to indicate a dead branch.
if (currentMatch.length() > maxLength) return currentMatch;
if (currentMatch.length() > maxLength || state.getTransitions().isEmpty()) return currentMatch;

if (state.isAccept() && shouldTerminate(currentMatch.length(), minLength, maxLength)) return currentMatch;
String returnValue = null;

if (state.isAccept()) {
// Set the current match to the value to return, just in case this would happen to be the cloest match to
// the target length.
returnValue = currentMatch;

if (currentMatch.length() == targetLength) return currentMatch;
}

// Make a copy so the original set is never modified.
Set<Transition> possibleTransitions = new HashSet<>(state.getTransitions());
int totalWeightedTransitions = calculateTotalWeightedTransitions(possibleTransitions);

String returnValue = currentMatch;

// Will never start as empty due to the initial if statement in the function.
while (!possibleTransitions.isEmpty()) {

Transition randomTransition = pickRandomWeightedTransition(possibleTransitions, totalWeightedTransitions);
Expand All @@ -378,39 +407,88 @@ private String prepareRandom(String currentMatch, State state, int minLength, in
possibleTransitions.remove(randomTransition);

char randomChar = (char) (random.nextInt(subTransitions) + randomTransition.getMin());
String result = prepareRandom(currentMatch + randomChar, randomTransition.getDest(), minLength, maxLength);
String result = prepareRandom(currentMatch + randomChar, randomTransition.getDest(), minLength, maxLength, targetLength);

// Greedily return the first valid result found.
if (minLength <= result.length() && result.length() <= maxLength) return result;
// Greedily return the first valid result found that is of the wanted length..
if (result.length() == targetLength) return result;

// Continue to search for a valid result if the result is greater than the max length, or if the result is
// less than the minimum length. In the case a result never reaches the minimum length, return the longest
// match found.
if (returnValue.length() < result.length()) returnValue = result;
returnValue = getBestMatch(result, returnValue, minLength, maxLength, targetLength);
}

return returnValue;
}

/**
* Attempts to randomly terminate regexes in a way where a uniform distribution of lengths is produced by initially
* having a low probability of termination when close to the minimum length, and linearly increasing this
* probability as a regex nears its maximum requested length.
* <br>
* In practice this doesn't work well when an infinitely repeating part of the regex is located in the middle with a
* non-repeating terminal ending, but still works better than a flat chance of termination regardless of the range
* of lengths requested.
* Determines if the new generation is better than the current generation.
* <p></p>
* The new generation is better if it is within the bounds and is closer to the target length than the current
* generation. Otherwise, the current generation is better.
*
* @param newMatch the new generation to compare against the current generation.
* @param currentMatch the current generation.
* @param min minimum length of the generated string.
* @param max maximum length of the generated string.
* @param target the target length of the generated string.
* @return the best match between the new generation and the current generation.
*/
private String getBestMatch(String newMatch, String currentMatch, int min, int max, int target) {

if (currentMatch == null) return newMatch;
if (newMatch.length() > max && currentMatch.length() > min) return currentMatch;

boolean newInRange = newMatch.length() >= min;
boolean currentInRange = currentMatch.length() >= min && currentMatch.length() <= max;

if (newInRange && !currentInRange) return newMatch;
if (currentInRange && !newInRange) return currentMatch;

int currentTargetDistance = Math.abs(currentMatch.length() - target);
int newTargetDistance = Math.abs(newMatch.length() - target);

if (newTargetDistance < currentTargetDistance) return newMatch;
return currentMatch;
}

/**
* Calculate the possible bounds of the generated string by traversing the regex
*/
private void calculateLengthBounds() {
if (cachedMinLength != null) return;

int[] bounds = dfsLengthBounds(automaton.getInitialState(), new HashSet<>());
cachedMinLength = bounds[0];
cachedMaxLength = bounds[1];
}

/**
* Uses a depth first search to calculate the minimum and maximum length of the regex by
* traversing through the automaton tree.
* <br>
* It is assumed `maxLength` is not an absurdly large value, as this could allow the regex to grow extremely long,
* and that `maxLength` is greater than `minLength`
* We can use DFS because the automaton is finite (does not contain infinite loops) and
* we need to visit every state regardless to determine the longest length.
*
* @param depth Size of the current string produced to match a regex.
* @param minLength Minimum wanted length of the produced string.
* @param maxLength Maximum wanted length of the produced string.
* @return Whether the current string should be returned as a match for the regex.
* @param state the current state of the automaton.
* @param visited the set of visited states.
* @return an int array containing the minimum and maximum length of the regex.
*/
private boolean shouldTerminate(int depth, int minLength, int maxLength) {
return depth >= minLength && random.nextInt(maxLength - depth + 1) == 0;
private int[] dfsLengthBounds(State state, Set<State> visited) {
if (visited.contains(state)) return new int[]{Integer.MAX_VALUE, 0};

int minLength = state.isAccept() ? 0 : Integer.MAX_VALUE;
int maxLength = 0;

visited.add(state);

for (Transition transition : state.getTransitions()) {
int[] bounds = dfsLengthBounds(transition.getDest(), visited);
if (bounds[0] != Integer.MAX_VALUE) {
minLength = Math.min(minLength, bounds[0] + 1);
}
maxLength = Math.max(maxLength, bounds[1] + 1);
}

visited.remove(state);
return new int[]{minLength, maxLength};
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.mifmif.common.regex;
package com.pkware.generex;

import dk.brics.automaton.State;
import dk.brics.automaton.Transition;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.mifmif.common.regex;
package com.pkware.generex;

/**
* @author y.mifrah
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.mifmif.common.regex;
package com.pkware.generex;

import java.util.ArrayList;
import java.util.List;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.mifmif.common.regex;
package com.pkware.generex;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.mifmif.common.regex;
package com.pkware.generex;

import dk.brics.automaton.Automaton;
import dk.brics.automaton.State;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.mifmif.common.regex;
package com.pkware.generex;

import kotlin.ranges.IntRange;
import org.junit.jupiter.params.ParameterizedTest;
Expand Down
Loading
Loading