pptxboss 2.2.0__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. {pptxboss-2.2.0 → pptxboss-2.3.0}/Cargo.lock +6 -6
  2. {pptxboss-2.2.0 → pptxboss-2.3.0}/Cargo.toml +4 -4
  3. {pptxboss-2.2.0 → pptxboss-2.3.0}/PKG-INFO +6 -4
  4. {pptxboss-2.2.0/crates/pptxboss-check → pptxboss-2.3.0}/README.md +5 -3
  5. {pptxboss-2.2.0/crates/pptxboss-testkit → pptxboss-2.3.0/crates/pptxboss-check}/README.md +5 -3
  6. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-check/src/container.rs +1 -1
  7. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/README.md +5 -3
  8. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/src/inflate.rs +212 -19
  9. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/src/package.rs +121 -17
  10. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/src/ppt.rs +2 -1
  11. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/src/zip.rs +18 -1
  12. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/tests/document.rs +64 -0
  13. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-py/src/write.rs +100 -5
  14. {pptxboss-2.2.0 → pptxboss-2.3.0/crates/pptxboss-testkit}/README.md +5 -3
  15. pptxboss-2.3.0/crates/pptxboss-write/README.md +338 -0
  16. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-write/examples/placed.rs +18 -3
  17. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-write/src/layout.rs +280 -8
  18. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-write/src/lib.rs +66 -0
  19. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-write/src/parts.rs +146 -16
  20. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-write/tests/roundtrip.rs +83 -2
  21. {pptxboss-2.2.0 → pptxboss-2.3.0}/pyproject.toml +1 -1
  22. {pptxboss-2.2.0 → pptxboss-2.3.0}/python/pptxboss/_pptxboss.pyi +29 -3
  23. {pptxboss-2.2.0 → pptxboss-2.3.0}/python/pptxboss/write.py +4 -2
  24. pptxboss-2.2.0/crates/pptxboss-write/README.md +0 -336
  25. {pptxboss-2.2.0 → pptxboss-2.3.0}/LICENSE-APACHE +0 -0
  26. {pptxboss-2.2.0 → pptxboss-2.3.0}/LICENSE-MIT +0 -0
  27. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-check/Cargo.toml +0 -0
  28. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-check/src/content_types.rs +0 -0
  29. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-check/src/core_props.rs +0 -0
  30. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-check/src/lib.rs +0 -0
  31. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-check/src/pml.rs +0 -0
  32. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-check/src/relationships.rs +0 -0
  33. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-check/src/xml_parts.rs +0 -0
  34. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-check/tests/rules.rs +0 -0
  35. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/Cargo.toml +0 -0
  36. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/benches/read.rs +0 -0
  37. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/examples/cfb_ls.rs +0 -0
  38. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/examples/inflate_ab.rs +0 -0
  39. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/examples/inflate_check.rs +0 -0
  40. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/examples/open_loop.rs +0 -0
  41. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/examples/open_phases.rs +0 -0
  42. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/examples/part_costs.rs +0 -0
  43. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/examples/ppt_dump.rs +0 -0
  44. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/examples/ppt_text.rs +0 -0
  45. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/examples/ppt_walk.rs +0 -0
  46. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/examples/read_counts.rs +0 -0
  47. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/examples/time_file.rs +0 -0
  48. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/src/cfb.rs +0 -0
  49. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/src/chart.rs +0 -0
  50. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/src/comments.rs +0 -0
  51. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/src/crc32.rs +0 -0
  52. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/src/diagram.rs +0 -0
  53. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/src/document.rs +0 -0
  54. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/src/encoding.rs +0 -0
  55. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/src/error.rs +0 -0
  56. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/src/hash.rs +0 -0
  57. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/src/lib.rs +0 -0
  58. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/src/markdown.rs +0 -0
  59. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/src/mce.rs +0 -0
  60. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/src/model.rs +0 -0
  61. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/src/opc.rs +0 -0
  62. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/src/pml.rs +0 -0
  63. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/src/presentation.rs +0 -0
  64. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/src/properties.rs +0 -0
  65. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/src/slide.rs +0 -0
  66. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/src/text.rs +0 -0
  67. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-core/src/xml.rs +0 -0
  68. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-py/Cargo.toml +0 -0
  69. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-py/src/lib.rs +0 -0
  70. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-py/src/model.rs +0 -0
  71. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-py/src/package.rs +0 -0
  72. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-testkit/Cargo.toml +0 -0
  73. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-testkit/examples/gen_fixtures.rs +0 -0
  74. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-testkit/src/deck.rs +0 -0
  75. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-testkit/src/legacy.rs +0 -0
  76. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-testkit/src/lib.rs +0 -0
  77. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-write/Cargo.toml +0 -0
  78. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-write/examples/styled.rs +0 -0
  79. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-write/src/fonts.rs +0 -0
  80. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-write/src/image.rs +0 -0
  81. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-write/src/lzcomp.rs +0 -0
  82. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-write/src/markdown.rs +0 -0
  83. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-write/src/metrics.rs +0 -0
  84. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-write/src/metrics_data.rs +0 -0
  85. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-write/src/mtx.rs +0 -0
  86. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-write/src/style.rs +0 -0
  87. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-write/src/xml.rs +0 -0
  88. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-write/src/zipw.rs +0 -0
  89. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-write/tests/data/boxy-bold.ttf +0 -0
  90. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-write/tests/data/boxy-hinted.ttf +0 -0
  91. {pptxboss-2.2.0 → pptxboss-2.3.0}/crates/pptxboss-write/tests/data/boxy-regular.ttf +0 -0
  92. {pptxboss-2.2.0 → pptxboss-2.3.0}/python/pptxboss/__init__.py +0 -0
  93. {pptxboss-2.2.0 → pptxboss-2.3.0}/python/pptxboss/py.typed +0 -0
@@ -355,7 +355,7 @@ checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
355
355
 
356
356
  [[package]]
357
357
  name = "pptxboss-check"
358
- version = "2.2.0"
358
+ version = "2.3.0"
359
359
  dependencies = [
360
360
  "pptxboss-core",
361
361
  "pptxboss-testkit",
@@ -364,7 +364,7 @@ dependencies = [
364
364
 
365
365
  [[package]]
366
366
  name = "pptxboss-cli"
367
- version = "2.2.0"
367
+ version = "2.3.0"
368
368
  dependencies = [
369
369
  "clap",
370
370
  "pptxboss-check",
@@ -377,7 +377,7 @@ dependencies = [
377
377
 
378
378
  [[package]]
379
379
  name = "pptxboss-core"
380
- version = "2.2.0"
380
+ version = "2.3.0"
381
381
  dependencies = [
382
382
  "criterion",
383
383
  "flate2",
@@ -388,7 +388,7 @@ dependencies = [
388
388
 
389
389
  [[package]]
390
390
  name = "pptxboss-py"
391
- version = "2.2.0"
391
+ version = "2.3.0"
392
392
  dependencies = [
393
393
  "pptxboss-check",
394
394
  "pptxboss-core",
@@ -398,14 +398,14 @@ dependencies = [
398
398
 
399
399
  [[package]]
400
400
  name = "pptxboss-testkit"
401
- version = "2.2.0"
401
+ version = "2.3.0"
402
402
  dependencies = [
403
403
  "flate2",
404
404
  ]
405
405
 
406
406
  [[package]]
407
407
  name = "pptxboss-write"
408
- version = "2.2.0"
408
+ version = "2.3.0"
409
409
  dependencies = [
410
410
  "flate2",
411
411
  "pptxboss-check",
@@ -3,7 +3,7 @@ resolver = "2"
3
3
  members = ["crates/pptxboss-core", "crates/pptxboss-check", "crates/pptxboss-write", "crates/pptxboss-py", "crates/pptxboss-testkit"]
4
4
 
5
5
  [workspace.package]
6
- version = "2.2.0" # x-release-please-version
6
+ version = "2.3.0" # x-release-please-version
7
7
  edition = "2021"
8
8
  license = "MIT OR Apache-2.0"
9
9
  repository = "https://github.com/4thel00z/pptxboss"
@@ -16,9 +16,9 @@ keywords = ["pptx", "powerpoint", "ecma-376", "ooxml"]
16
16
  # Inter-crate dependencies carry an explicit `version` (required by
17
17
  # `cargo publish`) alongside the local `path`. The markers keep them in
18
18
  # lockstep with the workspace version at release time.
19
- pptxboss-core = { path = "crates/pptxboss-core", version = "2.2.0" } # x-release-please-version
20
- pptxboss-check = { path = "crates/pptxboss-check", version = "2.2.0" } # x-release-please-version
21
- pptxboss-write = { path = "crates/pptxboss-write", version = "2.2.0" } # x-release-please-version
19
+ pptxboss-core = { path = "crates/pptxboss-core", version = "2.3.0" } # x-release-please-version
20
+ pptxboss-check = { path = "crates/pptxboss-check", version = "2.3.0" } # x-release-please-version
21
+ pptxboss-write = { path = "crates/pptxboss-write", version = "2.3.0" } # x-release-please-version
22
22
  criterion = { version = "0.5", default-features = false, features = ["cargo_bench_support"] }
23
23
 
24
24
  # Release builds trade compile time for cross-crate inlining and tighter
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pptxboss
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: Programming Language :: Rust
@@ -210,9 +210,11 @@ deck.write_to("review.pptx")?;
210
210
  ```
211
211
 
212
212
  Content without coordinates is placed by the layout engine: it measures
213
- text with embedded font metrics, stacks blocks or sets them in columns,
214
- shrinks body text to the theme's minimum when a slide is full, and
215
- continues what is left on the next slide. `Theme::embed_font` stores a
213
+ text with embedded font metrics, stacks blocks (text, pictures, tables,
214
+ key numbers, pull quotes) or sets them in columns, shrinks body text to
215
+ the theme's minimum when a slide is full, and continues what is left on
216
+ the next slide. Section slides and a footer band with the slide number
217
+ come from `Slide::section` and `Theme::footer`. `Theme::embed_font` stores a
216
218
  TrueType or OpenType file in the deck, compressed the way PowerPoint
217
219
  stores its own embedded fonts, so it renders in that font on machines
218
220
  that lack it. Output is deterministic (fixed timestamps, fixed
@@ -187,9 +187,11 @@ deck.write_to("review.pptx")?;
187
187
  ```
188
188
 
189
189
  Content without coordinates is placed by the layout engine: it measures
190
- text with embedded font metrics, stacks blocks or sets them in columns,
191
- shrinks body text to the theme's minimum when a slide is full, and
192
- continues what is left on the next slide. `Theme::embed_font` stores a
190
+ text with embedded font metrics, stacks blocks (text, pictures, tables,
191
+ key numbers, pull quotes) or sets them in columns, shrinks body text to
192
+ the theme's minimum when a slide is full, and continues what is left on
193
+ the next slide. Section slides and a footer band with the slide number
194
+ come from `Slide::section` and `Theme::footer`. `Theme::embed_font` stores a
193
195
  TrueType or OpenType file in the deck, compressed the way PowerPoint
194
196
  stores its own embedded fonts, so it renders in that font on machines
195
197
  that lack it. Output is deterministic (fixed timestamps, fixed
@@ -187,9 +187,11 @@ deck.write_to("review.pptx")?;
187
187
  ```
188
188
 
189
189
  Content without coordinates is placed by the layout engine: it measures
190
- text with embedded font metrics, stacks blocks or sets them in columns,
191
- shrinks body text to the theme's minimum when a slide is full, and
192
- continues what is left on the next slide. `Theme::embed_font` stores a
190
+ text with embedded font metrics, stacks blocks (text, pictures, tables,
191
+ key numbers, pull quotes) or sets them in columns, shrinks body text to
192
+ the theme's minimum when a slide is full, and continues what is left on
193
+ the next slide. Section slides and a footer band with the slide number
194
+ come from `Slide::section` and `Theme::footer`. `Theme::embed_font` stores a
193
195
  TrueType or OpenType file in the deck, compressed the way PowerPoint
194
196
  stores its own embedded fonts, so it renders in that font on machines
195
197
  that lack it. Output is deterministic (fixed timestamps, fixed
@@ -297,7 +297,7 @@ pub fn run(ctx: &Context<'_>, sink: &mut Sink<'_>) {
297
297
  let Some(entry) = archive.get(index) else {
298
298
  continue;
299
299
  };
300
- if let Err(err) = archive.read(entry, &mut data) {
300
+ if let Err(err) = archive.read_parsed(entry, &mut data) {
301
301
  sink.push(
302
302
  Finding::new(&ZIP008, format!("part could not be read: {err}"))
303
303
  .in_part(&part.name),
@@ -187,9 +187,11 @@ deck.write_to("review.pptx")?;
187
187
  ```
188
188
 
189
189
  Content without coordinates is placed by the layout engine: it measures
190
- text with embedded font metrics, stacks blocks or sets them in columns,
191
- shrinks body text to the theme's minimum when a slide is full, and
192
- continues what is left on the next slide. `Theme::embed_font` stores a
190
+ text with embedded font metrics, stacks blocks (text, pictures, tables,
191
+ key numbers, pull quotes) or sets them in columns, shrinks body text to
192
+ the theme's minimum when a slide is full, and continues what is left on
193
+ the next slide. Section slides and a footer band with the slide number
194
+ come from `Slide::section` and `Theme::footer`. `Theme::embed_font` stores a
193
195
  TrueType or OpenType file in the deck, compressed the way PowerPoint
194
196
  stores its own embedded fonts, so it renders in that font on machines
195
197
  that lack it. Output is deterministic (fixed timestamps, fixed
@@ -37,6 +37,29 @@ const DIST_EXTRA: [u8; 30] = [
37
37
  /// DEFLATE expands at most this much (a 258-byte match per 2-bit code),
38
38
  /// which bounds the reservation a corrupt size hint can ask for.
39
39
  const MAX_RATIO: usize = 1032;
40
+ /// The most one part may inflate to.
41
+ pub const MAX_PART_BYTES: usize = 128 << 20;
42
+ /// How far past its compressed size a parsed part may inflate.
43
+ pub const MAX_INFLATE_RATIO: usize = 100;
44
+ /// Output up to this size is exempt from the ratio, and counted in the deck limit.
45
+ pub const RATIO_EXEMPT_BYTES: usize = 1 << 20;
46
+ /// The most a deck's parsed parts may inflate to in all.
47
+ pub const MAX_DECK_BYTES: usize = 512 << 20;
48
+
49
+ /// The output limit for a part parsed as XML: `MAX_INFLATE_RATIO` times its
50
+ /// compressed size, within `RATIO_EXEMPT_BYTES` and `MAX_PART_BYTES`.
51
+ pub fn ratio_limit(compressed: usize) -> usize {
52
+ compressed
53
+ .saturating_mul(MAX_INFLATE_RATIO)
54
+ .clamp(RATIO_EXEMPT_BYTES, MAX_PART_BYTES)
55
+ }
56
+
57
+ /// The output limit for all of a deck's parsed parts together: the ratio times
58
+ /// the file size, within `RATIO_EXEMPT_BYTES` and `MAX_DECK_BYTES`.
59
+ pub fn deck_limit(file: usize) -> usize {
60
+ file.saturating_mul(MAX_INFLATE_RATIO)
61
+ .clamp(RATIO_EXEMPT_BYTES, MAX_DECK_BYTES)
62
+ }
40
63
  /// The order in which code length code lengths are transmitted (3.2.7).
41
64
  const CODE_LENGTH_ORDER: [usize; 19] = [
42
65
  16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15,
@@ -50,6 +73,10 @@ fn invalid(msg: &str) -> Error {
50
73
  Error::Inflate(msg.into())
51
74
  }
52
75
 
76
+ fn past_limit(limit: usize) -> Error {
77
+ Error::Inflate(format!("output passes the inflate limit of {limit} bytes"))
78
+ }
79
+
53
80
  /// A little-endian bit reader over the whole input. Failures are recorded
54
81
  /// in `failure` and checked once per symbol, keeping `Result` out of the
55
82
  /// decode loop.
@@ -315,34 +342,60 @@ struct Out<'a> {
315
342
  base: usize,
316
343
  /// Next byte to write.
317
344
  at: usize,
345
+ /// The most this stream may write.
346
+ limit: usize,
318
347
  }
319
348
 
320
349
  impl Out<'_> {
350
+ /// Room for `extra` more bytes; false when the buffer cannot hold them
351
+ /// within the limit.
321
352
  #[inline(always)]
322
- fn ensure(&mut self, extra: usize) {
323
- if self.at + extra > self.buf.len() {
324
- let grown = (self.buf.len() * 2).max(self.at + extra + 256);
325
- self.buf.resize(grown, 0);
353
+ fn ensure(&mut self, extra: usize) -> bool {
354
+ self.at + extra <= self.buf.len() || self.grow(extra)
355
+ }
356
+
357
+ /// The end of the buffer at its fullest: the limit plus the 16 bytes a
358
+ /// chunked copy overshoots into.
359
+ fn ceiling(&self) -> usize {
360
+ self.base.saturating_add(self.limit).saturating_add(16)
361
+ }
362
+
363
+ /// Grows the buffer for `extra` more bytes, never past `ceiling`. Every
364
+ /// write comes here once the buffer is full, so the buffer itself holds
365
+ /// the limit for literals, matches and stored blocks alike, and costs
366
+ /// nothing while it has room.
367
+ #[cold]
368
+ #[inline(never)]
369
+ fn grow(&mut self, extra: usize) -> bool {
370
+ let ceiling = self.ceiling();
371
+ if self.at + extra > ceiling {
372
+ return false;
326
373
  }
374
+ let grown = (self.buf.len() * 2).max(self.at + extra + 256).min(ceiling);
375
+ self.buf.resize(grown, 0);
376
+ true
327
377
  }
328
378
 
379
+ /// Appends `byte`; false when that passes the limit.
329
380
  #[inline(always)]
330
- fn literal(&mut self, byte: u8) {
331
- self.ensure(1);
381
+ fn literal(&mut self, byte: u8) -> bool {
382
+ if !self.ensure(1) {
383
+ return false;
384
+ }
332
385
  self.buf[self.at] = byte;
333
386
  self.at += 1;
387
+ true
334
388
  }
335
389
 
336
- /// Appends `length` bytes copied from `distance` bytes back; false when
337
- /// the distance reaches before this stream's output. Copies run in 16-
338
- /// or 8-byte chunks that may overshoot `length`; the overshoot lands in
339
- /// slack that later output overwrites.
390
+ /// Appends `length` bytes copied from `distance` bytes back, a distance
391
+ /// the caller has checked; false when that passes the limit. Copies run
392
+ /// in 16- or 8-byte chunks that may overshoot `length`; the overshoot
393
+ /// lands in slack that later output overwrites.
340
394
  #[inline(always)]
341
395
  fn copy_match(&mut self, distance: usize, length: usize) -> bool {
342
- if distance > self.at - self.base {
396
+ if !self.ensure(length + 16) {
343
397
  return false;
344
398
  }
345
- self.ensure(length + 16);
346
399
  let start = self.at - distance;
347
400
  if distance >= 16 {
348
401
  let mut offset = 0;
@@ -386,18 +439,36 @@ impl Out<'_> {
386
439
  }
387
440
 
388
441
  /// Inflates a raw DEFLATE stream into `out` (appended); `expected` sizes the
389
- /// reservation. Trailing bytes after the final block are ignored.
442
+ /// reservation. Trailing bytes after the final block are ignored. The output
443
+ /// is held to `MAX_PART_BYTES` by what the stream actually produces, never by
444
+ /// a declared size.
390
445
  pub fn inflate(input: &[u8], expected: usize, out: &mut Vec<u8>) -> Result<()> {
446
+ inflate_within(input, expected, MAX_PART_BYTES, out)
447
+ }
448
+
449
+ /// `inflate` held to `limit` output bytes.
450
+ pub fn inflate_within(
451
+ input: &[u8],
452
+ expected: usize,
453
+ limit: usize,
454
+ out: &mut Vec<u8>,
455
+ ) -> Result<()> {
391
456
  let base = out.len();
392
457
  let most = input.len().saturating_mul(MAX_RATIO).saturating_add(64);
393
- out.resize(base + expected.min(most).max(64) + 64, 0);
394
458
  let mut out = Out {
395
459
  buf: out,
396
460
  base,
397
461
  at: base,
462
+ limit,
398
463
  };
464
+ let reserved = (expected.min(most).min(limit).max(64) + 64).min(out.ceiling() - base);
465
+ out.buf.resize(base + reserved, 0);
399
466
  let mut bits = Bits::new(input);
400
- let result = inflate_blocks(&mut bits, &mut out);
467
+ let result =
468
+ inflate_blocks(&mut bits, &mut out).and_then(|()| match out.at - out.base > limit {
469
+ true => Err(past_limit(limit)),
470
+ false => Ok(()),
471
+ });
401
472
  let written = out.at;
402
473
  out.buf.truncate(written);
403
474
  result
@@ -445,7 +516,9 @@ fn stored_block(bits: &mut Bits<'_>, out: &mut Out<'_>) -> Result<()> {
445
516
  if end > data.len() {
446
517
  return Err(truncated());
447
518
  }
448
- out.ensure(len);
519
+ if !out.ensure(len) {
520
+ return Err(past_limit(out.limit));
521
+ }
449
522
  out.buf[out.at..out.at + len].copy_from_slice(&data[start..end]);
450
523
  out.at += len;
451
524
  bits.pos = end;
@@ -524,7 +597,9 @@ fn decode_block(
524
597
  }
525
598
  let symbol = literals.decode(bits);
526
599
  if symbol < END_OF_BLOCK {
527
- out.literal(symbol as u8);
600
+ if !out.literal(symbol as u8) {
601
+ return Err(past_limit(out.limit));
602
+ }
528
603
  continue;
529
604
  }
530
605
  if symbol == END_OF_BLOCK {
@@ -545,9 +620,12 @@ fn decode_block(
545
620
  let distance =
546
621
  usize::from(DIST_BASE[distance_code]) + bits.take(u32::from(DIST_EXTRA[distance_code]));
547
622
  bits.check()?;
548
- if !out.copy_match(distance, length) {
623
+ if distance > out.at - out.base {
549
624
  return Err(invalid("distance reaches before the start of output"));
550
625
  }
626
+ if !out.copy_match(distance, length) {
627
+ return Err(past_limit(out.limit));
628
+ }
551
629
  }
552
630
  }
553
631
 
@@ -642,9 +720,10 @@ mod tests {
642
720
  buf: &mut vec![0u8; 64],
643
721
  base: 0,
644
722
  at: 0,
723
+ limit: usize::MAX,
645
724
  };
646
725
  for byte in b"abcdefghijklm" {
647
- out.literal(*byte);
726
+ assert!(out.literal(*byte));
648
727
  }
649
728
  assert!(out.copy_match(10, 12));
650
729
  assert_eq!(&out.buf[..25], b"abcdefghijklmdefghijklmde");
@@ -711,4 +790,118 @@ mod tests {
711
790
  assert!(code.build(&[1, 1], DISTANCE_BITS).is_ok());
712
791
  assert!(code.build(&[1], DISTANCE_BITS).is_ok());
713
792
  }
793
+
794
+ #[test]
795
+ fn a_stream_inflating_past_the_ratio_is_refused() {
796
+ // 32 MiB of zeros deflates about 1000:1, far past any real part.
797
+ let stream = deflate(&vec![0u8; 32 << 20], Compression::default());
798
+ let mut out = Vec::new();
799
+ let error = inflate_within(&stream, 0, ratio_limit(stream.len()), &mut out).unwrap_err();
800
+ assert!(error.to_string().contains("inflate limit"), "{error}");
801
+ }
802
+
803
+ #[test]
804
+ fn a_stream_inflating_past_the_part_limit_is_refused() {
805
+ let stream = deflate(&noise(4096, 7).repeat(64), Compression::default());
806
+ let mut out = Vec::new();
807
+ let error = inflate_within(&stream, 0, 4096, &mut out).unwrap_err();
808
+ assert!(error.to_string().contains("inflate limit"), "{error}");
809
+ }
810
+
811
+ #[test]
812
+ fn a_small_part_is_exempt_from_the_ratio() {
813
+ // 512 KiB of zeros inflates well past the ratio but under the exemption.
814
+ let stream = deflate(&vec![0u8; 512 << 10], Compression::default());
815
+ let mut out = Vec::new();
816
+ inflate_within(&stream, 0, ratio_limit(stream.len()), &mut out).unwrap();
817
+ assert_eq!(out, vec![0u8; 512 << 10]);
818
+ }
819
+
820
+ /// Bits written least significant first, as DEFLATE reads them.
821
+ #[derive(Default)]
822
+ struct BitWriter {
823
+ bytes: Vec<u8>,
824
+ pending: u64,
825
+ used: u32,
826
+ }
827
+
828
+ impl BitWriter {
829
+ fn put(&mut self, value: u64, width: u32) {
830
+ self.pending |= value << self.used;
831
+ self.used += width;
832
+ while self.used >= 8 {
833
+ self.bytes.push(self.pending as u8);
834
+ self.pending >>= 8;
835
+ self.used -= 8;
836
+ }
837
+ }
838
+
839
+ /// A run of `count` zero code lengths, as code length symbols 18
840
+ /// (runs of 11 to 138).
841
+ fn zero_lengths(&mut self, mut count: u64) {
842
+ while count > 0 {
843
+ let run = count.min(138);
844
+ self.put(1, 1);
845
+ self.put(run - 11, 7);
846
+ count -= run;
847
+ }
848
+ }
849
+
850
+ fn finish(mut self) -> Vec<u8> {
851
+ if self.used > 0 {
852
+ self.bytes.push(self.pending as u8);
853
+ }
854
+ self.bytes
855
+ }
856
+ }
857
+
858
+ /// One dynamic block whose only codes are `a` and end-of-block, one bit
859
+ /// each: `count` literals in `count` bits, with no match at all.
860
+ fn literal_stream(count: usize) -> Vec<u8> {
861
+ let mut bits = BitWriter::default();
862
+ const FINAL_BLOCK: u64 = 1;
863
+ const DYNAMIC_CODES: u64 = 2;
864
+ const MINIMUM_LITERAL_CODES: u64 = 0;
865
+ const MINIMUM_DISTANCE_CODES: u64 = 0;
866
+ const CODE_LENGTH_CODES_SENT: u64 = 14;
867
+ bits.put(FINAL_BLOCK, 1);
868
+ bits.put(DYNAMIC_CODES, 2);
869
+ bits.put(MINIMUM_LITERAL_CODES, 5);
870
+ bits.put(MINIMUM_DISTANCE_CODES, 5);
871
+ bits.put(CODE_LENGTH_CODES_SENT, 4);
872
+ for symbol in &CODE_LENGTH_ORDER[..18] {
873
+ bits.put(u64::from(*symbol == 1 || *symbol == 18), 3);
874
+ }
875
+ bits.zero_lengths(u64::from(b'a'));
876
+ bits.put(0, 1);
877
+ bits.zero_lengths(u64::from(END_OF_BLOCK) - u64::from(b'a') - 1);
878
+ bits.put(0, 1);
879
+ bits.put(0, 1);
880
+ for _ in 0..count {
881
+ bits.put(0, 1);
882
+ }
883
+ bits.put(1, 1);
884
+ bits.finish()
885
+ }
886
+
887
+ #[test]
888
+ fn a_stream_of_literals_alone_is_held_to_the_limit() {
889
+ // One-bit literal codes inflate 8x without a single match.
890
+ let stream = literal_stream(1 << 16);
891
+ let mut out = Vec::new();
892
+ inflate_within(&stream, 0, usize::MAX, &mut out).unwrap();
893
+ assert_eq!(out, vec![b'a'; 1 << 16]);
894
+ out.clear();
895
+ let error = inflate_within(&stream, 0, 4096, &mut out).unwrap_err();
896
+ assert!(error.to_string().contains("inflate limit"), "{error}");
897
+ }
898
+
899
+ #[test]
900
+ fn the_limit_is_exact_for_literals() {
901
+ for (count, limit, accepted) in [(4096, 4096, true), (4097, 4096, false), (8, 4, false)] {
902
+ let mut out = Vec::new();
903
+ let result = inflate_within(&literal_stream(count), 0, limit, &mut out);
904
+ assert_eq!(result.is_ok(), accepted, "{count} literals, limit {limit}");
905
+ }
906
+ }
714
907
  }