Skip to content
Open
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
19 changes: 19 additions & 0 deletions audit/mutation-test-scans.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[
{
"timestamp": "2026-06-15T00:00:00Z",
"commit": "dc85846152e2fd42f8f2c07feb72cc1abc708674",
"publishedTag": "v0.2.0",
"commitsAheadOfTag": 0,
Comment on lines +3 to +6

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

recorded_commit="$(jq -r '.[0].commit' audit/mutation-test-scans.json)"
published_tag="$(jq -r '.[0].publishedTag' audit/mutation-test-scans.json)"
expected_ahead="$(jq -r '.[0].commitsAheadOfTag' audit/mutation-test-scans.json)"

tag_commit="$(git rev-parse "${published_tag}^{commit}")"
actual_ahead="$(git rev-list --count "${tag_commit}..${recorded_commit}")"

test "${tag_commit}" = "${recorded_commit}"
test "${actual_ahead}" -eq "${expected_ahead}"

git diff --quiet "${recorded_commit}" HEAD -- \
  src/lib/parse/LibFixedPointDecimalParse.sol \
  test/src/lib/parse/LibFixedPointDecimalParse.decimalStringToFixedPoint.t.sol

Repository: rainlanguage/rain.math.fixedpoint

Length of output: 371


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- record ---'
cat -n audit/mutation-test-scans.json

printf '%s\n' '--- local refs and objects ---'
git show-ref --tags | tail -20 || true
git rev-parse --verify dc85846152e2fd42f8f2c07feb72cc1abc708674^{commit} 2>&1 || true
git rev-parse --verify HEAD
git diff --stat dc85846152e2fd42f8f2c07feb72cc1abc708674 HEAD -- \
  src/lib/parse/LibFixedPointDecimalParse.sol \
  test/src/lib/parse/LibFixedPointDecimalParse.decimalStringToFixedPoint.t.sol 2>&1 || true

printf '%s\n' '--- remote metadata ---'
git remote -v || true

Repository: rainlanguage/rain.math.fixedpoint

Length of output: 1291


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

repo='rainlanguage/rain.math.fixedpoint'
tag='v0.2.0'
sha='dc85846152e2fd42f8f2c07feb72cc1abc708674'

api() {
  curl --fail --silent --show-error \
    -H 'Accept: application/vnd.github+json' \
    "https://api.github.com/$1"
}

printf '%s\n' '--- tag ref ---'
api "repos/${repo}/git/ref/tags/${tag}" | jq '{ref, object}'

printf '%s\n' '--- recorded commit ---'
api "repos/${repo}/commits/${sha}" | jq '{sha, message: .commit.message, date: .commit.committer.date, parents: [.parents[].sha]}'

printf '%s\n' '--- tag commit list ---'
api "repos/${repo}/commits?sha=${tag}&per_page=100" |
  jq --arg sha "$sha" '[.[] | {sha, date: .commit.committer.date}] | {count: length, recorded: map(select(.sha == $sha))}'

printf '%s\n' '--- repository contents at recorded commit ---'
for path in \
  src/lib/parse/LibFixedPointDecimalParse.sol \
  test/src/lib/parse/LibFixedPointDecimalParse.decimalStringToFixedPoint.t.sol
do
  printf '%s\n' "PATH: $path"
  api "repos/${repo}/contents/${path}?ref=${sha}" |
    jq '{path, sha, size}'
done

Repository: rainlanguage/rain.math.fixedpoint

Length of output: 238


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

sha='dc85846152e2fd42f8f2c07feb72cc1abc708674'
tag='v0.2.0'

printf '%s\n' '--- all local tag refs ---'
git show-ref --tags --dereference || true

printf '%s\n' '--- recorded commit ---'
git show -s --format='commit=%H%nparents=%P%nauthor=%aI%ncommitter=%cI%ntitle=%s' "$sha"

printf '%s\n' '--- current commit ---'
git show -s --format='commit=%H%nparents=%P%nauthor=%aI%ncommitter=%cI%ntitle=%s' HEAD

printf '%s\n' '--- remote tag refs ---'
git ls-remote --tags origin "refs/tags/${tag}" "refs/tags/${tag}^{}" || true

printf '%s\n' '--- ancestry and distance ---'
if git cat-file -e "${sha}^{commit}" 2>/dev/null; then
  printf 'recorded_is_ancestor_of_HEAD=%s\n' \
    "$(git merge-base --is-ancestor "$sha" HEAD; printf '%s' "$?")"
  git rev-list --count "${sha}..HEAD"
fi

printf '%s\n' '--- scoped file blob IDs at scan and HEAD ---'
for path in \
  src/lib/parse/LibFixedPointDecimalParse.sol \
  test/src/lib/parse/LibFixedPointDecimalParse.decimalStringToFixedPoint.t.sol
do
  printf 'PATH %s\n' "$path"
  printf 'scan='
  git rev-parse "${sha}:${path}" 2>&1 || true
  printf 'head='
  git rev-parse "HEAD:${path}" 2>&1 || true
  git diff --quiet "$sha" HEAD -- "$path"; printf 'unchanged=%s\n' "$?"
done

Repository: rainlanguage/rain.math.fixedpoint

Length of output: 1198


🏁 Script executed:

#!/usr/bin/env bash
set -u

sha='dc85846152e2fd42f8f2c07feb72cc1abc708674'
tag_commit='a535add9a2d76dfc0464af0177dbe53ef13ed600'

printf '%s\n' '--- tag relation ---'
printf 'tag_commit=%s\n' "$tag_commit"
printf 'recorded_commit=%s\n' "$sha"
printf 'recorded_equals_tag=%s\n' "$([ "$tag_commit" = "$sha" ] && echo true || echo false)"
printf 'commits_ahead=%s\n' "$(git rev-list --count "${tag_commit}..${sha}")"

printf '%s\n' '--- scoped file comparison ---'
for path in \
  src/lib/parse/LibFixedPointDecimalParse.sol \
  test/src/lib/parse/LibFixedPointDecimalParse.decimalStringToFixedPoint.t.sol
do
  printf 'PATH %s\n' "$path"
  printf 'scan_blob='
  git rev-parse "${sha}:${path}" 2>&1 || true
  printf 'head_blob='
  git rev-parse "HEAD:${path}" 2>&1 || true
  if git diff --quiet "$sha" HEAD -- "$path"; then
    printf 'unchanged=true\n'
  else
    printf 'unchanged=false\n'
  fi
done

printf '%s\n' '--- scoped changes after recorded commit ---'
git diff --name-status "$sha" HEAD -- \
  src/lib/parse/LibFixedPointDecimalParse.sol \
  test/src/lib/parse/LibFixedPointDecimalParse.decimalStringToFixedPoint.t.sol

printf '%s\n' '--- remote tag resolution ---'
git ls-remote --tags origin 'refs/tags/v0.2.0' 'refs/tags/v0.2.0^{}'

Repository: rainlanguage/rain.math.fixedpoint

Length of output: 1060


Correct the mutation-scan audit record.

v0.2.0 resolves to a535add9a2d76dfc0464af0177dbe53ef13ed600. The recorded commit is two commits ahead, not zero. Both scoped files changed between the recorded commit and HEAD. Rerun the scan at the current revision and update the audit record.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@audit/mutation-test-scans.json` around lines 3 - 6, Update the mutation-scan
audit record for publishedTag v0.2.0 by rerunning the scan at the current HEAD,
replacing the stale commit and commitsAheadOfTag values with the resulting
revision and count, and refreshing the timestamp as appropriate. Preserve the
existing audit record format and scoped-file coverage.

"scope": "LibFixedPointDecimalParse.decimalStringTofixedPoint",
"tool": "adversarial-mutation-test",
"skillVersion": "0.24.0",
"summary": {
"behaviours": 15,
"survivingMutantsVsExisting": 2,
"gapsFilled": 2,
"equivalentMutants": 2,
"newTests": 2,
"filed": []
}
}
]
10 changes: 9 additions & 1 deletion src/lib/parse/LibFixedPointDecimalParse.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ library LibFixedPointDecimalParse {
/// Converts a decimal string to a fixed point decimal. Returns error
/// selector if the string is not a valid fixed point decimal string. Fails
/// on overflow and precision loss, as well as invalid characters in any
/// position. DOES NOT support scientific notation.
/// position. A decimal point MUST be followed by at least one digit; an
/// empty fraction such as "1." is invalid. DOES NOT support scientific
/// notation.
/// Caller MUST check the error selector is 0 before using the value.
/// @param str The string to convert.
/// @return errorSelector 0 if successful, otherwise the error selector.
Expand Down Expand Up @@ -49,6 +51,12 @@ library LibFixedPointDecimalParse {
uint256 fracStart = cursor;
cursor = LibParseChar.skipMask(cursor, end, CMASK_NUMERIC_0_9);

// The decimal point MUST be followed by at least one digit.
// An empty fraction is malformed.
if (cursor == fracStart) {
return (ParseDecimalInvalidString.selector, 0);
}

// Ensure there's no unprocessed garbage.
if (cursor < end) {
return (ParseDecimalInvalidString.selector, 0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,88 @@ contract LibFixedPointDecimalParseTest is Test {
ParseDecimalOverflow.selector
);
}

/// Failure because the FRACTIONAL part itself overflows a `uint256` while
/// being parsed by `unsafeDecimalStringToInt`, before the digit-count
/// precision-loss check is reached. A frac of 78 nines cannot fit in a
/// `uint256` so the inner integer parse returns `ParseDecimalOverflow`, and
/// that selector is propagated unchanged from the fractional-parse error
/// branch.
function testDecimalStringToFixedPointFailureFractionalPartOverflow() external pure {
// 78 nines after the point. 1e78 > type(uint256).max so the inner
// unsafeDecimalStringToInt overflows.
checkDecimalStringToFixedPointFailure(
"0.999999999999999999999999999999999999999999999999999999999999999999999999999999",
ParseDecimalOverflow.selector
);
// 79 digits, leading non-zero, also overflows the inner integer parse.
checkDecimalStringToFixedPointFailure(
"0.9000000000000000000000000000000000000000000000000000000000000000000000000000009",
ParseDecimalOverflow.selector
);
}

/// A character immediately after the integer part that is NOT a decimal
/// point must be rejected as an invalid string by the decimal-point gate.
/// Critically, `"1x"` has no further input after the bad character, so if
/// the decimal-point gate is bypassed the parse wrongly SUCCEEDS as `1e18`
/// rather than erroring; this is what distinguishes the decimal-point gate
/// from the downstream trailing-garbage gate (which the pre-existing corrupt
/// integer tests already reach via inputs whose garbage is caught later).
function testDecimalStringToFixedPointInvalidAfterInteger() external pure {
// Non-point character directly after the integer, with nothing after it,
// so the decimal-point gate is the only thing that can reject it.
checkDecimalStringToFixedPointFailure("1x", ParseDecimalInvalidString.selector);
checkDecimalStringToFixedPointFailure("10x", ParseDecimalInvalidString.selector);
// A bare trailing decimal point with no fractional digits is a
// malformed empty fraction and is rejected.
checkDecimalStringToFixedPointFailure("1.", ParseDecimalInvalidString.selector);
}

/// An empty fraction is invalid. The decimal point MUST be followed by at
/// least one digit, so every integer prefix with a bare trailing point is
/// rejected as an invalid string.
function testDecimalStringToFixedPointEmptyFraction() external pure {
checkDecimalStringToFixedPointFailure("0.", ParseDecimalInvalidString.selector);
checkDecimalStringToFixedPointFailure("1.", ParseDecimalInvalidString.selector);
checkDecimalStringToFixedPointFailure("123.", ParseDecimalInvalidString.selector);
checkDecimalStringToFixedPointFailure("00.", ParseDecimalInvalidString.selector);
checkDecimalStringToFixedPointFailure(
"115792089237316195423570985008687907853269984665640564039457.", ParseDecimalInvalidString.selector
);
// Scientific notation is unsupported, so nothing following the bare
// point can make the empty fraction parseable.
checkDecimalStringToFixedPointFailure("1.e5", ParseDecimalInvalidString.selector);
// A lone point also has an empty integer part, which is rejected
// before the fraction is considered.
checkDecimalStringToFixedPointFailure(".", ParseEmptyDecimalString.selector);
// A point-led fraction has an empty integer part, which is rejected
// before the fraction is considered.
checkDecimalStringToFixedPointFailure(".5", ParseEmptyDecimalString.selector);
}

/// Any integer with a bare trailing decimal point appended is an invalid
/// string. The integer is bounded so it cannot overflow the fixed point
/// representation, leaving the empty fraction as the only rejection.
function testDecimalStringToFixedPointEmptyFractionFuzz(uint256 value) external pure {
value = bound(value, 0, type(uint256).max / 1e18);
checkDecimalStringToFixedPointFailure(
string.concat(vm.toString(value), "."), ParseDecimalInvalidString.selector
);
}

/// The valid forms adjacent to the rejected empty fractions parse to the
/// values given by decimal semantics: the same digits with an explicit
/// fractional digit, and the bare integer without the point.
function testDecimalStringToFixedPointEmptyFractionAdjacentValid() external pure {
checkDecimalStringToFixedPoint("0", 0);
checkDecimalStringToFixedPoint("0.0", 0);
checkDecimalStringToFixedPoint("1", 1e18);
checkDecimalStringToFixedPoint("1.0", 1e18);
checkDecimalStringToFixedPoint("123", 123e18);
checkDecimalStringToFixedPoint("123.0", 123e18);
checkDecimalStringToFixedPoint(
"115792089237316195423570985008687907853269984665640564039457.584007913129639935", type(uint256).max
);
}
}
Loading