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
29 changes: 21 additions & 8 deletions src/css/css_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3601,27 +3601,40 @@ impl<'a> Parser<'a> {
}

/// The source text of the numeric token `next*` returned last, if an f32 may not hold it
/// (`1700000000.25`, `123456.78`) or it touches another number: `5.0.1` is `5.0` and `.1`,
/// (`1700000000.25`, `123456.78`, `8e64`) or it touches another number: `5.0.1` is `5.0` and `.1`,
/// whose values print as `5.1`.
#[inline]
pub(crate) fn raw_number_text(&self) -> Option<&'static [u8]> {
let cached = self.input.cached_token.as_ref()?;
if !matches!(
cached.token,
Token::Number(_) | Token::Percentage { .. } | Token::Dimension(_)
) {
return None;
}
let int_value = match cached.token {
Token::Number(num) | Token::Dimension(Dimension { num, .. }) => num.int_value,
Token::Percentage { int_value, .. } => int_value,
_ => return None,
};
let text = self.slice_from(cached.start_position);
let before = &self.input.tokenizer.src[..cached.start_position];
let touches_a_number = self.next_byte() == Some(b'.')
|| text.starts_with(b"+")
|| (text.starts_with(b".") && before.last().is_some_and(u8::is_ascii_digit));
if let Some(int) = int_value {
// No fraction and no exponent: long is more than 6 digits.
return (touches_a_number || int.unsigned_abs() >= 1_000_000).then_some(text);
}
let digits = text.strip_prefix(b"-").unwrap_or(text);
let digits = &digits[digits.iter().take_while(|b| **b == b'0').count()..];
let integer_digits = digits.iter().take_while(|b| b.is_ascii_digit()).count();
let is_long = integer_digits > 6 || (integer_digits == 6 && digits.get(6) == Some(&b'.'));
(is_long || touches_a_number).then_some(text)
let rest = &digits[integer_digits..];
let rest = rest.strip_prefix(b".").unwrap_or(rest);
let rest = &rest[rest.iter().take_while(|b| b.is_ascii_digit()).count()..];
// As in `Tokenizer::consume_numeric`: the `e` of `1em` and `1e-x` starts a unit.
let has_exponent = matches!(
rest,
[b'e' | b'E', b'0'..=b'9', ..] | [b'e' | b'E', b'+' | b'-', b'0'..=b'9', ..]
);
// `1e3\70` stops inside an escape, which reads on into what prints after it.
let has_escape = || strings::contains_char(text, b'\\');
(is_long || touches_a_number || (has_exponent && !has_escape())).then_some(text)
Comment thread
robobun marked this conversation as resolved.
Resolved
}

fn parse_empty(_: &mut Parser) -> CssResult<()> {
Expand Down
11 changes: 7 additions & 4 deletions test/js/bun/css/angle-serialization-hang.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,20 @@ import { bunEnv, bunExe } from "harness";

const cases: [css: string, expected: string][] = [
// Fuzzer repros: huge radian values in a known property, an unknown
// property (token list path), and an unparsed/invalid declaration.
// property, and an unparsed/invalid declaration. A value kept as tokens
// prints a long integer or a number with an exponent as written. There a
// huge angle reaches `Angle::to_css` only when its unit has an escape.
["a { rotate: 99999999999999999999999999999999999999999999999999rad }", "a{rotate:3.40282e38rad}"],
["a { ro: 1.3157e308rad }", "a{ro:3.40282e38rad}"],
["a { ro: 1.3157e308rad }", "a{ro:1.3157e308rad}"],
["a { ro: 1e50ra\\64}", "a{ro:3.40282e38rad}"],
[
"a { rotate: 0.0000000000000000000000000000000000000\t00000000000000000000000000000000000200000000000000000000000000000001rad }",
"a{rotate:0 00000000000000000000000000000000000200000000000000000000000000000001rad}",
],
["a { rotate: 0 2e32rad }", "a{rotate:0 2e32rad}"],
// Other paths that serialize an Angle: transform functions and filters.
// Another path that serializes an Angle: transform functions. `filter` is kept as tokens.
["a { transform: rotate(3.3e33rad) }", "a{transform:rotate(3.3e33rad)}"],
["a { filter: hue-rotate(1e50rad) }", "a{filter:hue-rotate(3.40282e38rad)}"],
["a { filter: hue-rotate(1e50rad) }", "a{filter:hue-rotate(1e50rad)}"],
// calc() can produce a real infinity.
["a { rotate: calc(infinity * 1rad) }", "a{rotate:3.40282e38rad}"],
// Sanity check: small radian values still switch to degrees when shorter.
Expand Down
39 changes: 39 additions & 0 deletions test/js/bun/css/css.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8521,10 +8521,49 @@ describe("numeric tokens that print as written", () => {
"a{--v:1.0.0}",
"a{--v:2.0.0-beta.1}",
"a{--v:10.20.30}",
// An exponent: an f32 clamps `8e64` and reads `1e-50` as 0.
":root{--commit:12e45ab;--build:1e5f3a2;--ver:2e3;--tag:8e64}",
":root{--uuid:12345678-12e3-8d9b-a456-426614174000}",
"a{--small:1e-50;--w:1.23456789e10;--v:1.6777217e7}",
":root{--rounding:4.9406564584124654e-324}",
":root{--radius-round:1e5px}",
"a{--x:1E-5E;--y:1e+3;--z:.5e3;--w:-0.5e-3px;--v:1.50e3;--p:1e3%;--q:1e39%}",
"a{--a:8E64;--b:1e0;--c:1e09;--d:1e-07;--e:1E+09px;--f:1.50e-9}",
"a{width:var(--w,8e64px)}",
"a{--x:calc(1e39*1px)}",
"@unknown 1e3{a{b:1e3}}",
"a:unknown(8e64 1e-50){color:red}",
// `1000e3` and `10e1e` are other tokens.
"a{--g:1e3e3;--c:1e1e1e}",
// Two values, or two spellings of one, are two rules.
".x{--tag:8e64}.y{--tag:9e99}",
".x{--n:1e3}.y{--n:1E3}",
// The text is kept before a unit is converted (`1s`).
"a{--t:1e3ms}",
// A known property whose value does not parse is kept as tokens.
".a{z-index:7;z-index:1e3}",
Comment thread
robobun marked this conversation as resolved.
Resolved
"a{order:1e1}",
"a{width:calc(7.3px*4.9406564584124654e-324/4.9406564584124654e-324)}",
])("%s", css => {
minify_test(css, css);
});
minify_test("a{--v:5.0 .10}", "a{--v:5 .1}");
// The `e` of a unit is no exponent.
minify_test(
"a{--x:1.50e-x;--y:1.50E+;--z:1.50e;--w:1.50EM}",
"a{--x:1.5\\65 -x;--y:1.5\\65 +;--z:1.5\\65 ;--w:1.5em}",
);
// A typed value is an f32.
minify_test("a{width:1e39px}", "a{width:3.40282e38px}");
minify_test("a{flex-grow:1e3}", "a{flex-grow:1000}");
// Leading zeros are not digits.
minify_test("a{--x:0999999;--y:01000000;--z:-01000000px}", "a{--x:999999;--y:01000000;--z:-01000000px}");
// A unit with an escape prints from the value: text that stops inside one takes in the blank.
minify_test("a{--x:1e3\\70/**/a}", "a{--x:1000p a}");
cssTest(
":root { --tag: 8e64; --uuid: 12345678-12e3-8d9b-a456-426614174000 }",
":root { --tag: 8e64; --uuid: 12345678-12e3-8d9b-a456-426614174000; }",
);
});

describe("number serialization", () => {
Expand Down
Loading