Keep hostile Lisp, diary sexps and formulas from crashing !140

merged merged by cmc on 2026-10-07 22:23 UTC · krz/orgstar:hostile-inputs into main

5 files changed, +188 −68

Layout: unified · split

Sources/OrgCore/Agenda/DiarySexp.swift +37 −25
@@ -37,10 +37,17 @@ public enum DiarySexp {
3737 return .list([.integer(date.month), .integer(date.day), .integer(date.year)])
3838 }
3939
40 /// A number from Lisp used in date arithmetic, within what that arithmetic handles.
41 static func bounded(_ v: Sexp) throws -> Int {
42 let i = try Elisp.int(v)
43 guard (-100_000_000...100_000_000).contains(i) else { throw Elisp.Unsupported(what: "date number \(i)") }
44 return i
45 }
46
4047 static func parts(_ date: Sexp) throws -> (month: Int, day: Int, year: Int) {
4148 let items = try Elisp.elements(date)
4249 guard items.count == 3 else { throw Elisp.Signal(message: "Bad date \(date.description)") }
43 return (try Elisp.int(items[0]), try Elisp.int(items[1]), try Elisp.int(items[2]))
50 return (try bounded(items[0]), try bounded(items[1]), try bounded(items[2]))
4451 }
4552
4653 static func absolute(_ date: Sexp) throws -> Int {
@@ -50,17 +57,18 @@ public enum DiarySexp {
5057
5158 static func isLeap(_ year: Int) -> Bool { year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) }
5259
53 static func lastDay(month: Int, year: Int) -> Int {
54 month == 2 ? (isLeap(year) ? 29 : 28) : [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][(month - 1 + 12) % 12]
60 static func lastDay(month: Int, year: Int) throws -> Int {
61 guard (1...12).contains(month) else { throw Elisp.Signal(message: "Args out of range: \(month)") }
62 return month == 2 ? (isLeap(year) ? 29 : 28) : [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1]
5563 }
5664
5765 /// `calendar-nth-named-absday`.
58 static func nthNamedAbsday(_ n: Int, _ dayname: Int, _ month: Int, _ year: Int, _ day: Int?) -> Int {
66 static func nthNamedAbsday(_ n: Int, _ dayname: Int, _ month: Int, _ year: Int, _ day: Int?) throws -> Int {
5967 func onOrBefore(_ date: Int) -> Int { date - (date - dayname) % 7 }
6068 if n > 0 {
6169 return 7 * (n - 1) + onOrBefore(6 + Days.absolute(year: year, month: month, day: day ?? 1))
6270 }
63 return 7 * (n + 1) + onOrBefore(Days.absolute(year: year, month: month, day: day ?? lastDay(month: month, year: year)))
71 return 7 * (n + 1) + onOrBefore(Days.absolute(year: year, month: month, day: try day ?? lastDay(month: month, year: year)))
6472 }
6573
6674 /// `diary-ordinal-suffix`.
@@ -102,7 +110,7 @@ public enum DiarySexp {
102110 }
103111 lisp.define("calendar-gregorian-from-absolute") { _, args in
104112 try arity(args, 1...1, "calendar-gregorian-from-absolute")
105 return gregorian(try Elisp.int(args[0]))
113 return gregorian(try bounded(args[0]))
106114 }
107115 lisp.define("calendar-day-of-week") { _, args in
108116 try arity(args, 1...1, "calendar-day-of-week")
@@ -114,7 +122,7 @@ public enum DiarySexp {
114122 }
115123 lisp.define("calendar-last-day-of-month") { _, args in
116124 try arity(args, 2...2, "calendar-last-day-of-month")
117 return .integer(lastDay(month: try Elisp.int(args[0]), year: try Elisp.int(args[1])))
125 return .integer(try lastDay(month: try bounded(args[0]), year: try bounded(args[1])))
118126 }
119127 lisp.define("calendar-date-equal") { _, args in
120128 try arity(args, 2...2, "calendar-date-equal")
@@ -129,15 +137,15 @@ public enum DiarySexp {
129137 }
130138 lisp.define("calendar-nth-named-absday") { _, args in
131139 try arity(args, 4...5, "calendar-nth-named-absday")
132 let day = Elisp.isNil(optional(args, 4)) ? nil : try Elisp.int(args[4])
133 return .integer(nthNamedAbsday(try Elisp.int(args[0]), try Elisp.int(args[1]), try Elisp.int(args[2]), try Elisp.int(args[3]), day))
140 let day = Elisp.isNil(optional(args, 4)) ? nil : try bounded(args[4])
141 return .integer(try nthNamedAbsday(try bounded(args[0]), try bounded(args[1]), try bounded(args[2]), try bounded(args[3]), day))
134142 }
135143 lisp.define("calendar-nth-named-day") { lisp, args in
136 gregorian(try Elisp.int(try lisp.callNamed("calendar-nth-named-absday", args)))
144 gregorian(try bounded(try lisp.callNamed("calendar-nth-named-absday", args)))
137145 }
138146 lisp.define("calendar-iso-from-absolute") { _, args in
139147 try arity(args, 1...1, "calendar-iso-from-absolute")
140 let day = try Elisp.int(args[0])
148 let day = try bounded(args[0])
141149 let thursday = day - (Days.weekday(day) + 6) % 7 + 3
142150 return .list([.integer(Days.isoWeek(day)), .integer(Days.weekday(day)), .integer(Days.date(thursday).year)])
143151 }
@@ -175,13 +183,13 @@ public enum DiarySexp {
175183 lisp.define("diary-float") { _, args in
176184 try arity(args, 3...5, "diary-float")
177185 let month = args[0]
178 let dayname = try Elisp.int(args[1])
179 let n = try Elisp.int(args[2])
180 let day = Elisp.isNil(optional(args, 3)) ? nil : try Elisp.int(args[3])
186 let dayname = try bounded(args[1])
187 let n = try bounded(args[2])
188 let day = Elisp.isNil(optional(args, 3)) ? nil : try bounded(args[3])
181189 let current = try absolute(try date())
182190 guard dayname == Days.weekday(current) else { return .nil }
183191 let d = Days.date(current)
184 let limit = nthNamedAbsday(-n, dayname, d.month, d.year, d.day)
192 let limit = try nthNamedAbsday(-n, dayname, d.month, d.year, d.day)
185193 let lastAbs = n > 0 ? limit : limit + 6
186194 let firstAbs = n > 0 ? limit - 6 : limit
187195 let first = Days.date(firstAbs)
@@ -194,25 +202,26 @@ public enum DiarySexp {
194202 default: false
195203 }
196204 }
197 func base(_ m: Int, _ y: Int) -> Int { day ?? (n > 0 ? 1 : lastDay(month: m, year: y)) }
205 func base(_ m: Int, _ y: Int) throws -> Int { try day ?? (n > 0 ? 1 : lastDay(month: m, year: y)) }
198206 let applies: Bool
199207 if first.month == last.month {
200 let b = base(first.month, first.year)
208 let b = try base(first.month, first.year)
201209 applies = monthMatches(first.month) && first.day <= b && b <= last.day
202210 } else {
211 let firstBase = try base(first.month, first.year)
212 let lastBase = try base(last.month, last.year)
203213 applies = (first.year < last.year || (first.year == last.year && first.month < last.month))
204 && ((monthMatches(first.month) && first.day <= base(first.month, first.year))
205 || (monthMatches(last.month) && base(last.month, last.year) <= last.day))
214 && ((monthMatches(first.month) && first.day <= firstBase) || (monthMatches(last.month) && lastBase <= last.day))
206215 }
207216 return applies ? Elisp.cons(optional(args, 4), try entry()) : .nil
208217 }
209218 lisp.define("diary-anniversary") { lisp, args in
210219 try arity(args, 2...4, "diary-anniversary")
211220 let made = try makeDate(lisp, args[0], args[1], optional(args, 2))
212 var dd = try Elisp.int(made[1])
213 var mm = try Elisp.int(made[0])
221 var dd = try bounded(made[1])
222 var mm = try bounded(made[0])
214223 let y = try parts(try date()).year
215 let diff = Elisp.isNil(made[2]) ? 100 : y - (try Elisp.int(made[2]))
224 let diff = Elisp.isNil(made[2]) ? 100 : y - (try bounded(made[2]))
216225 if mm == 2, dd == 29, !isLeap(y) {
217226 mm = 3
218227 dd = 1
@@ -233,12 +242,15 @@ public enum DiarySexp {
233242 lisp.define("diary-remind") { lisp, args in
234243 try arity(args, 2...3, "diary-remind")
235244 var days = args[1]
236 if case .integer(let n) = days, n < 0 { days = Elisp.list((1...(-n)).map { .integer($0) }) }
245 if case .integer(let n) = days, n < 0 {
246 guard n >= -Elisp.lengthLimit else { throw Elisp.Signal(message: "Too many days for diary-remind") }
247 days = Elisp.list((1...(-n)).map { .integer($0) })
248 }
237249 func remind(_ days: Sexp) throws -> Sexp {
238250 let applies = try lisp.eval(args[0])
239251 if !Elisp.isNil(applies) { return applies }
240252 if case .integer(let n) = days {
241 let shifted = gregorian(try absolute(try date()) + n)
253 let shifted = gregorian(try absolute(try date()) + (try bounded(.integer(n))))
242254 var found = try lisp.binding(["date": shifted]) { try lisp.eval(args[0]) }
243255 guard !Elisp.isNil(found) else { return .nil }
244256 if case .dotted = found { found = try Elisp.cdr(found) } else if case .list = found { found = try Elisp.cdr(found) }
@@ -258,7 +270,7 @@ public enum DiarySexp {
258270 lisp.define("org-date") { lisp, args in try iso(lisp) { try lisp.callNamed("diary-date", args) } }
259271 lisp.define("org-class") { _, args in
260272 try arity(args, 7...Int.max, "org-class")
261 let ints = try args.prefix(7).map(Elisp.int)
273 let ints = try args.prefix(7).map(bounded)
262274 let first = Days.absolute(year: ints[0], month: ints[1], day: ints[2])
263275 let last = Days.absolute(year: ints[3], month: ints[4], day: ints[5])
264276 let d = try absolute(try date())
Sources/OrgCore/Capture/Capture.swift +4 −2
@@ -289,7 +289,7 @@ public enum Capture {
289289 let ns = text as NSString
290290 var out = text
291291 for m in regex.matches(in: text, range: NSRange(location: 0, length: ns.length)).reversed() where !escaped(ns, at: m.range.location) {
292 let n = Int(ns.substring(with: m.range(at: 1)))!
292 let n = Int(ns.substring(with: m.range(at: 1))) ?? Int.max
293293 out = (out as NSString).replacingCharacters(in: m.range, with: n <= values.count ? values[n - 1] : "")
294294 }
295295 text = out
@@ -774,7 +774,9 @@ public enum Capture {
774774 guard let m = pos.firstMatch(of: /(I+)([-+][0-9]+)/) else { throw Failure.message("Invalid table line specification \"\(pos)\"") }
775775 let hlines = rows.indices.filter { rows[$0].range(of: "^[ \\t]*\\|-", options: .regularExpression) != nil }
776776 guard m.1.count <= hlines.count else { throw Failure.message("Invalid table line specification \"\(pos)\"") }
777 let delta = Int(m.2.replacingOccurrences(of: "+", with: ""))!
777 guard let delta = Int(m.2.replacingOccurrences(of: "+", with: "")), (-999_999...999_999).contains(delta) else {
778 throw Failure.message("Invalid table line specification \"\(pos)\"")
779 }
778780 buffer.point = lineStart(hlines[m.1.count - 1] + delta + (delta < 0 ? 0 : -1) + 1)
779781 } else if options.prepend {
780782 if let rule = rows.firstIndex(where: { $0.range(of: "^[ \\t]*\\|-", options: .regularExpression) != nil }) {
Sources/OrgCore/Compute/TableFormulas.swift +41 −25
@@ -343,18 +343,28 @@ extension EmacsBuffer {
343343 }
344344 }
345345
346 /// A number written in a formula, small enough to work with.
347 static func integer(_ s: String) throws -> Int {
348 guard let n = Int(s.hasPrefix("+") ? String(s.dropFirst()) : s), (-999_999_999...999_999_999).contains(n) else { throw Calc.Unsupported("number size \(s)") }
349 return n
350 }
351
346352 /// `org-table-time-string-to-seconds`.
347 static func timeStringToSeconds(_ s: String) -> String {
353 static func timeStringToSeconds(_ s: String) throws -> String {
348354 guard !s.isEmpty else { return s }
349 func seconds(_ sign: Substring, _ parts: [Substring]) -> String {
350 let total = parts.enumerated().reduce(0) { sum, part in
351 sum + (Int(part.element) ?? 0) * [3600, 60, 1][part.offset]
355 func seconds(_ sign: Substring, _ parts: [Substring]) throws -> String {
356 var total = 0
357 for (i, part) in parts.enumerated() {
358 let (scaled, o1) = (Int(part) ?? Int.max).multipliedReportingOverflow(by: [3600, 60, 1][i])
359 let (sum, o2) = total.addingReportingOverflow(scaled)
360 if o1 || o2 || Int(part) == nil { throw Calc.Unsupported("duration size") }
361 total = sum
352362 }
353363 return String(sign.isEmpty ? total : -total)
354364 }
355 if let m = s.firstMatch(of: /(-?)([0-9]+):([0-9]+):([0-9]+)/) { return seconds(m.1, [m.2, m.3, m.4]) }
365 if let m = s.firstMatch(of: /(-?)([0-9]+):([0-9]+):([0-9]+)/) { return try seconds(m.1, [m.2, m.3, m.4]) }
356366 if s.range(of: timestampBoth, options: .regularExpression) == nil, let m = s.firstMatch(of: /(-?)([0-9]+):([0-9]+)/) {
357 return seconds(m.1, [m.2, m.3])
367 return try seconds(m.1, [m.2, m.3])
358368 }
359369 return numberText(s)
360370 }
@@ -363,9 +373,10 @@ extension EmacsBuffer {
363373
364374 /// `org-table-time-seconds-to-string`, with `org-table-duration-custom-format` `hours`
365375 /// and zero-padded hours.
366 static func secondsToString(_ seconds: Sexp, _ format: DurationFormat) -> String {
376 static func secondsToString(_ seconds: Sexp, _ format: DurationFormat) throws -> String {
367377 let value = (try? Elisp.number(seconds))?.double ?? 0
368378 let magnitude = abs(value)
379 guard magnitude.isFinite, magnitude < 1e15 else { throw Calc.Unsupported("duration size") }
369380 let result: String
370381 switch format {
371382 case .hours:
@@ -406,7 +417,7 @@ extension EmacsBuffer {
406417
407418 /// `org-table-formula-substitute-names`: column names become `$N`; parameters, named
408419 /// fields and constants their values, in parentheses outside Lisp formulas.
409 func substituteNames(_ formula: String, analysis: TableAnalysis) -> String {
420 func substituteNames(_ formula: String, analysis: TableAnalysis) throws -> String {
410421 let parenthesize = !formula.hasPrefix("'")
411422 let duration = formula.range(of: ";.*[Tt].*$", options: .regularExpression) != nil
412423 var new = formula
@@ -423,15 +434,19 @@ extension EmacsBuffer {
423434 }
424435 let regex = try! NSRegularExpression(pattern: "\\$([a-zA-Z][_a-zA-Z0-9]*)|(\\bremote\\([^)]*\\))")
425436 var start = 0
437 var substitutions = 0
426438 while let m = regex.firstMatch(in: new, range: NSRange(location: start, length: (new as NSString).length - start)) {
427439 let ns = new as NSString
440 // A constant whose value names itself would go on forever, as it does in Org.
441 substitutions += 1
442 if substitutions > 1000 { throw Calc.Unsupported("constants that refer to themselves") }
428443 if m.range(at: 2).location != NSNotFound {
429444 start = NSMaxRange(m.range(at: 2))
430445 continue
431446 }
432447 start = m.range.location + 1
433448 var value = tableConstant(ns.substring(with: m.range(at: 1)), analysis: analysis)
434 if duration, value.contains(where: { !$0.isWhitespace }) { value = Self.timeStringToSeconds(value) }
449 if duration, value.contains(where: { !$0.isWhitespace }) { value = try Self.timeStringToSeconds(value) }
435450 new = ns.replacingCharacters(in: m.range, with: parenthesize ? "(" + value + ")" : value)
436451 }
437452 return new
@@ -453,7 +468,7 @@ extension EmacsBuffer {
453468 }
454469 let remote = EmacsBuffer(string, point: m.range.location, settings: settings, options: options)
455470 let analysis = try remote.tableAnalyze()
456 let resolved = remote.substituteNames(try Self.handleFirstLast(Self.refsToRC(form), analysis: analysis), analysis: analysis)
471 let resolved = try remote.substituteNames(try Self.handleFirstLast(Self.refsToRC(form), analysis: analysis), analysis: analysis)
457472 let range = try! NSRegularExpression(pattern: Self.rangePattern)
458473 let rns = resolved as NSString
459474 if let r = range.firstMatch(in: resolved, range: NSRange(location: 0, length: rns.length)), r.range.length > 1 {
@@ -477,7 +492,7 @@ extension EmacsBuffer {
477492 let numeric = try! NSRegularExpression(pattern: "([pnfse])(-?[0-9]+)")
478493 while let m = numeric.firstMatch(in: flags, range: NSRange(location: 0, length: (flags as NSString).length)) {
479494 let ns = flags as NSString
480 let n = Int(ns.substring(with: m.range(at: 2)))!
495 let n = try Self.integer(ns.substring(with: m.range(at: 2)))
481496 switch ns.substring(with: m.range(at: 1)) {
482497 case "p": modes.precision = n
483498 case "n": modes.format = .float(n)
@@ -505,16 +520,16 @@ extension EmacsBuffer {
505520 }
506521 if flags.contains(where: { !$0.isWhitespace }) { fmt = flags }
507522 }
508 if !substituted { formula = try Self.handleFirstLast(substituteNames(formula, analysis: analysis), analysis: analysis) }
523 if !substituted { formula = try Self.handleFirstLast(try substituteNames(formula, analysis: analysis), analysis: analysis) }
509524 var fields = Self.splitFields(substring(lineStart(point), lineEnd(point)).trimmingCharacters(in: .whitespaces), separator: " *\\| *")
510 if duration != nil { fields = fields.map(Self.timeStringToSeconds) }
525 if duration != nil { fields = try fields.map(Self.timeStringToSeconds) }
511526 if mode.numbers { fields = fields.map { $0.contains(where: { !$0.isWhitespace }) ? Self.numberText($0) : $0 } }
512527 if formula.count > 2, formula.hasPrefix("'(") { mode.lisp = literal ? .literal : .lisp }
513528 func reference(_ value: TableRangeValue) throws -> String {
514529 guard duration != nil else { return try Self.makeReference(value, mode) }
515530 switch value {
516 case .field(let s): return try Self.makeReference(.field(Self.timeStringToSeconds(s)), mode)
517 case .list(let items): return try Self.makeReference(.list(items.map(Self.timeStringToSeconds)), mode)
531 case .field(let s): return try Self.makeReference(.field(try Self.timeStringToSeconds(s)), mode)
532 case .list(let items): return try Self.makeReference(.list(try items.map(Self.timeStringToSeconds)), mode)
518533 }
519534 }
520535 let n0 = tableCurrentColumn()
@@ -561,15 +576,15 @@ extension EmacsBuffer {
561576 let rowRange = try! NSRegularExpression(pattern: "\\$(([-+])?[0-9]+)\\.\\.\\$(([-+])?[0-9]+)")
562577 while let m = rowRange.firstMatch(in: form, range: NSRange(location: 0, length: (form as NSString).length)) {
563578 let ns = form as NSString
564 let a = Int(ns.substring(with: m.range(at: 1)))! + (m.range(at: 2).location != NSNotFound ? n0 : 0)
565 let b = Int(ns.substring(with: m.range(at: 3)))! + (m.range(at: 4).location != NSNotFound ? n0 : 0)
579 let a = try Self.integer(ns.substring(with: m.range(at: 1))) + (m.range(at: 2).location != NSNotFound ? n0 : 0)
580 let b = try Self.integer(ns.substring(with: m.range(at: 3))) + (m.range(at: 4).location != NSNotFound ? n0 : 0)
566581 guard a >= 1, b <= fields.count, a - 1 <= b else { throw Calc.Unsupported("range outside row") }
567582 form = ns.replacingCharacters(in: m.range, with: try Self.makeReference(.list(Array(fields[(a - 1)..<b])), mode))
568583 }
569584 let single = try! NSRegularExpression(pattern: "\\$(([-+])?[0-9]+)")
570585 while let m = single.firstMatch(in: form, range: NSRange(location: 0, length: (form as NSString).length)) {
571586 let ns = form as NSString
572 let n = Int(ns.substring(with: m.range(at: 1)))! + (m.range(at: 2).location != NSNotFound ? n0 : 0)
587 let n = try Self.integer(ns.substring(with: m.range(at: 1))) + (m.range(at: 2).location != NSNotFound ? n0 : 0)
573588 let index = (n == 0 ? n0 : max(n, 1)) - 1
574589 guard index < fields.count else { throw UserError(message: "Invalid field specifier \"\(ns.substring(with: m.range))\"") }
575590 let replacement = try Self.makeReference(.field(fields[index]), mode)
@@ -593,8 +608,8 @@ extension EmacsBuffer {
593608 }
594609 }
595610 if let duration, !result.isEmpty, result != "#ERROR" {
596 let seconds = result.range(of: "^[0-9]+:[0-9]+(:[0-9]+)?$", options: .regularExpression) != nil ? Self.timeStringToSeconds(result) : result
597 result = Self.secondsToString(Elisp.stringToNumber(seconds), duration)
611 let seconds = result.range(of: "^[0-9]+:[0-9]+(:[0-9]+)?$", options: .regularExpression) != nil ? try Self.timeStringToSeconds(result) : result
612 result = try Self.secondsToString(Elisp.stringToNumber(seconds), duration)
598613 }
599614 }
600615 if let fmt {
@@ -622,7 +637,7 @@ extension EmacsBuffer {
622637 return "#ERROR"
623638 }
624639 var result = Elisp.printed(value)
625 if let duration { result = Self.secondsToString(Elisp.stringToNumber(result), duration) }
640 if let duration { result = try Self.secondsToString(Elisp.stringToNumber(result), duration) }
626641 return result
627642 }
628643
@@ -710,11 +725,11 @@ extension EmacsBuffer {
710725 var columnEquations: [(Int, String)] = []
711726 var fieldEquations: [(String, String)] = []
712727 for (oldLHS, rawRHS) in stored {
713 let rhs = substituteNames(try Self.handleFirstLast(rawRHS, analysis: analysis), analysis: analysis)
728 let rhs = try substituteNames(try Self.handleFirstLast(rawRHS, analysis: analysis), analysis: analysis)
714729 if oldLHS.range(of: "^@-?I+", options: .regularExpression) != nil { throw UserError(message: "Can't assign to hline relative reference") }
715730 let lhs = try Self.handleFirstLast(oldLHS, analysis: analysis)
716731 if lhs.range(of: "^\\$[0-9]+$", options: .regularExpression) != nil {
717 columnEquations.append((Int(lhs.dropFirst())!, rhs))
732 columnEquations.append((try Self.integer(String(lhs.dropFirst())), rhs))
718733 } else {
719734 fieldEquations.append((lhs, rhs))
720735 }
@@ -723,7 +738,7 @@ extension EmacsBuffer {
723738 var fields: [(row: Int, column: Int, rhs: String)] = []
724739 for (lhs, rhs) in fieldEquations {
725740 if let m = lhs.firstMatch(of: /^@([0-9]+)\$([0-9]+)$/) {
726 fields.append((Int(m.1)!, Int(m.2)!, rhs))
741 fields.append((try Self.integer(String(m.1)), try Self.integer(String(m.2)), rhs))
727742 } else if lhs.range(of: "^[a-zA-Z][_a-zA-Z0-9]*$", options: .regularExpression) != nil {
728743 guard let location = analysis.namedFields.last(where: { $0.name == lhs }), let row = Self.lineToDline(location.line, analysis) else {
729744 throw UserError(message: "Unknown field: \(lhs)")
@@ -732,7 +747,8 @@ extension EmacsBuffer {
732747 } else if lhs.range(of: "^@[-+0-9]+\\$-?[0-9]+$", options: .regularExpression) != nil {
733748 throw Calc.Unsupported("relative field formula")
734749 } else if let m = lhs.firstMatch(of: /^@([0-9]+)$/) {
735 for c in 1...analysis.ncol { fields.append((Int(m.1)!, c, rhs)) }
750 let row = try Self.integer(String(m.1))
751 for c in 1...analysis.ncol { fields.append((row, c, rhs)) }
736752 } else {
737753 let corners = try rangeCorners(lhs, analysis: analysis)
738754 guard let r1 = Self.lineToDline(corners.r1, analysis), let r2 = Self.lineToDline(corners.r2, analysis, above: true), r1 <= r2 else {
Sources/OrgCore/Config/Elisp.swift +58 −16
@@ -20,6 +20,11 @@ public final class Elisp {
2020 private var functions: [String: Builtin] = [:]
2121 private var steps = 0
2222 static let stepLimit = 1_000_000
23 /// Nested evaluations, as `max-lisp-eval-depth` limits them.
24 private var depth = 0
25 static let depthLimit = 1600
26 /// The most elements a list built at once may have.
27 static let lengthLimit = 1_000_000
2328 /// What `princ` and the other printers wrote: the innermost `with-output-to-string`, or
2429 /// standard output.
2530 private var outputs: [String] = [""]
@@ -168,9 +173,21 @@ public final class Elisp {
168173
169174 // MARK: - Evaluation
170175
176 /// Less than 128 KB of this thread's stack left: background threads have only 512 KB.
177 static func stackIsLow() -> Bool {
178 var marker = 0
179 let here = withUnsafeMutablePointer(to: &marker) { UInt(bitPattern: $0) }
180 let top = UInt(bitPattern: pthread_get_stackaddr_np(pthread_self()))
181 let size = UInt(pthread_get_stacksize_np(pthread_self()))
182 return here < top - size + 128 * 1024
183 }
184
171185 public func eval(_ form: Sexp) throws -> Sexp {
172186 steps += 1
173187 if steps > Self.stepLimit { throw Signal(message: "Evaluation took too long") }
188 depth += 1
189 defer { depth -= 1 }
190 if depth > Self.depthLimit || Self.stackIsLow() { throw Signal(message: "Lisp nesting exceeds ‘max-lisp-eval-depth’") }
174191 switch form {
175192 case .symbol(let name):
176193 if name == "nil" || name == "t" || name.hasPrefix(":") { return form }
@@ -285,15 +302,18 @@ public final class Elisp {
285302 let spec = try args.first.map(Self.elements) ?? []
286303 guard case .symbol(let variable)? = spec.first, spec.count >= 2 else { throw Signal(message: "Bad \(name)") }
287304 let source = try eval(spec[1])
288 let items = name == "dolist" ? try Self.elements(source) : (0..<max(0, try Self.int(source))).map { Sexp.integer($0) }
305 let items = name == "dolist" ? try Self.elements(source) : []
306 let count = name == "dolist" ? items.count : max(0, try Self.int(source))
289307 scopes.append([:])
290308 defer { scopes.removeLast() }
291 for item in items {
292 scopes[scopes.count - 1][variable] = item
309 for i in 0..<count {
310 steps += 1
311 if steps > Self.stepLimit { throw Signal(message: "Evaluation took too long") }
312 scopes[scopes.count - 1][variable] = name == "dolist" ? items[i] : .integer(i)
293313 _ = try progn(args.dropFirst())
294314 }
295315 if spec.count > 2 {
296 scopes[scopes.count - 1][variable] = name == "dotimes" ? .integer(items.count) : .nil
316 scopes[scopes.count - 1][variable] = name == "dotimes" ? .integer(count) : .nil
297317 return try eval(spec[2])
298318 }
299319 return .nil
@@ -379,7 +399,9 @@ public final class Elisp {
379399 guard var result = numbers.first else { return .integer(identity) }
380400 if numbers.count == 1, name == "-" {
381401 switch result {
382 case .int(let i): return .integer(-i)
402 case .int(let i):
403 guard i != .min else { throw Unsupported(what: "integer size") }
404 return .integer(-i)
383405 case .float(let d): return .float(-d)
384406 }
385407 }
@@ -412,14 +434,17 @@ public final class Elisp {
412434 try arity(args, 2...2, "%")
413435 let b = try int(args[1])
414436 guard b != 0 else { throw Signal(message: "Arithmetic error") }
415 return .integer(try int(args[0]) % b)
437 let (r, overflow) = try int(args[0]).remainderReportingOverflow(dividingBy: b)
438 if overflow { throw Unsupported(what: "integer size") }
439 return .integer(r)
416440 }
417441 lisp.define("mod") { _, args in
418442 try arity(args, 2...2, "mod")
419443 switch (try number(args[0]), try number(args[1])) {
420444 case (.int(let a), .int(let b)):
421445 guard b != 0 else { throw Signal(message: "Arithmetic error") }
422 let r = a % b
446 let (r, overflow) = a.remainderReportingOverflow(dividingBy: b)
447 if overflow { throw Unsupported(what: "integer size") }
423448 return .integer(r != 0 && (r < 0) != (b < 0) ? r + b : r)
424449 case (let a, let b):
425450 let r = fmod(a.double, b.double)
@@ -431,7 +456,9 @@ public final class Elisp {
431456 lisp.define("abs") { _, args in
432457 try arity(args, 1...1, "abs")
433458 switch try number(args[0]) {
434 case .int(let i): return .integer(abs(i))
459 case .int(let i):
460 guard i != .min else { throw Unsupported(what: "integer size") }
461 return .integer(abs(i))
435462 case .float(let d): return .float(abs(d))
436463 }
437464 }
@@ -575,6 +602,8 @@ public final class Elisp {
575602 let to = try int(args[1])
576603 let step = args.count > 2 && !isNil(args[2]) ? try int(args[2]) : 1
577604 guard step != 0 else { throw Signal(message: "The increment can not be zero") }
605 let span = Double(to) - Double(from)
606 guard span / Double(step) < Double(Self.lengthLimit) else { throw Signal(message: "Too many elements for number-sequence") }
578607 return list(Array(stride(from: from, through: to, by: step)).map { .integer($0) })
579608 }
580609 for name in ["memq", "member", "memql"] {
@@ -682,12 +711,12 @@ public final class Elisp {
682711 lisp.define("string-lessp") { lisp, args in try lisp.callNamed("string<", args) }
683712 lisp.define("upcase") { _, args in
684713 try arity(args, 1...1, "upcase")
685 if case .integer(let c) = args[0] { return .integer(Int(Unicode.Scalar(c).map { String($0).uppercased().unicodeScalars.first!.value } ?? UInt32(c))) }
714 if case .integer(let c) = args[0] { return .integer(Int(String(try scalar(c)).uppercased().unicodeScalars.first!.value)) }
686715 return .string(try string(args[0]).uppercased())
687716 }
688717 lisp.define("downcase") { _, args in
689718 try arity(args, 1...1, "downcase")
690 if case .integer(let c) = args[0] { return .integer(Int(Unicode.Scalar(c).map { String($0).lowercased().unicodeScalars.first!.value } ?? UInt32(c))) }
719 if case .integer(let c) = args[0] { return .integer(Int(String(try scalar(c)).lowercased().unicodeScalars.first!.value)) }
691720 return .string(try string(args[0]).lowercased())
692721 }
693722 lisp.define("capitalize") { _, args in
@@ -753,17 +782,30 @@ public final class Elisp {
753782 }
754783 }
755784
756 static func add(_ n: Number, _ k: Int) -> Number {
785 static func add(_ n: Number, _ k: Int) throws -> Number {
757786 switch n {
758 case .int(let i): .int(i + k)
759 case .float(let d): .float(d + Double(k))
787 case .int(let i):
788 let (sum, overflow) = i.addingReportingOverflow(k)
789 if overflow { throw Unsupported(what: "integer size") }
790 return .int(sum)
791 case .float(let d): return .float(d + Double(k))
792 }
793 }
794
795 /// A character code as a character.
796 static func scalar(_ code: Int) throws -> Unicode.Scalar {
797 guard code >= 0, code <= 0x10FFFF, let scalar = Unicode.Scalar(UInt32(code)) else {
798 throw Signal(message: "Invalid character: \(code)")
760799 }
800 return scalar
761801 }
762802
763803 static func divide(_ a: Number, _ b: Number, floating: Bool) throws -> Number {
764804 if !floating, case .int(let x) = a, case .int(let y) = b {
765805 guard y != 0 else { throw Signal(message: "Arithmetic error") }
766 return .int(x / y)
806 let (q, overflow) = x.dividedReportingOverflow(by: y)
807 if overflow { throw Unsupported(what: "integer size") }
808 return .int(q)
767809 }
768810 return .float(a.double / b.double)
769811 }
@@ -807,7 +849,7 @@ public final class Elisp {
807849 case .string(let s): return s
808850 case _ where isNil(v): return ""
809851 case .list, .vector:
810 return String(String.UnicodeScalarView(try sequence(v).map { Unicode.Scalar(UInt32(try int($0))) ?? " " }))
852 return String(String.UnicodeScalarView(try sequence(v).map { try scalar(try int($0)) }))
811853 default: throw Signal(message: "Wrong type argument: sequencep, \(v.description)")
812854 }
813855 }
@@ -887,7 +929,7 @@ public final class Elisp {
887929 out += text
888930 continue
889931 case "c":
890 text = Unicode.Scalar(UInt32(try int(try argument()))).map { String($0) } ?? ""
932 text = String(try scalar(try int(try argument())))
891933 case "e", "f", "g":
892934 let value = try number(try argument()).double
893935 out += String(format: "%" + flags + width + (precision.map { ".\($0)" } ?? "") + String(conversion), value)
Tests/OrgCoreTests/ElispTests.swift +48
@@ -46,3 +46,51 @@ struct ElispTests {
4646 }
4747 }
4848}
49
50/// Inputs that once crashed the evaluator, the diary functions or duration formulas.
51struct HostileInputTests {
52 static let forms = [
53 "(1+ 9223372036854775807)", "(1- -9223372036854775808)", "(- -9223372036854775808)", "(abs -9223372036854775808)",
54 "(% -9223372036854775808 -1)", "(mod -9223372036854775808 -1)", "(/ -9223372036854775808 -1)",
55 "(upcase -1)", "(downcase -1)", "(concat (list -1))", "(format \"%c\" -1)", "(format \"%c\" 99999999)",
56 "(funcall (lambda (f) (funcall f f)) (lambda (f) (funcall f f)))",
57 "(dotimes (i 10000000000) nil)", "(number-sequence 1 10000000000)",
58 ]
59
60 @Test func evaluatorRefusesWithoutCrashing() {
61 for form in Self.forms { #expect(ElispTests.ours(form) == "error", "\(form)") }
62 }
63
64 /// On a stack the size of a background task's, deep recursion stops with an error.
65 @Test func deepRecursionFitsABackgroundStack() async {
66 let done = await withCheckedContinuation { continuation in
67 let thread = Thread {
68 continuation.resume(returning: ElispTests.ours("(funcall (lambda (f) (funcall f f)) (lambda (f) (funcall f f)))"))
69 }
70 thread.stackSize = 512 * 1024
71 thread.start()
72 }
73 #expect(done == "error")
74 }
75
76 @Test func diaryFunctionsRefuseWithoutCrashing() throws {
77 let day = Days.absolute(year: 2026, month: 10, day: 7)
78 for form in ["(calendar-last-day-of-month -20 2024)", "(calendar-gregorian-from-absolute 9223372036854775807)",
79 "(calendar-nth-named-absday -9223372036854775808 1 10 2026)", "(diary-remind '(diary-date 1 1 2027) -9223372036854775808)",
80 "(diary-float 10 3 -9223372036854775808)", "(diary-anniversary 10 7 -9223372036854775808)"] {
81 #expect(DiarySexp.entries(try LispReader.readFirst(form).sexp, entry: "E", day: day) == nil, "\(form)")
82 }
83 }
84
85 @Test func durationsAndHugeNumbersDoNotCrashFormulas() {
86 for table in [
87 "| 1e300 | |\n#+TBLFM: $2=$1*2;T\n", "| 1e300 | |\n#+TBLFM: $2='(* $1 2);T\n", "| 9999999999999999:00:00 | |\n#+TBLFM: $2=$1;T\n",
88 "| 1 | |\n#+TBLFM: $2=$1;p99999999999999999999\n", "| 1 | |\n#+TBLFM: $2=$99999999999999999999\n",
89 "| 1 | |\n#+TBLFM: @99999999999999999999$2=1\n", "#+CONSTANTS: a=$a\n| 1 | |\n#+TBLFM: $2=$a\n",
90 ] {
91 let caret = (table as NSString).range(of: "#+TBLFM").location + 3
92 let result = runCommand(TableRecalculate(all: true), table, caret: caret)
93 #expect(result.text == table, "\(table)")
94 }
95 }
96}