diff --git a/NEWS.md b/NEWS.md
index 2f38cf6..e4e5a8b 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,3 +1,23 @@
+# monolix2rx 0.0.7
+
+* Fixed implicit `ptrdiff_t` to `int` truncation in `rc_dup_str` (`src/shared.c`);
+ pointer differences are now range-checked before conversion to `int`.
+
+* Fixed potential integer overflow in all 13 `trans_*` parser entry-points: the
+ `strlen(gBuf)` result is now checked against `INT_MAX` before being cast to
+ `int` for the `dparse()` call.
+
+* Fixed signed integer overflow in `sbuf` size arithmetic (`src/sbuf.c`):
+ `sAppendN`, `sAppend`, and `addLine` now guard against overflow before
+ computing reallocation sizes.
+
+* Fixed `int col` overflow in `getLine` (`src/parseSyntaxErrors.h`): the column
+ accumulator is now `size_t` with an explicit bounds check before use.
+
+* Added thread-safety comment to `src/shared.c` documenting that the global
+ parser state is intentionally not mutex-protected, consistent with R's
+ single-threaded execution model.
+
# monolix2rx 0.0.6
* Updated to add types for rstudio completion
diff --git a/src/dataSettings.c b/src/dataSettings.c
index d53b02a..cacb574 100644
--- a/src/dataSettings.c
+++ b/src/dataSettings.c
@@ -132,7 +132,13 @@ void trans_data_settings(const char* parse){
errP = curP;
eBufLast = 0;
gBufFree=0;
- _pn= dparse(curP, gBuf, (int)strlen(gBuf));
+ {
+ size_t _gBuf_len = strlen(gBuf);
+ if (_gBuf_len > (size_t)INT_MAX) {
+ Rf_error(_("input too large to parse (exceeds INT_MAX bytes)"));
+ }
+ _pn= dparse(curP, gBuf, (int)_gBuf_len);
+ }
if (!_pn || curP->syntax_errors) {
} else {
wprint_parsetree_data_settings(parser_tables_dataSettings , _pn, 0, wprint_node_data_settings, NULL);
diff --git a/src/equation.c b/src/equation.c
index c4e811c..d549f93 100644
--- a/src/equation.c
+++ b/src/equation.c
@@ -526,7 +526,13 @@ void trans_equation(const char* parse){
errP = curP;
eBufLast = 0;
gBufFree=0;
- _pn= dparse(curP, gBuf, (int)strlen(gBuf));
+ {
+ size_t _gBuf_len = strlen(gBuf);
+ if (_gBuf_len > (size_t)INT_MAX) {
+ Rf_error(_("input too large to parse (exceeds INT_MAX bytes)"));
+ }
+ _pn= dparse(curP, gBuf, (int)_gBuf_len);
+ }
if (!_pn || curP->syntax_errors) {
} else {
wprint_parsetree_equation(parser_tables_equation , _pn, 0, wprint_node_equation, NULL);
diff --git a/src/longDef.c b/src/longDef.c
index d792fa7..bacdf73 100644
--- a/src/longDef.c
+++ b/src/longDef.c
@@ -549,7 +549,13 @@ void trans_longdef(const char* parse){
errP = curP;
eBufLast = 0;
gBufFree=0;
- _pn= dparse(curP, gBuf, (int)strlen(gBuf));
+ {
+ size_t _gBuf_len = strlen(gBuf);
+ if (_gBuf_len > (size_t)INT_MAX) {
+ Rf_error(_("input too large to parse (exceeds INT_MAX bytes)"));
+ }
+ _pn= dparse(curP, gBuf, (int)_gBuf_len);
+ }
if (!_pn || curP->syntax_errors) {
} else {
wprint_parsetree_longdef(parser_tables_longDef , _pn, 0, wprint_node_longdef, NULL);
diff --git a/src/longOutput.c b/src/longOutput.c
index 412fdc0..ba991e0 100644
--- a/src/longOutput.c
+++ b/src/longOutput.c
@@ -118,7 +118,13 @@ void trans_longoutput(const char* parse){
errP = curP;
eBufLast = 0;
gBufFree=0;
- _pn= dparse(curP, gBuf, (int)strlen(gBuf));
+ {
+ size_t _gBuf_len = strlen(gBuf);
+ if (_gBuf_len > (size_t)INT_MAX) {
+ Rf_error(_("input too large to parse (exceeds INT_MAX bytes)"));
+ }
+ _pn= dparse(curP, gBuf, (int)_gBuf_len);
+ }
if (!_pn || curP->syntax_errors) {
} else {
wprint_parsetree_longoutput(parser_tables_longOutput , _pn, 0, wprint_node_longoutput, NULL);
diff --git a/src/mlxtranContent.c b/src/mlxtranContent.c
index 25a4f41..7c1d02c 100644
--- a/src/mlxtranContent.c
+++ b/src/mlxtranContent.c
@@ -369,7 +369,13 @@ void trans_content(const char* parse){
errP = curP;
eBufLast = 0;
gBufFree=0;
- _pn= dparse(curP, gBuf, (int)strlen(gBuf));
+ {
+ size_t _gBuf_len = strlen(gBuf);
+ if (_gBuf_len > (size_t)INT_MAX) {
+ Rf_error(_("input too large to parse (exceeds INT_MAX bytes)"));
+ }
+ _pn= dparse(curP, gBuf, (int)_gBuf_len);
+ }
if (!_pn || curP->syntax_errors) {
} else {
wprint_parsetree_content(parser_tables_mlxtranContent , _pn, 0, wprint_node_content, NULL);
diff --git a/src/mlxtranFileinfo.c b/src/mlxtranFileinfo.c
index d3fe67e..a459819 100644
--- a/src/mlxtranFileinfo.c
+++ b/src/mlxtranFileinfo.c
@@ -123,7 +123,13 @@ void trans_fileinfo(const char* parse){
errP = curP;
eBufLast = 0;
gBufFree=0;
- _pn= dparse(curP, gBuf, (int)strlen(gBuf));
+ {
+ size_t _gBuf_len = strlen(gBuf);
+ if (_gBuf_len > (size_t)INT_MAX) {
+ Rf_error(_("input too large to parse (exceeds INT_MAX bytes)"));
+ }
+ _pn= dparse(curP, gBuf, (int)_gBuf_len);
+ }
if (!_pn || curP->syntax_errors) {
} else {
wprint_parsetree_fileinfo(parser_tables_mlxtranFileinfo , _pn, 0, wprint_node_fileinfo, NULL);
diff --git a/src/mlxtranFit.c b/src/mlxtranFit.c
index 4c53be0..e315043 100644
--- a/src/mlxtranFit.c
+++ b/src/mlxtranFit.c
@@ -141,7 +141,13 @@ void trans_fit(const char* parse){
errP = curP;
eBufLast = 0;
gBufFree=0;
- _pn= dparse(curP, gBuf, (int)strlen(gBuf));
+ {
+ size_t _gBuf_len = strlen(gBuf);
+ if (_gBuf_len > (size_t)INT_MAX) {
+ Rf_error(_("input too large to parse (exceeds INT_MAX bytes)"));
+ }
+ _pn= dparse(curP, gBuf, (int)_gBuf_len);
+ }
if (!_pn || curP->syntax_errors) {
} else {
wprint_parsetree_fit(parser_tables_mlxtranFit , _pn, 0, wprint_node_fit, NULL);
diff --git a/src/mlxtranInd.c b/src/mlxtranInd.c
index 5cf9729..c450539 100644
--- a/src/mlxtranInd.c
+++ b/src/mlxtranInd.c
@@ -168,7 +168,13 @@ void trans_individual(const char* parse){
errP = curP;
eBufLast = 0;
gBufFree=0;
- _pn= dparse(curP, gBuf, (int)strlen(gBuf));
+ {
+ size_t _gBuf_len = strlen(gBuf);
+ if (_gBuf_len > (size_t)INT_MAX) {
+ Rf_error(_("input too large to parse (exceeds INT_MAX bytes)"));
+ }
+ _pn= dparse(curP, gBuf, (int)_gBuf_len);
+ }
if (!_pn || curP->syntax_errors) {
} else {
wprint_parsetree_individual(parser_tables_mlxtranInd , _pn, 0, wprint_node_individual, NULL);
diff --git a/src/mlxtranIndDefinition.c b/src/mlxtranIndDefinition.c
index 71f86a5..42611c8 100644
--- a/src/mlxtranIndDefinition.c
+++ b/src/mlxtranIndDefinition.c
@@ -280,7 +280,13 @@ void trans_indDef(const char* parse){
errP = curP;
eBufLast = 0;
gBufFree=0;
- _pn= dparse(curP, gBuf, (int)strlen(gBuf));
+ {
+ size_t _gBuf_len = strlen(gBuf);
+ if (_gBuf_len > (size_t)INT_MAX) {
+ Rf_error(_("input too large to parse (exceeds INT_MAX bytes)"));
+ }
+ _pn= dparse(curP, gBuf, (int)_gBuf_len);
+ }
if (!_pn || curP->syntax_errors) {
} else {
wprint_parsetree_indDef(parser_tables_mlxtranIndDefinition , _pn, 0, wprint_node_indDef, NULL);
diff --git a/src/mlxtranOp.c b/src/mlxtranOp.c
index 4dfb785..7b7ea3e 100644
--- a/src/mlxtranOp.c
+++ b/src/mlxtranOp.c
@@ -183,7 +183,13 @@ void trans_mlxtran_op(const char* parse){
errP = curP;
eBufLast = 0;
gBufFree=0;
- _pn= dparse(curP, gBuf, (int)strlen(gBuf));
+ {
+ size_t _gBuf_len = strlen(gBuf);
+ if (_gBuf_len > (size_t)INT_MAX) {
+ Rf_error(_("input too large to parse (exceeds INT_MAX bytes)"));
+ }
+ _pn= dparse(curP, gBuf, (int)_gBuf_len);
+ }
if (!_pn || curP->syntax_errors) {
} else {
wprint_parsetree_mlxtran_op(parser_tables_mlxtranOp , _pn, 0, wprint_node_mlxtran_op, NULL);
diff --git a/src/mlxtranParameter.c b/src/mlxtranParameter.c
index be2f09d..89c59ef 100644
--- a/src/mlxtranParameter.c
+++ b/src/mlxtranParameter.c
@@ -140,7 +140,13 @@ void trans_parameter(const char* parse){
errP = curP;
eBufLast = 0;
gBufFree=0;
- _pn= dparse(curP, gBuf, (int)strlen(gBuf));
+ {
+ size_t _gBuf_len = strlen(gBuf);
+ if (_gBuf_len > (size_t)INT_MAX) {
+ Rf_error(_("input too large to parse (exceeds INT_MAX bytes)"));
+ }
+ _pn= dparse(curP, gBuf, (int)_gBuf_len);
+ }
if (!_pn || curP->syntax_errors) {
} else {
wprint_parsetree_parameter(parser_tables_mlxtranParameter , _pn, 0, wprint_node_parameter, NULL);
diff --git a/src/mlxtranTask.c b/src/mlxtranTask.c
index 1cdad77..87fad0c 100644
--- a/src/mlxtranTask.c
+++ b/src/mlxtranTask.c
@@ -142,7 +142,13 @@ void trans_mlxtrantask(const char* parse){
errP = curP;
eBufLast = 0;
gBufFree=0;
- _pn= dparse(curP, gBuf, (int)strlen(gBuf));
+ {
+ size_t _gBuf_len = strlen(gBuf);
+ if (_gBuf_len > (size_t)INT_MAX) {
+ Rf_error(_("input too large to parse (exceeds INT_MAX bytes)"));
+ }
+ _pn= dparse(curP, gBuf, (int)_gBuf_len);
+ }
if (!_pn || curP->syntax_errors) {
} else {
wprint_parsetree_mlxtrantask(parser_tables_mlxtranTask , _pn, 0, wprint_node_mlxtrantask, NULL);
diff --git a/src/parseSyntaxErrors.h b/src/parseSyntaxErrors.h
index acd4594..a4b7926 100644
--- a/src/parseSyntaxErrors.h
+++ b/src/parseSyntaxErrors.h
@@ -21,13 +21,21 @@
#define syntaxErrorExtra monolix2rx_syntaxErrorExtra
static inline char *getLine (char *src, int line, int *lloc) {
- int cur = 1, col=0, i;
+ int cur = 1, i;
+ size_t col = 0;
for(i = 0; src[i] != '\0' && cur != line; i++){
if(src[i] == '\n') cur++;
}
- for(col = 0; src[i + col] != '\n' && src[i + col] != '\0'; col++);
- *lloc=i+col;
- char *buf = R_Calloc(col + 1, char);
+ for(col = 0; src[i + col] != '\n' && src[i + col] != '\0'; col++){
+ if (col == (size_t)INT_MAX) {
+ Rf_error(_("line too long in getLine"));
+ }
+ }
+ if ((size_t)i + col > (size_t)INT_MAX) {
+ Rf_error(_("source offset overflow in getLine"));
+ }
+ *lloc = i + (int)col;
+ char *buf = R_Calloc((int)col + 1, char);
memcpy(buf, src + i, col);
buf[col] = '\0';
return buf;
diff --git a/src/sbuf.c b/src/sbuf.c
index 9eba1be..c3c57ab 100644
--- a/src/sbuf.c
+++ b/src/sbuf.c
@@ -27,6 +27,10 @@ void sFreeIni(sbuf *sbb) {
void sAppendN(sbuf *sbb, const char *what, int n) {
if (sbb->sN == 0) sIni(sbb);
+ if (n < 0) Rf_error(_("invalid negative length in sAppendN"));
+ if (sbb->o > INT_MAX - 2 - n - SBUF_MXBUF) {
+ Rf_error(_("buffer size overflow in sAppendN"));
+ }
if (sbb->sN <= 2 + n + sbb->o){
int mx = sbb->o + 2 + n + SBUF_MXBUF;
sbb->s = R_Realloc(sbb->s, mx, char);
@@ -51,6 +55,9 @@ void sAppend(sbuf *sbb, const char *format, ...) {
n = vsnprintf(zero, 0, format, copy) + 1;
#endif
va_end(copy);
+ if (n > 0 && sbb->o > INT_MAX - n - 1 - SBUF_MXBUF) {
+ Rf_error(_("buffer size overflow in sAppend"));
+ }
if (sbb->sN <= sbb->o + n + 1) {
int mx = sbb->o + n + 1 + SBUF_MXBUF;
sbb->s = R_Realloc(sbb->s, mx, char);
@@ -109,6 +116,9 @@ void addLine(vLines *sbb, const char *format, ...) {
Rf_errorcall(R_NilValue, _("encoding error in 'addLine' format: '%s' n: %d; errno: %d"), format, n, errno);
}
va_end(copy);
+ if (sbb->sN > INT_MAX - n - 2 - SBUF_MXBUF) {
+ Rf_error(_("buffer size overflow in addLine (string buffer)"));
+ }
if (sbb->sN <= sbb->o + n){
int mx = sbb->sN + n + 2 + SBUF_MXBUF;
sbb->s = R_Realloc(sbb->s, mx, char);
@@ -121,6 +131,9 @@ void addLine(vLines *sbb, const char *format, ...) {
}
vsnprintf(sbb->s + sbb->o, sbb->sN - sbb->o, format, argptr);
va_end(argptr);
+ if (sbb->nL > INT_MAX - n - 2 - SBUF_MXLINE) {
+ Rf_error(_("line array size overflow in addLine"));
+ }
if (sbb->n + 2 >= sbb->nL){
int mx = sbb->nL + n + 2 + SBUF_MXLINE;
sbb->lProp = R_Realloc(sbb->lProp, mx, int);
diff --git a/src/shared.c b/src/shared.c
index d7e48d1..b71a9dc 100644
--- a/src/shared.c
+++ b/src/shared.c
@@ -19,6 +19,10 @@ dparserPtrIni
#include "parseSyntaxErrors.h"
// These are the shared variables
+// NOTE: These globals are intentionally not mutex-protected.
+// R's interpreter is single-threaded; its memory allocator (R_Calloc, R_Free)
+// and error handling (Rf_error) are not safe to call from multiple threads.
+// All parse operations must occur on the R main thread.
const char *record;
int _rxode2_reallyHasAfter = 0;
@@ -41,8 +45,20 @@ int lastStrLoc=0;
vLines _dupStrs;
char * rc_dup_str(const char *s, const char *e) {
lastStr=s;
- int l = e ? e-s : (int)strlen(s);
- //syntaxErrorExtra=min(l-1, 40);
+ int l;
+ if (e) {
+ ptrdiff_t diff = e - s;
+ if (diff < 0 || diff > (ptrdiff_t)INT_MAX) {
+ Rf_error(_("string segment too long in rc_dup_str"));
+ }
+ l = (int)diff;
+ } else {
+ size_t slen = strlen(s);
+ if (slen > (size_t)INT_MAX) {
+ Rf_error(_("string too long in rc_dup_str"));
+ }
+ l = (int)slen;
+ }
addLine(&_dupStrs, "%.*s", l, s);
return _dupStrs.line[_dupStrs.n-1];
}
diff --git a/src/summaryData.c b/src/summaryData.c
index 75b563e..88df4c0 100644
--- a/src/summaryData.c
+++ b/src/summaryData.c
@@ -131,7 +131,13 @@ void trans_summaryData(const char* parse){
errP = curP;
eBufLast = 0;
gBufFree=0;
- _pn= dparse(curP, gBuf, (int)strlen(gBuf));
+ {
+ size_t _gBuf_len = strlen(gBuf);
+ if (_gBuf_len > (size_t)INT_MAX) {
+ Rf_error(_("input too large to parse (exceeds INT_MAX bytes)"));
+ }
+ _pn= dparse(curP, gBuf, (int)_gBuf_len);
+ }
if (!_pn || curP->syntax_errors) {
} else {
wprint_parsetree_summaryData(parser_tables_summaryData , _pn, 0, wprint_node_summaryData, NULL);
diff --git a/tests/testthat/_snaps/content.md b/tests/testthat/_snaps/content.md
deleted file mode 100644
index 994910b..0000000
--- a/tests/testthat/_snaps/content.md
+++ /dev/null
@@ -1,19 +0,0 @@
-# content
-
- Code
- print(tmp)
- Output
- ID = {use=identifier}
- TIME = {use=time}
- EVID = {use=eventidentifier}
- AMT = {use=amount}
- YTYPE = {use=observationtype}
- ADM = {use=administration}
- DV = {use=observation, name={y1, y2}, yname={'1', '2'}, type={continuous, continuous}}
- E0 = {use = regressor}
- Emax = {use = regressor}
- WT = {use=covariate, type=continuous}
- CRCL = {use=covariate, type=continuous}
- Race = {type=categorical, categories={Caucasian, Black, Latin}}
- Sex = {type=categorical, categories={M, F}}
-
diff --git a/tests/testthat/_snaps/data-import/single-endpoint-theo-p1.svg b/tests/testthat/_snaps/data-import/single-endpoint-theo-p1.svg
deleted file mode 100644
index 4307d49..0000000
--- a/tests/testthat/_snaps/data-import/single-endpoint-theo-p1.svg
+++ /dev/null
@@ -1,639 +0,0 @@
-
-
diff --git a/tests/testthat/_snaps/data-import/single-endpoint-theo.svg b/tests/testthat/_snaps/data-import/single-endpoint-theo.svg
deleted file mode 100644
index 90de8dc..0000000
--- a/tests/testthat/_snaps/data-import/single-endpoint-theo.svg
+++ /dev/null
@@ -1,532 +0,0 @@
-
-
diff --git a/tests/testthat/_snaps/dataSettings.md b/tests/testthat/_snaps/dataSettings.md
deleted file mode 100644
index 51fd9a4..0000000
--- a/tests/testthat/_snaps/dataSettings.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# [SETTINGS]
-
- Code
- print(tmp2)
- Output
- dataType = {'dv'=plasma}
-
diff --git a/tests/testthat/_snaps/fileinfo.md b/tests/testthat/_snaps/fileinfo.md
deleted file mode 100644
index 787ea2e..0000000
--- a/tests/testthat/_snaps/fileinfo.md
+++ /dev/null
@@ -1,9 +0,0 @@
-# fileinfo
-
- Code
- print(.fi)
- Output
- file = 'pk.turnover.emax3-monolix.csv'
- delimiter = comma
- header = {ID, TIME, EVID, AMT, DV, ADM, YTYPE, nlmixrRowNums}
-
diff --git a/tests/testthat/_snaps/fit.md b/tests/testthat/_snaps/fit.md
deleted file mode 100644
index d8eee0a..0000000
--- a/tests/testthat/_snaps/fit.md
+++ /dev/null
@@ -1,8 +0,0 @@
-# fit parsing
-
- Code
- print(tmp)
- Output
- data = {y1, y2}
- model = {rx_prd_cp, rx_prd_effect}
-
diff --git a/tests/testthat/_snaps/ind-def.md b/tests/testthat/_snaps/ind-def.md
deleted file mode 100644
index 2622d67..0000000
--- a/tests/testthat/_snaps/ind-def.md
+++ /dev/null
@@ -1,12 +0,0 @@
-# standard individual definition
-
- Code
- print(tmp)
- Output
- F = {distribution=logitnormal, typical=F_pop, sd=omega_F, min=0, max=1}
- ka = {distribution=lognormal, typical=ka_pop, no-variability}
- V = {distribution=lognormal, typical=V_pop, sd=omega_V}
- Cl = {distribution=lognormal, typical=Cl_pop, sd=omega_Cl}
- correlation = {r(Cl, V)=corr1_V_Cl}
-
-
diff --git a/tests/testthat/_snaps/ind.md b/tests/testthat/_snaps/ind.md
deleted file mode 100644
index e17c573..0000000
--- a/tests/testthat/_snaps/ind.md
+++ /dev/null
@@ -1,29 +0,0 @@
-# input categorical covariates and regressors
-
- Code
- print(.tmp)
- Output
- input = {V_pop, omega_V, ka_pop, omega_ka, Cl_pop, omega_Cl, logtAge, Race, Sex, logtWeight, beta_Cl_Race_Caucasian, beta_Cl_Race_Latin, beta_Cl_Smoke_yes, beta_Cl_logtAge, beta_V_logtWeight, E0}
- E0 = {use = regressor}
- Race = {type=categorical, categories={Caucasian, Black, Latin}}
- Sex = {type=categorical, categories={M, F}}
-
----
-
- Code
- print(.tmp)
- Output
- input = {AGE, DOSE, SEX}
- DOSE = {type=categorical, categories={'50 mg', '100 mg'}}
- SEX = {type=categorical, categories={Female, Male}}
-
----
-
- Code
- print(.tmp)
- Output
- input = {V_pop, omega_V, ka_pop, omega_ka, Cl_pop, omega_Cl, logtAge, Race, Sex, logtWeight, beta_Cl_Race_Caucasian, beta_Cl_Race_Latin, beta_Cl_Smoke_yes, beta_Cl_logtAge, beta_V_logtWeight, E0}
- E0 = {use = regressor}
- Race = {type=categorical, categories={'Caucasian 1', Black, Latin}}
- Sex = {type=categorical, categories={M, F}}
-
diff --git a/tests/testthat/_snaps/longDef.md b/tests/testthat/_snaps/longDef.md
deleted file mode 100644
index b02ca0d..0000000
--- a/tests/testthat/_snaps/longDef.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# [LONGITUDINAL] DEFINITION:
-
- Code
- print(tmp)
- Output
- Seizure = {type=event, eventType=intervalCensored, maxEventNumber=1, rightCensoringTime=120, intervalLength=10, hazard=haz}
-
diff --git a/tests/testthat/_snaps/longOut.md b/tests/testthat/_snaps/longOut.md
deleted file mode 100644
index ebbf8a7..0000000
--- a/tests/testthat/_snaps/longOut.md
+++ /dev/null
@@ -1,8 +0,0 @@
-# long output
-
- Code
- print(.ret)
- Output
- output = {Conc, Effect}
- table = {Ap, T12}
-
diff --git a/tests/testthat/_snaps/longitudinal.md b/tests/testthat/_snaps/longitudinal.md
deleted file mode 100644
index f311dee..0000000
--- a/tests/testthat/_snaps/longitudinal.md
+++ /dev/null
@@ -1,8 +0,0 @@
-# longitudinal()
-
- Code
- print(f)
- Output
- input = {pkadd__err, prop__err, pdadd__err}
- file = 'pk.turnover.emax3-monolix.txt'
-
diff --git a/tests/testthat/_snaps/mlxtran.md b/tests/testthat/_snaps/mlxtran.md
deleted file mode 100644
index 303982e..0000000
--- a/tests/testthat/_snaps/mlxtran.md
+++ /dev/null
@@ -1,128 +0,0 @@
-# mlxtran initial list
-
- Code
- print(v)
- Output
- DESCRIPTION:
- model translated from `babelmixr2` and `nlmixr2` function pk.turnover.emax3 to pk.turnover.emax3-monolix.txt
-
-
- [FILEINFO]
- ; parsed: $DATAFILE$FILEINFO$FILEINFO
- file = 'pk.turnover.emax3-monolix.csv'
- delimiter = comma
- header = {ID, TIME, EVID, AMT, DV, ADM, YTYPE, nlmixrRowNums}
-
- [CONTENT]
- ; parsed: $DATAFILE$CONTENT$CONTENT
- ID = {use=identifier}
- TIME = {use=time}
- EVID = {use=eventidentifier}
- AMT = {use=amount}
- YTYPE = {use=observationtype}
- ADM = {use=administration}
- DV = {use=observation, name={y1, y2}, yname={'1', '2'}, type={continuous, continuous}}
-
-
- [INDIVIDUAL]
- ; parsed: $MODEL$INDIVIDUAL$INDIVIDUAL
- input = {ktr_pop, omega_ktr, ka_pop, omega_ka, cl_pop, omega_cl, v_pop, omega_v, emax_pop, omega_emax, ec50_pop, omega_ec50, kout_pop, omega_kout, e0_pop, omega_e0}
-
- DEFINITION:
- ; parsed: $MODEL$INDIVIDUAL$DEFINITION
- ktr = {distribution=lognormal, typical=ktr_pop, sd=omega_ktr}
- ka = {distribution=lognormal, typical=ka_pop, sd=omega_ka}
- cl = {distribution=lognormal, typical=cl_pop, sd=omega_cl}
- v = {distribution=lognormal, typical=v_pop, sd=omega_v}
- emax = {distribution=logitnormal, typical=emax_pop, sd=omega_emax, min=0, max=1}
- ec50 = {distribution=lognormal, typical=ec50_pop, sd=omega_ec50}
- kout = {distribution=lognormal, typical=kout_pop, sd=omega_kout}
- e0 = {distribution=lognormal, typical=e0_pop, sd=omega_e0}
-
- [LONGITUDINAL]
- ; parsed: $MODEL$LONGITUDINAL$LONGITUDINAL
- input = {pkadd__err, prop__err, pdadd__err, ktr, ka, cl, v, emax, ec50, kout, e0}
-
- DEFINITION:
- ; parsed: $MODEL$LONGITUDINAL$DEFINITION
- rx_prd_cp = {distribution=normal, prediction=rx_pred_cp, errorModel=combined2(pkadd__err, prop__err)}
- rx_prd_effect = {distribution=normal, prediction=rx_pred_effect, errorModel=constant(pdadd__err)}
-
- PK:
- ; parsed: $MODEL$LONGITUDINAL$PK
- compartment(cmt = 1, amount = depot, volume = 1.0)
-
- depot(adm = 1, target = depot, Tlag = 0, p = 1)
-
- EQUATION:
- ; parsed: $MODEL$LONGITUDINAL$EQUATION
- DCP = center/v
- PD = 1-emax*DCP/(ec50+DCP)
- effect_0 = e0
- kin = e0*kout
- ddt_depot = - ktr*depot
- ddt_gut = ktr*depot-ka*gut
- ddt_center = ka*gut-cl/v*center
- ddt_effect = kin*PD-kout*effect
- cp = center/v
- rx_pred_cp = cp
- rx_pred_effect = effect
-
- OUTPUT:
- ; parsed: $MODEL$LONGITUDINAL$OUTPUT
- output = {rx_pred_cp, rx_pred_effect}
-
-
- ; parsed: $FIT$FIT
- data = {y1, y2}
- model = {rx_prd_cp, rx_prd_effect}
-
-
- ; parsed: $PARAMETER$PARAMETER
- ktr_pop = {value=1, method=MLE}
- ka_pop = {value=1, method=MLE}
- cl_pop = {value=0.1, method=MLE}
- v_pop = {value=10, method=MLE}
- prop__err = {value=0.1, method=MLE}
- pkadd__err = {value=0.1, method=MLE}
- emax_pop = {value=0.8, method=MLE}
- ec50_pop = {value=0.5, method=MLE}
- kout_pop = {value=0.05, method=MLE}
- e0_pop = {value=100, method=MLE}
- pdadd__err = {value=10, method=MLE}
- omega_ktr = {value=1, method=MLE}
- omega_ka = {value=1, method=MLE}
- omega_cl = {value=1.4142135623731, method=MLE}
- omega_v = {value=1, method=MLE}
- omega_emax = {value=0.707106781186548, method=MLE}
- omega_ec50 = {value=0.707106781186548, method=MLE}
- omega_kout = {value=0.707106781186548, method=MLE}
- omega_e0 = {value=0.707106781186548, method=MLE}
-
-
- [TASKS]
- ; parsed: $MONOLIX$TASKS$TASKS
- populationParameters()
- individualParameters(method = {conditionalMode})
- fim(method = Linearization)
- logLikelihood(method = Linearization)
- plotResult(method = {outputplot, indfits, obspred, residualsscatter, residualsdistribution, parameterdistribution, covariatemodeldiagnosis, randomeffects, covariancemodeldiagnosis, saemresults})
-
- [SETTINGS]
- GLOBAL:
- ; parsed: $MONOLIX$SETTINGS$GLOBAL
- exportpath = 'pk.turnover.emax3-monolix'
-
- POPULATION:
- ; parsed: $MONOLIX$SETTINGS$POPULATION
- exploratoryautostop = no
- smoothingautostop = no
- burniniterations = 5
- exploratoryiterations = 250
- simulatedannealingiterations = 250
- smoothingiterations = 200
- exploratoryalpha = 0
- exploratoryinterval = 200
- omegatau = 0.95
- errormodeltau = 0.95
-
diff --git a/tests/testthat/_snaps/mlxtranOp.md b/tests/testthat/_snaps/mlxtranOp.md
deleted file mode 100644
index 4cbd138..0000000
--- a/tests/testthat/_snaps/mlxtranOp.md
+++ /dev/null
@@ -1,17 +0,0 @@
-# mlxtranOp
-
- Code
- print(.tmp)
- Output
- exportpath = 'pk.turnover.emax3-monolix'
- exploratoryautostop = no
- smoothingautostop = no
- burniniterations = 5
- exploratoryiterations = 250
- simulatedannealingiterations = 250
- smoothingiterations = 200
- exploratoryalpha = 0
- exploratoryinterval = 200
- omegatau = 0.95
- errormodeltau = 0.95
-
diff --git a/tests/testthat/_snaps/parameter.md b/tests/testthat/_snaps/parameter.md
deleted file mode 100644
index 05ad0d8..0000000
--- a/tests/testthat/_snaps/parameter.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# parameters
-
- Code
- print(tmp)
- Output
- ktr_pop = {value=1, method=MLE}
- ka_pop = {value=1, method=FIXED}
- cl_pop = {value=0.1, method=MLE}
- v_pop = {value=10, method=MLE}
- prop__err = {value=0.1, method=MLE}
-
diff --git a/tests/testthat/_snaps/task.md b/tests/testthat/_snaps/task.md
deleted file mode 100644
index b4efe94..0000000
--- a/tests/testthat/_snaps/task.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# TASK
-
- Code
- print(.ret)
- Output
- populationParameters()
- individualParameters(method = {conditionalMode})
- fim(method = Linearization)
- logLikelihood(method = Linearization)
- plotResult(method = {outputplot, indfits, obspred, residualsscatter, residualsdistribution, parameterdistribution, covariatemodeldiagnosis, randomeffects, covariancemodeldiagnosis, saemresults}, color = red)
-
diff --git a/tests/testthat/test-memory-safety.R b/tests/testthat/test-memory-safety.R
new file mode 100644
index 0000000..d1dc21b
--- /dev/null
+++ b/tests/testthat/test-memory-safety.R
@@ -0,0 +1,89 @@
+# Tests for integer overflow and memory safety fixes
+#
+# Issues addressed:
+# - rc_dup_str: implicit ptrdiff_t to int truncation (shared.c)
+# - dparse: (int)strlen(gBuf) overflow in all 13 parser entry-points
+# - sbuf: signed integer overflow in size arithmetic (sbuf.c)
+# - getLine: int col overflow in parseSyntaxErrors.h
+#
+# NOTE on the >2GB skipped tests: R's internal CHARSXP type uses a signed
+# 32-bit integer for string length, capping individual R strings at
+# INT_MAX (2,147,483,647) bytes. Because of this, the overflow guards in
+# the C code protect primarily against direct C-level misuse (e.g., calls
+# from C code that bypasses R's string limit). The tests below document
+# the boundary behaviour and require ~2GB of free RAM to run.
+
+test_that("rc_dup_str handles normal strings without error", {
+ # Regression: short strings must work correctly after the overflow guards
+ .ret <- .equation("x_0 = V\nddt_x = -k*x", .pk(""))
+ expect_type(.ret$rx, "character")
+ expect_true(length(.ret$rx) > 0)
+})
+
+test_that("equation parser handles multi-statement input correctly", {
+ # Regression: multi-statement equations still parse cleanly after all guards
+ .ret <- .equation(
+ "x_0 = V\ny_0 = 1\nddt_x = -k*x\nddt_y = k*x - k2*y",
+ .pk("")
+ )
+ expect_type(.ret$rx, "character")
+ expect_true(any(grepl("d/dt", .ret$rx)))
+})
+
+test_that("integer overflow protection: dparse input approaching INT_MAX bytes", {
+ skip(paste(
+ "requires ~2GB free RAM;",
+ "tests the (int)strlen(gBuf) overflow guard before each dparse() call.",
+ "Without the fix the cast silently wraps to a negative value for inputs",
+ "> INT_MAX bytes, causing dparse to crash. R strings are internally capped",
+ "at INT_MAX-1 bytes so the guard fires for C-level misuse; this test",
+ "exercises the largest string R can construct to verify the path is safe.",
+ "NOTE: use strrep() not paste0(rep()) to avoid a large intermediate vector."
+ ))
+ # 6 bytes x 357,913,941 = 2,147,483,646 bytes (INT_MAX - 1): the largest
+ # string strrep can produce before R itself errors on string length.
+ # The overflow guard fires only for > INT_MAX, so this input passes through;
+ # the test confirms no crash or corruption occurs near the boundary.
+ huge_str <- strrep("a = b\n", 357913941L)
+ expect_no_error(
+ .equation(huge_str, .pk(""))
+ )
+})
+
+test_that("integer overflow protection: sbuf size arithmetic near INT_MAX", {
+ skip(paste(
+ "requires ~2GB free RAM;",
+ "tests signed integer overflow guards in sbuf.c sAppend/sAppendN/addLine.",
+ "Without the fix, sbb->o + n wraps to a negative int when the accumulated",
+ "buffer approaches INT_MAX, causing R_Realloc to allocate a tiny buffer and",
+ "corrupt the heap. The guard fires before that arithmetic.",
+ "NOTE: use strrep() not paste0(rep()) to avoid a large intermediate vector."
+ ))
+ # 35 bytes x 51,000,000 = 1,785,000,000 bytes (~1.78GB); feeds the parser
+ # with enough content to accumulate near the sbuf overflow boundary.
+ huge_eq <- strrep("var_a_b_c_d_e_f_g = var_h_i_j_k_l\n", 51000000L)
+ # The equation parser processes all lines; cumulative rc_dup_str calls
+ # grow _dupStrs toward INT_MAX. The guard prevents heap corruption.
+ expect_no_error(
+ .equation(huge_eq, .pk(""))
+ )
+})
+
+test_that("integer overflow protection: getLine col accumulation near INT_MAX", {
+ skip(paste(
+ "requires ~2GB free RAM;",
+ "tests size_t col overflow guard in getLine (parseSyntaxErrors.h).",
+ "Without the fix, int col wraps at INT_MAX+1 and R_Calloc(col+1) receives",
+ "a negative/tiny size, corrupting the heap. The guard fires at col == INT_MAX.",
+ "A syntax error is triggered so getLine is actually called on the long line.",
+ "NOTE: use strrep() not paste0(rep()) to avoid a large intermediate vector."
+ ))
+ # Construct a valid-but-huge LHS with an invalid RHS to force a syntax error,
+ # which causes getLine to be called on the single giant line (~2GB, no newlines).
+ # 200,000,000 x 10 bytes = 2,000,000,000 bytes (~2GB, under INT_MAX).
+ giant_line <- strrep("x_var_abc", 200000000L)
+ expect_error(
+ .equation(paste0(giant_line, " = !!!bad"),
+ .pk(""))
+ )
+})