Conversation
|
@gsdali , Macros like if (std::abs(aUi - aUU2) <= theEPSILON
|| (aUU2 - aUi < aDelta && theC.Value(aUi).Distance(aPEnd) <= theTol3d))Normally, OCCT tries to avoid an overhead of calling
|
| @@ -130,6 +130,14 @@ void GCPnts_QuasiUniformAbscissa::initialize(const TheCurve& theC, | |||
| Standard_ConstructionError_Raise_if( | |||
| theNbPoints <= 1, | |||
| "GCPnts_QuasiUniformAbscissa::Initialize(), number of points should be >= 2"); | |||
There was a problem hiding this comment.
conflict with the previous checks of Standard_ConstructionError_Raise_if
| aUi = anAbscissaFinder.Parameter(); | ||
| if (std::abs(aUi - aUU2) <= theEPSILON) | ||
| // theEPSILON is a parametric tolerance obtained from theTol3d through Resolution(), | ||
| // which converts using the curve's largest derivative; where the local derivative is |
There was a problem hiding this comment.
Too much text which is unreadable without PR context, please concentrate the description in shorter form
| myDone = false; | ||
| if (theNbPoints <= 1) | ||
| { | ||
| // The check above is compiled out in a build defining No_Exception, and the abscissa below |
There was a problem hiding this comment.
Too much text which is unreadable without PR context, please concentrate the description in shorter form
GCPnts_UniformAbscissa::NbPoints() is not bounded by the requested count. initialize() sizes myParams at theNbPoints + 5 and the walk fills it until it reaches the end parameter or runs out of room, so a caller that sizes its own buffer from the requested count rather than from NbPoints() is handed more points than it asked for. GCPnts_QuasiUniformAbscissa inherits this for every curve that is neither Bezier nor BSpline, since it forwards to GCPnts_UniformAbscissa for those, so the same class returns exactly theNbPoints on a Bezier and possibly more on an ellipse. The cause is a tolerance mismatch. Perform() terminates on abs(aUi - aUU2) <= theEPSILON, where theEPSILON is theC.Resolution(theTol): a parametric tolerance derived from a 3D one using the curve's largest derivative. On an ellipse with major radius 1e6 and minor radius 1e-3 that is about 1e-13, while the local derivative at the end of that curve is 1e-3, so the parametric tolerance actually corresponding to 1e-7 in 3D there is about 1e-4. The walk stops 1.557e-08 short of the end, does not treat that as done, takes one more step and snaps it to the end. The appended point is a duplicate: 1.175e-10 from its neighbour in 3D. On that curve, 22 of the counts from 2 to 60 return one point more than requested. - Perform() also accepts a point that coincides with the end within the caller's 3D tolerance, not only one that is close in parameter. The tolerance is squared once, outside the loop, and compared against SquareDistance() rather than calling Distance() on every iteration; the parameter is named theTol rather than theTol3d, since Perform() also instantiates on Adaptor2d_Curve2d. The end point is evaluated once before the walk, and the distance test is gated behind aUU2 - aUi < aDelta so it runs on the final step rather than on every step. Clamping myNbPoints to theNbPoints was the other option and is worse: the surplus point is the one carrying the exact end parameter, so clamping leaves the distribution stopping short of the curve. Separately, a point count below 2 stores out of bounds. Both classes document theNbPoints >= 2 and previously enforced it with Standard_ConstructionError_Raise_if, which compiles to nothing when No_Exception is defined, as it is for Release builds with BUILD_RELEASE_DISABLE_EXCEPTIONS. GCPnts_QuasiUniformAbscissa::initialize() then allocates NCollection_HArray1<double>(1, theNbPoints), an empty range for such a count, and the next statement is an unconditional myParams->SetValue(1, theU1). SetValue()'s own bounds check is a Raise_if too, so the store lands out of bounds. Reproduced on a 4-pole Bezier and an 8-point BSpline fit with theNbPoints = 0 and with a negative count. - Both classes now leave the object not done for a count below 2 by construction, not by a macro that a build configuration can compile away: the ordinary if replaces Standard_ConstructionError_Raise_if rather than duplicating its condition alongside it, so the result is the same whether or not No_Exception is defined. GCPnts_UniformAbscissa had the same missing precondition without the out-of-bounds store, answering a request for zero points with five, and is guarded the same way. No public API signature changes. Measured across 17 curve types (line, circles of radius 1e-6 to 1e7, two ellipses, hyperbola, parabola, two Beziers, two BSplines, two offsets, a trimmed circle) and counts 2 to 200 for both classes, 6766 configurations: 232 results change from the unpatched baseline and they are exactly the 232 that were returning more points than requested. Every other result is identical parameter for parameter, and on the changed ones the last parameter is still exactly the end. Re-measured after the count-below-2 guard stopped duplicating Standard_ConstructionError_Raise_if: identical 232/6766, and a degenerate count now answers IsDone() == false in every build rather than only when No_Exception is defined.
ed05c66 to
62c0ff0
Compare
Description
Two defects in the
GCPntsarc-length samplers, both about the requested point count.NbPoints()is not bounded by the requested countGCPnts_UniformAbscissa::initializesizesmyParamsattheNbPoints + 5and the walk fills ituntil it reaches the end parameter or runs out of room, setting
myNbPointsto whatever it reached.A caller that sizes its own buffer from the requested count rather than from
NbPoints()is handedmore points than it asked for.
GCPnts_QuasiUniformAbscissainherits this for every curve that isneither Bezier nor BSpline, since it forwards to
GCPnts_UniformAbscissafor those, so the sameclass returns exactly
theNbPointson a Bezier and possibly more on an ellipse.The cause is a tolerance mismatch rather than an off-by-one.
Performterminates onif (std::abs(aUi - aUU2) <= theEPSILON)where
theEPSILONistheC.Resolution(theTol), a parametric tolerance derived from a 3D one usingthe curve's largest derivative. On an ellipse with major radius 1e6 and minor radius 1e-3 that is
about 1e-13, while the local derivative at the end of that curve is 1e-3, so the parametric tolerance
that actually corresponds to 1e-7 in 3D there is about 1e-4. The test is around nine orders of
magnitude too tight at that end of the curve. The walk stops 1.557e-08 short, does not treat that as
done, takes one more step and snaps it to the end.
The appended point is a duplicate: 1.175e-10 from its neighbour in 3D. On that curve, 22 of the
counts from 2 to 60 return one point more than requested, for both classes.
Performnow also accepts a point that coincides with the end within the caller's 3D tolerance, notonly one close in parameter. The tolerance is passed in alongside the parametric one, the end point
is evaluated once before the walk, and the distance test is gated behind
aUU2 - aUi < aDeltaso itruns on the final step rather than on every step.
Clamping
myNbPointstotheNbPointswas the other option and is worse: the surplus point is theone carrying the exact end parameter, so clamping leaves the distribution stopping short of the
curve.
A point count below 2 stores out of bounds
Both classes document
theNbPoints >= 2and enforce it withStandard_ConstructionError_Raise_if,which compiles to nothing when
No_Exceptionis defined, as it is for Release builds withBUILD_RELEASE_DISABLE_EXCEPTIONS(the default).GCPnts_QuasiUniformAbscissa::initializethenallocates
NCollection_HArray1<double>(1, theNbPoints), an empty range for such a count, and thenext statement is an unconditional
myParams->SetValue(1, theU1).SetValue's own bounds check is aRaise_iftoo, so the store lands out of bounds.Reproduced with
theNbPoints = 0and with a negative count, on a 4-pole Bezier and an 8-pointBSpline fit.
GCPnts_UniformAbscissahas the same missing precondition without the out-of-boundsstore: it answers a request for zero points with five.
Both classes now leave the object not done for a count below 2. The
Raise_ifcalls stay, so a buildwith exceptions enabled throws exactly as before; this only stops the undefined behaviour in builds
where the check is compiled out.
No public API signature changes.
Type of change
How Has This Been Tested?
A harness fingerprints both classes across 17 curve types (a line, circles of radius 1e-6 to 1e7, a
1e6 x 1e-3 ellipse, a 5 x 2 ellipse, a hyperbola, a parabola, a 2-pole and a 4-pole Bezier, an
8-point and a 40-point BSpline, an offset circle, an offset BSpline, a trimmed circle, and a
half-period slice of the pathological ellipse) and counts 2 to 200, recording the point count, the
first and last parameter and a digest of the whole parameter list, so a stock run and a patched run
can be diffed result by result.
returning more points than requested. Every other result is identical parameter for parameter,
and on the changed ones the last parameter is still exactly the end.
IsDone() == falsefor both classes,in place of an out-of-bounds store, a five-point answer or a one-point answer depending on which
curve and which class was called.
clang-formatclean on both touched files.Also built into a downstream project's kernel and run against its full suite (4842 tests): clean.
Note for anyone reproducing the second defect: build with
-DNo_Exception(or a Release build withBUILD_RELEASE_DISABLE_EXCEPTIONS=ON). Without it theRaise_ifis live and throws, hiding theout-of-bounds store that a shipped Release build performs.
Reproducers and the full write-up:
https://github.com/SecondMouseAU/OCCTSwift/tree/main/Scripts/repro/555-gcpnts-count-contract
Checklist:
clang-formatclean on every touched file)