├── .editorconfig ├── .github └── workflows │ └── crystar-ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── Crystar.html ├── Crystar │ ├── ErrWriteTooLong.html │ ├── Error.html │ ├── Format.html │ ├── Header.html │ ├── Reader.html │ ├── Reader │ │ └── FileReader.html │ ├── SparseDatas.html │ ├── SparseEntry.html │ ├── SparseHoles.html │ └── Writer.html ├── css │ └── style.css ├── index.html ├── index.json ├── js │ └── doc.js └── search-index.js ├── example ├── compressed.cr └── example.cr ├── shard.yml ├── spec ├── crystar_spec.cr ├── reader_spec.cr ├── spec_helper.cr ├── testdata │ ├── gnu-incremental.tar │ ├── gnu-long-nul.tar │ ├── gnu-not-utf8.tar │ ├── gnu-utf8.tar │ ├── gnu.tar │ ├── neg-size.tar │ ├── pax-bad-hdr-file.tar │ ├── pax-bad-mtime-file.tar │ ├── pax-multi-hdrs.tar │ ├── pax-nil-sparse-data.tar │ ├── pax-nul-path.tar │ ├── pax-nul-xattrs.tar │ ├── pax-pos-size-file.tar │ ├── pax-records.tar │ ├── pax.tar │ ├── small.txt │ ├── sparse-formats.tar │ ├── star.tar │ ├── trailing-slash.tar │ └── v7.tar └── writer_spec.cr └── src ├── crystar.cr └── tar ├── format.cr ├── header.cr ├── helper.cr ├── reader.cr └── writer.cr /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.cr] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/workflows/crystar-ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | schedule: 9 | - cron: '0 6 * * 6' 10 | jobs: 11 | build: 12 | timeout-minutes: 30 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: [ubuntu-latest, macos-latest] 17 | runs-on: ${{ matrix.os }} 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Install Crystal 22 | uses: oprypin/install-crystal@v1.3.0 23 | - name: Install dependencies 24 | run: shards install 25 | - name: Run tests 26 | run: crystal spec 27 | - name: Generate docs 28 | run: crystal doc 29 | - name: Deploy 30 | uses: JamesIves/github-pages-deploy-action@3.7.1 31 | with: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | BRANCH: gh-pages 34 | FOLDER: docs 35 | SINGLE_COMMIT: true 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /docs/ 2 | /lib/ 3 | /bin/ 4 | /.shards/ 5 | *.dwarf 6 | /.vscode/ 7 | # Libraries don't need dependency lock 8 | # Dependencies will be locked in applications that use them 9 | /shard.lock 10 | .ameba.yml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Ali Naqvi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Crystal Tar (Crystar) 2 | ![CI](https://github.com/naqvis/crystar/workflows/CI/badge.svg) 3 | [![GitHub release](https://img.shields.io/github/release/naqvis/crystar.svg)](https://github.com/naqvis/crystar/releases) 4 | [![Docs](https://img.shields.io/badge/docs-available-brightgreen.svg)](https://naqvis.github.io/crystar/) 5 | 6 | Shard `Crystar` implements access to tar archives. 7 | 8 | *No external library needed.* This is written in **pure Crystal**. 9 | 10 | Tape archives (tar) are a file format for storing a sequence of files that can be read and written in a streaming manner. This shard aims to cover most variations of the format, including those produced by **GNU** and **BSD** tar tools. 11 | 12 | This module is mostly based on [`Tar`](https://golang.google.cn/pkg/archive/tar/) package implementation of [Golang](http://golang.org/) 13 | 14 | 15 | Format represents the tar archive format. 16 | 17 | The original tar format was introduced in Unix V7. 18 | Since then, there have been multiple competing formats attempting to 19 | standardize or extend the **V7** format to overcome its limitations. 20 | The most common formats are the **USTAR**, **PAX**, and **GNU** formats, 21 | each with their own advantages and limitations. 22 | 23 | The following table captures the capabilities of each format: 24 | 25 | | USTAR | PAX | GNU 26 | ------------------+--------+-----------+---------- 27 | Name | 256B | unlimited | unlimited 28 | Linkname | 100B | unlimited | unlimited 29 | Size | uint33 | unlimited | uint89 30 | Mode | uint21 | uint21 | uint57 31 | Uid/Gid | uint21 | unlimited | uint57 32 | Uname/Gname | 32B | unlimited | 32B 33 | ModTime | uint33 | unlimited | int89 34 | AccessTime | n/a | unlimited | int89 35 | ChangeTime | n/a | unlimited | int89 36 | Devmajor/Devminor | uint21 | uint21 | uint57 37 | ------------------+--------+-----------+---------- 38 | string encoding | ASCII | UTF-8 | binary 39 | sub-second times | no | yes | no 40 | sparse files | no | yes | yes 41 | 42 | The table's upper portion shows the Header fields, where each format reports 43 | the maximum number of bytes allowed for each string field and 44 | the integer type used to store each numeric field 45 | (where timestamps are stored as the number of seconds since the Unix epoch). 46 | 47 | The table's lower portion shows specialized features of each format, 48 | such as supported string encodings, support for sub-second timestamps, 49 | or support for sparse files. 50 | 51 | The `Writer` currently provides **no support** for _sparse files_. 52 | 53 | ## Installation 54 | 55 | 1. Add the dependency to your `shard.yml`: 56 | 57 | ```yaml 58 | dependencies: 59 | crystar: 60 | github: naqvis/crystar 61 | ``` 62 | 63 | 2. Run `shards install` 64 | 65 | ## Usage 66 | 67 | ```crystal 68 | require "crystar" 69 | ``` 70 | 71 | `Crystar` module contains readers and writers for tar archive. 72 | Tape archives (tar) are a file format for storing a sequence of files that can be read and written in a streaming manner. 73 | This module aims to cover most variations of the format, including those produced by GNU and BSD tar tools. 74 | 75 | ## Sample Usage 76 | ```crystal 77 | files = [ 78 | {"readme.txt", "This archive contains some text files."}, 79 | {"minerals.txt", "Mineral names:\nalunite\nchromium\nvlasovite"}, 80 | {"todo.txt", "Get crystal mining license."}, 81 | ] 82 | 83 | buf = IO::Memory.new 84 | Crystar::Writer.open(buf) do |tw| 85 | files.each do |f| 86 | hdr = Crystar::Header.new( 87 | name: f[0], 88 | mode: 0o600_i64, 89 | size: f[1].size.to_i64 90 | ) 91 | tw.write_header(hdr) 92 | tw.write(f[1].to_slice) 93 | end 94 | end 95 | #Open and iterate through the files in the archive 96 | buf.pos = 0 97 | Crystar::Reader.open(buf) do |tar| 98 | tar.each_entry do |entry| 99 | p "Contents of #{entry.name}" 100 | IO.copy entry.io, STDOUT 101 | p "\n" 102 | end 103 | end 104 | ``` 105 | 106 | Supports compressed archives as well. 107 | 108 | ```crystal 109 | require "gzip" 110 | 111 | File.open("test.tar.gz") do |file| 112 | Gzip::Reader.open(file) do |gzip| 113 | Crystar::Reader.open(gzip) do |tar| 114 | tar.each_entry do |entry| 115 | p "Contents of #{entry.name}" 116 | IO.copy entry.io, STDOUT 117 | end 118 | end 119 | end 120 | end 121 | ``` 122 | 123 | Refer to `Crystar::Reader` and `Crystar::Writer` module for documentation on detailed usage. 124 | 125 | # Development 126 | 127 | To run all tests: 128 | 129 | ``` 130 | crystal spec 131 | ``` 132 | 133 | # Contributing 134 | 135 | 1. Fork it () 136 | 2. Create your feature branch (`git checkout -b my-new-feature`) 137 | 3. Commit your changes (`git commit -am 'Add some feature'`) 138 | 4. Push to the branch (`git push origin my-new-feature`) 139 | 5. Create a new Pull Request 140 | 141 | # Contributors 142 | 143 | - [Ali Naqvi](https://github.com/naqvis) - creator and maintainer 144 | -------------------------------------------------------------------------------- /docs/Crystar/ErrWriteTooLong.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | Crystar::ErrWriteTooLong - github.com/naqvis/crystar 18 | 19 | 20 | 21 | 108 | 109 | 110 |
111 |

112 | 113 | class Crystar::ErrWriteTooLong 114 | 115 |

116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 |

Defined in:

137 | 138 | 139 | 140 | crystar.cr 141 | 142 | 143 |
144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 |
159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 |
201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 |
211 | 212 | 213 | 214 | -------------------------------------------------------------------------------- /docs/Crystar/Error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | Crystar::Error - github.com/naqvis/crystar 18 | 19 | 20 | 21 | 108 | 109 | 110 |
111 |

112 | 113 | class Crystar::Error 114 | 115 |

116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 |

Overview

124 | 125 |

Common Crystar exceptions

126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 |

Direct Known Subclasses

136 | 141 | 142 | 143 | 144 | 145 | 146 | 147 |

Defined in:

148 | 149 | 150 | 151 | crystar.cr 152 | 153 | 154 |
155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 |
170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 |
202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 |
212 | 213 | 214 | 215 | -------------------------------------------------------------------------------- /docs/Crystar/Format.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | Crystar::Format - github.com/naqvis/crystar 18 | 19 | 20 | 21 | 108 | 109 | 110 |
111 |

112 | 113 | enum Crystar::Format 114 | 115 |

116 | 117 | 118 | 119 | 120 | 121 |

Overview

122 | 123 |

Various Crystar formats

124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 |

Defined in:

139 | 140 | 141 | 142 | tar/format.cr 143 | 144 | 145 |
146 | 147 | 148 | 149 | 150 | 151 |

Enum Members

152 | 153 |
154 | 155 |
156 | V7 = 1 157 |
158 | 159 |
160 |

The format of the original Unix V7 tar tool prior to standardization.

161 |
162 | 163 | 164 |
165 | USTAR = 2 166 |
167 | 168 |
169 |

USTAR represents the USTAR header format defined in POSIX.1-1988.

170 | 171 |

While this format is compatible with most tar readers, 172 | the format has several limitations making it unsuitable for some usages. 173 | Most notably, it cannot support sparse files, files larger than 8GiB, 174 | filenames larger than 256 characters, and non-ASCII filenames.

175 | 176 |

Reference: 177 | http:#pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html#tag_20_92_13_06

178 |
179 | 180 | 181 |
182 | PAX = 4 183 |
184 | 185 |
186 |

PAX represents the PAX header format defined in POSIX.1-2001.

187 | 188 |

PAX extends USTAR by writing a special file with Typeflag TypeXHeader 189 | preceding the original header. This file contains a set of key-value 190 | records, which are used to overcome USTAR's shortcomings, in addition to 191 | providing the ability to have sub-second resolution for timestamps.

192 | 193 |

Some newer formats add their own extensions to PAX by defining their 194 | own keys and assigning certain semantic meaning to the associated values. 195 | For example, sparse file support in PAX is implemented using keys 196 | defined by the GNU manual (e.g., "GNU.sparse.map").

197 | 198 |

Reference: 199 | http:#pubs.opengroup.org/onlinepubs/009695399/utilities/pax.html

200 |
201 | 202 | 203 |
204 | GNU = 8 205 |
206 | 207 |
208 |

GNU represents the GNU header format.

209 | 210 |

The GNU header format is older than the USTAR and PAX standards and 211 | is not compatible with them. The GNU format supports 212 | arbitrary file sizes, filenames of arbitrary encoding and length, 213 | sparse files, and other features.

214 | 215 |

It is recommended that PAX be chosen over GNU unless the target 216 | application can only parse GNU formatted archives.

217 | 218 |

Reference: 219 | https:#www.gnu.org/softwarecrystar/manual/html_node/Standard.html

220 |
221 | 222 | 223 |
224 | STAR = 16 225 |
226 | 227 |
228 |

Schily's tar format, which is incompatible with USTAR. 229 | This does not cover STAR extensions to the PAX format; these fall under 230 | the PAX format.

231 |
232 | 233 | 234 |
235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 |

Instance Method Summary

243 | 296 | 297 | 298 | 299 | 300 | 301 |
302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 |
344 | 345 | 346 | 347 | 348 | 349 | 350 |

Instance Method Detail

351 | 352 |
353 |
354 | 355 | def gnu? 356 | 357 | # 358 |
359 | 360 |
361 |
362 | 363 | [View source] 364 | 365 |
366 |
367 | 368 |
369 |
370 | 371 | def has(f2 : self) 372 | 373 | # 374 |
375 | 376 |
377 |
378 | 379 | [View source] 380 | 381 |
382 |
383 | 384 |
385 |
386 | 387 | def may_only_be(f2) 388 | 389 | # 390 |
391 | 392 |
393 |
394 | 395 | [View source] 396 | 397 |
398 |
399 | 400 |
401 |
402 | 403 | def maybe(f2 : self) 404 | 405 | # 406 |
407 | 408 |
409 |
410 | 411 | [View source] 412 | 413 |
414 |
415 | 416 |
417 |
418 | 419 | def must_not_be(f2) 420 | 421 | # 422 |
423 | 424 |
425 |
426 | 427 | [View source] 428 | 429 |
430 |
431 | 432 |
433 |
434 | 435 | def none? 436 | 437 | # 438 |
439 | 440 |
441 |
442 | 443 | [View source] 444 | 445 |
446 |
447 | 448 |
449 |
450 | 451 | def pax? 452 | 453 | # 454 |
455 | 456 |
457 |
458 | 459 | [View source] 460 | 461 |
462 |
463 | 464 |
465 |
466 | 467 | def star? 468 | 469 | # 470 |
471 | 472 |
473 |
474 | 475 | [View source] 476 | 477 |
478 |
479 | 480 |
481 |
482 | 483 | def ustar? 484 | 485 | # 486 |
487 | 488 |
489 |
490 | 491 | [View source] 492 | 493 |
494 |
495 | 496 |
497 |
498 | 499 | def v7? 500 | 501 | # 502 |
503 | 504 |
505 |
506 | 507 | [View source] 508 | 509 |
510 |
511 | 512 | 513 | 514 | 515 | 516 |
517 | 518 | 519 | 520 | -------------------------------------------------------------------------------- /docs/Crystar/Reader.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | Crystar::Reader - github.com/naqvis/crystar 18 | 19 | 20 | 21 | 108 | 109 | 110 |
111 |

112 | 113 | class Crystar::Reader 114 | 115 |

116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 |

Overview

124 | 125 |

Reads tar file entries sequentially from an IO.

126 | 127 |

Example

128 | 129 |
require "tar"
130 | 
131 | File.open("./file.tar") do |file|
132 |   Crystar::Reader.open(file) do |tar|
133 |     tar.each_entry do |entry|
134 |       p entry.name
135 |       p entry.file?
136 |       p entry.dir?
137 |       p entry.io.gets_to_end
138 |     end
139 |   end
140 | end
141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 |

Defined in:

156 | 157 | 158 | 159 | tar/reader.cr 160 | 161 | 162 |
163 | 164 | 165 | 166 | 167 | 168 | 169 |

Constructors

170 | 187 | 188 | 189 | 190 |

Class Method Summary

191 | 208 | 209 | 210 | 211 |

Instance Method Summary

212 | 257 | 258 | 259 | 260 | 261 | 262 |
263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 |
285 | 286 | 287 |

Constructor Detail

288 | 289 |
290 |
291 | 292 | def self.new(io : IO, sync_close = false) 293 | 294 | # 295 |
296 | 297 |

Creates a new reader from the given io.

298 | 299 |
300 |
301 | 302 | [View source] 303 | 304 |
305 |
306 | 307 |
308 |
309 | 310 | def self.new(filename : String) 311 | 312 | # 313 |
314 | 315 |

Creates a new reader from the given filename.

316 | 317 |
318 |
319 | 320 | [View source] 321 | 322 |
323 |
324 | 325 | 326 | 327 | 328 |

Class Method Detail

329 | 330 |
331 |
332 | 333 | def self.open(io : IO, sync_close = false, &block) 334 | 335 | # 336 |
337 | 338 |

Creates a new reader from the given io, yields it to the given block, 339 | and closes it at the end.

340 | 341 |
342 |
343 | 344 | [View source] 345 | 346 |
347 |
348 | 349 |
350 |
351 | 352 | def self.open(filename : String, &block) 353 | 354 | # 355 |
356 | 357 |

Creates a new reader from the given filename, yields it to the given block, 358 | and closes it at the end.

359 | 360 |
361 |
362 | 363 | [View source] 364 | 365 |
366 |
367 | 368 | 369 | 370 | 371 |

Instance Method Detail

372 | 373 |
374 |
375 | 376 | def close 377 | 378 | # 379 |
380 | 381 |

Closes Crystar reader

382 | 383 |
384 |
385 | 386 | [View source] 387 | 388 |
389 |
390 | 391 |
392 |
393 | 394 | def closed? : Bool 395 | 396 | # 397 |
398 | 399 |

Returns true if this reader is closed.

400 | 401 |
402 |
403 | 404 | [View source] 405 | 406 |
407 |
408 | 409 |
410 |
411 | 412 | def each_entry(&block) 413 | 414 | # 415 |
416 | 417 |

Yields each entry in the zip to the given block.

418 | 419 |
420 |
421 | 422 | [View source] 423 | 424 |
425 |
426 | 427 |
428 |
429 | 430 | def next_entry : Header? 431 | 432 | # 433 |
434 | 435 |

next_entry advances to the next entry in the tar archive. 436 | The Header.Size determines how many bytes can be read for the next file. 437 | Any remaining data in the current file is automatically discarded.

438 | 439 |

EOF is returned at the end of the input.

440 | 441 |
442 |
443 | 444 | [View source] 445 | 446 |
447 |
448 | 449 |
450 |
451 | 452 | def sync_close=(sync_close) 453 | 454 | # 455 |
456 | 457 |

Whether to close the enclosed IO when closing this reader.

458 | 459 |
460 |
461 | 462 | [View source] 463 | 464 |
465 |
466 | 467 |
468 |
469 | 470 | def sync_close? : Bool 471 | 472 | # 473 |
474 | 475 |

Whether to close the enclosed IO when closing this reader.

476 | 477 |
478 |
479 | 480 | [View source] 481 | 482 |
483 |
484 | 485 | 486 | 487 | 488 | 489 |
490 | 491 | 492 | 493 | -------------------------------------------------------------------------------- /docs/Crystar/Reader/FileReader.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | Crystar::Reader::FileReader - github.com/naqvis/crystar 18 | 19 | 20 | 21 | 108 | 109 | 110 |
111 |

112 | 113 | abstract class Crystar::Reader::FileReader 114 | 115 |

116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 |

Included Modules

128 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 |

Defined in:

144 | 145 | 146 | 147 | tar/reader.cr 148 | 149 | 150 |
151 | 152 | 153 | 154 | 155 | 156 | 157 |

Constructors

158 | 166 | 167 | 168 | 169 | 170 | 171 |

Instance Method Summary

172 | 195 | 196 | 197 | 198 |

Macro Summary

199 | 207 | 208 | 209 | 210 |
211 | 212 | 213 | 214 |

Instance methods inherited from module Crystar::FileState

215 | 216 | 217 | 218 | logical_remaining : Int64 219 | logical_remaining, 220 | 221 | 222 | 223 | physical_remaining : Int64 224 | physical_remaining 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 |
267 | 268 | 269 |

Constructor Detail

270 | 271 |
272 |
273 | 274 | def self.new(io) 275 | 276 | # 277 |
278 | 279 |
280 |
281 | 282 | [View source] 283 | 284 |
285 |
286 | 287 | 288 | 289 | 290 | 291 | 292 |

Instance Method Detail

293 | 294 |
295 |
296 | 297 | def io : IO 298 | 299 | # 300 |
301 | 302 |
303 |
304 | 305 | [View source] 306 | 307 |
308 |
309 | 310 |
311 |
312 | abstract 313 | def read(b : Bytes) : Int 314 | 315 | # 316 |
317 | 318 |
319 |
320 | 321 | [View source] 322 | 323 |
324 |
325 | 326 |
327 |
328 | 329 | def write(b : Bytes) 330 | 331 | # 332 |
333 | 334 |
335 |
336 | 337 | [View source] 338 | 339 |
340 |
341 | 342 |
343 |
344 | abstract 345 | def write_to(w : IO) : Int 346 | 347 | # 348 |
349 | 350 |
351 |
352 | 353 | [View source] 354 | 355 |
356 |
357 | 358 | 359 | 360 | 361 |

Macro Detail

362 | 363 |
364 |
365 | 366 | macro method_missing(call) 367 | 368 | # 369 |
370 | 371 |
372 |
373 | 374 | [View source] 375 | 376 |
377 |
378 | 379 | 380 | 381 |
382 | 383 | 384 | 385 | -------------------------------------------------------------------------------- /docs/Crystar/SparseDatas.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | Crystar::SparseDatas - github.com/naqvis/crystar 18 | 19 | 20 | 21 | 108 | 109 | 110 |
111 |

112 | 113 | alias Crystar::SparseDatas 114 | 115 |

116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 |

Alias Definition

124 | Array(Crystar::SparseEntry) 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 |

Defined in:

138 | 139 | 140 | 141 | tar/header.cr 142 | 143 | 144 |
145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 |
160 | 161 |
162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 |
172 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /docs/Crystar/SparseEntry.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | Crystar::SparseEntry - github.com/naqvis/crystar 18 | 19 | 20 | 21 | 108 | 109 | 110 |
111 |

112 | 113 | struct Crystar::SparseEntry 114 | 115 |

116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 |

Overview

124 | 125 |

SparseEntry represents a Length-sized fragment at Offset in the file

126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 |

Defined in:

141 | 142 | 143 | 144 | tar/header.cr 145 | 146 | 147 |
148 | 149 | 150 | 151 | 152 | 153 | 154 |

Constructors

155 | 163 | 164 | 165 | 166 |

Class Method Summary

167 | 175 | 176 | 177 | 178 |

Instance Method Summary

179 | 227 | 228 | 229 | 230 | 231 | 232 |
233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 |
265 | 266 | 267 |

Constructor Detail

268 | 269 |
270 |
271 | 272 | def self.new(offset : Int64, length : Int64) 273 | 274 | # 275 |
276 | 277 |
278 |
279 | 280 | [View source] 281 | 282 |
283 |
284 | 285 | 286 | 287 | 288 |

Class Method Detail

289 | 290 |
291 |
292 | 293 | def self.empty 294 | 295 | # 296 |
297 | 298 |
299 |
300 | 301 | [View source] 302 | 303 |
304 |
305 | 306 | 307 | 308 | 309 |

Instance Method Detail

310 | 311 |
312 |
313 | 314 | def ==(other : self) 315 | 316 | # 317 |
318 | 319 |
320 |
321 | 322 |
323 |
324 | 325 |
326 |
327 | 328 | def clone 329 | 330 | # 331 |
332 | 333 |
334 |
335 | 336 | [View source] 337 | 338 |
339 |
340 | 341 |
342 |
343 | 344 | def copy_with(offset _offset = @offset, length _length = @length) 345 | 346 | # 347 |
348 | 349 |
350 |
351 | 352 | [View source] 353 | 354 |
355 |
356 | 357 |
358 |
359 | 360 | def end_of_offset 361 | 362 | # 363 |
364 | 365 |
366 |
367 | 368 | [View source] 369 | 370 |
371 |
372 | 373 |
374 |
375 | 376 | def hash(hasher) 377 | 378 | # 379 |
380 | 381 |
382 |
383 | 384 |
385 |
386 | 387 |
388 |
389 | 390 | def length : Int64 391 | 392 | # 393 |
394 | 395 |
396 |
397 | 398 | [View source] 399 | 400 |
401 |
402 | 403 |
404 |
405 | 406 | def length=(length) 407 | 408 | # 409 |
410 | 411 |
412 |
413 | 414 | [View source] 415 | 416 |
417 |
418 | 419 |
420 |
421 | 422 | def offset : Int64 423 | 424 | # 425 |
426 | 427 |
428 |
429 | 430 | [View source] 431 | 432 |
433 |
434 | 435 |
436 |
437 | 438 | def offset=(offset) 439 | 440 | # 441 |
442 | 443 |
444 |
445 | 446 | [View source] 447 | 448 |
449 |
450 | 451 | 452 | 453 | 454 | 455 |
456 | 457 | 458 | 459 | -------------------------------------------------------------------------------- /docs/Crystar/SparseHoles.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | Crystar::SparseHoles - github.com/naqvis/crystar 18 | 19 | 20 | 21 | 108 | 109 | 110 |
111 |

112 | 113 | alias Crystar::SparseHoles 114 | 115 |

116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 |

Alias Definition

124 | Array(Crystar::SparseEntry) 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 |

Defined in:

138 | 139 | 140 | 141 | tar/header.cr 142 | 143 | 144 |
145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 |
160 | 161 |
162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 |
172 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /docs/css/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | background: #FFFFFF; 3 | position: relative; 4 | margin: 0; 5 | padding: 0; 6 | width: 100%; 7 | height: 100%; 8 | overflow: hidden; 9 | } 10 | 11 | body { 12 | font-family: "Avenir", "Tahoma", "Lucida Sans", "Lucida Grande", Verdana, Arial, sans-serif; 13 | color: #333; 14 | line-height: 1.5; 15 | } 16 | 17 | a { 18 | color: #263F6C; 19 | } 20 | 21 | a:visited { 22 | color: #112750; 23 | } 24 | 25 | h1, h2, h3, h4, h5, h6 { 26 | margin: 35px 0 25px; 27 | color: #444444; 28 | } 29 | 30 | h1.type-name { 31 | color: #47266E; 32 | margin: 20px 0 30px; 33 | background-color: #F8F8F8; 34 | padding: 10px 12px; 35 | border: 1px solid #EBEBEB; 36 | border-radius: 2px; 37 | } 38 | 39 | h2 { 40 | border-bottom: 1px solid #E6E6E6; 41 | padding-bottom: 5px; 42 | } 43 | 44 | body { 45 | display: flex; 46 | } 47 | 48 | .sidebar, .main-content { 49 | overflow: auto; 50 | } 51 | 52 | .sidebar { 53 | width: 30em; 54 | color: #F8F4FD; 55 | background-color: #2E1052; 56 | padding: 0 0 30px; 57 | box-shadow: inset -3px 0 4px rgba(0,0,0,.35); 58 | line-height: 1.2; 59 | } 60 | 61 | .sidebar .search-box { 62 | padding: 8px 9px; 63 | } 64 | 65 | .sidebar input { 66 | display: block; 67 | box-sizing: border-box; 68 | margin: 0; 69 | padding: 5px; 70 | font: inherit; 71 | font-family: inherit; 72 | line-height: 1.2; 73 | width: 100%; 74 | border: 0; 75 | outline: 0; 76 | border-radius: 2px; 77 | box-shadow: 0px 3px 5px rgba(0,0,0,.25); 78 | transition: box-shadow .12s; 79 | } 80 | 81 | .sidebar input:focus { 82 | box-shadow: 0px 5px 6px rgba(0,0,0,.5); 83 | } 84 | 85 | .sidebar input::-webkit-input-placeholder { /* Chrome/Opera/Safari */ 86 | color: #C8C8C8; 87 | font-size: 14px; 88 | text-indent: 2px; 89 | } 90 | 91 | .sidebar input::-moz-placeholder { /* Firefox 19+ */ 92 | color: #C8C8C8; 93 | font-size: 14px; 94 | text-indent: 2px; 95 | } 96 | 97 | .sidebar input:-ms-input-placeholder { /* IE 10+ */ 98 | color: #C8C8C8; 99 | font-size: 14px; 100 | text-indent: 2px; 101 | } 102 | 103 | .sidebar input:-moz-placeholder { /* Firefox 18- */ 104 | color: #C8C8C8; 105 | font-size: 14px; 106 | text-indent: 2px; 107 | } 108 | 109 | .sidebar ul { 110 | margin: 0; 111 | padding: 0; 112 | list-style: none outside; 113 | } 114 | 115 | .sidebar li { 116 | display: block; 117 | position: relative; 118 | } 119 | 120 | .types-list li.hide { 121 | display: none; 122 | } 123 | 124 | .sidebar a { 125 | text-decoration: none; 126 | color: inherit; 127 | transition: color .14s; 128 | } 129 | .types-list a { 130 | display: block; 131 | padding: 5px 15px 5px 30px; 132 | } 133 | 134 | .types-list { 135 | display: block; 136 | } 137 | 138 | .sidebar a:focus { 139 | outline: 1px solid #D1B7F1; 140 | } 141 | 142 | .types-list a { 143 | padding: 5px 15px 5px 30px; 144 | } 145 | 146 | .sidebar .current > a, 147 | .sidebar a:hover { 148 | color: #866BA6; 149 | } 150 | 151 | .repository-links { 152 | padding: 5px 15px 5px 30px; 153 | } 154 | 155 | .types-list li ul { 156 | overflow: hidden; 157 | height: 0; 158 | max-height: 0; 159 | transition: 1s ease-in-out; 160 | } 161 | 162 | .types-list li.parent { 163 | padding-left: 30px; 164 | } 165 | 166 | .types-list li.parent::before { 167 | box-sizing: border-box; 168 | content: "▼"; 169 | display: block; 170 | width: 30px; 171 | height: 30px; 172 | position: absolute; 173 | top: 0; 174 | left: 0; 175 | text-align: center; 176 | color: white; 177 | font-size: 8px; 178 | line-height: 30px; 179 | transform: rotateZ(-90deg); 180 | cursor: pointer; 181 | transition: .2s linear; 182 | } 183 | 184 | 185 | .types-list li.parent > a { 186 | padding-left: 0; 187 | } 188 | 189 | .types-list li.parent.open::before { 190 | transform: rotateZ(0); 191 | } 192 | 193 | .types-list li.open > ul { 194 | height: auto; 195 | max-height: 1000em; 196 | } 197 | 198 | .main-content { 199 | padding: 0 30px 30px 30px; 200 | width: 100%; 201 | } 202 | 203 | .kind { 204 | font-size: 60%; 205 | color: #866BA6; 206 | } 207 | 208 | .superclass-hierarchy { 209 | margin: -15px 0 30px 0; 210 | padding: 0; 211 | list-style: none outside; 212 | font-size: 80%; 213 | } 214 | 215 | .superclass-hierarchy .superclass { 216 | display: inline-block; 217 | margin: 0 7px 0 0; 218 | padding: 0; 219 | } 220 | 221 | .superclass-hierarchy .superclass + .superclass::before { 222 | content: "<"; 223 | margin-right: 7px; 224 | } 225 | 226 | .other-types-list li { 227 | display: inline-block; 228 | } 229 | 230 | .other-types-list, 231 | .list-summary { 232 | margin: 0 0 30px 0; 233 | padding: 0; 234 | list-style: none outside; 235 | } 236 | 237 | .entry-const { 238 | font-family: Menlo, Monaco, Consolas, 'Courier New', Courier, monospace; 239 | } 240 | 241 | .entry-const code { 242 | white-space: pre-wrap; 243 | } 244 | 245 | .entry-summary { 246 | padding-bottom: 4px; 247 | } 248 | 249 | .superclass-hierarchy .superclass a, 250 | .other-type a, 251 | .entry-summary .signature { 252 | padding: 4px 8px; 253 | margin-bottom: 4px; 254 | display: inline-block; 255 | background-color: #f8f8f8; 256 | color: #47266E; 257 | border: 1px solid #f0f0f0; 258 | text-decoration: none; 259 | border-radius: 3px; 260 | font-family: Menlo, Monaco, Consolas, 'Courier New', Courier, monospace; 261 | transition: background .15s, border-color .15s; 262 | } 263 | 264 | .superclass-hierarchy .superclass a:hover, 265 | .other-type a:hover, 266 | .entry-summary .signature:hover { 267 | background: #D5CAE3; 268 | border-color: #624288; 269 | } 270 | 271 | .entry-summary .summary { 272 | padding-left: 32px; 273 | } 274 | 275 | .entry-summary .summary p { 276 | margin: 12px 0 16px; 277 | } 278 | 279 | .entry-summary a { 280 | text-decoration: none; 281 | } 282 | 283 | .entry-detail { 284 | padding: 30px 0; 285 | } 286 | 287 | .entry-detail .signature { 288 | position: relative; 289 | padding: 5px 15px; 290 | margin-bottom: 10px; 291 | display: block; 292 | border-radius: 5px; 293 | background-color: #f8f8f8; 294 | color: #47266E; 295 | border: 1px solid #f0f0f0; 296 | font-family: Menlo, Monaco, Consolas, 'Courier New', Courier, monospace; 297 | transition: .2s ease-in-out; 298 | } 299 | 300 | .entry-detail:target .signature { 301 | background-color: #D5CAE3; 302 | border: 1px solid #624288; 303 | } 304 | 305 | .entry-detail .signature .method-permalink { 306 | position: absolute; 307 | top: 0; 308 | left: -35px; 309 | padding: 5px 15px; 310 | text-decoration: none; 311 | font-weight: bold; 312 | color: #624288; 313 | opacity: .4; 314 | transition: opacity .2s; 315 | } 316 | 317 | .entry-detail .signature .method-permalink:hover { 318 | opacity: 1; 319 | } 320 | 321 | .entry-detail:target .signature .method-permalink { 322 | opacity: 1; 323 | } 324 | 325 | .methods-inherited { 326 | padding-right: 10%; 327 | line-height: 1.5em; 328 | } 329 | 330 | .methods-inherited h3 { 331 | margin-bottom: 4px; 332 | } 333 | 334 | .methods-inherited a { 335 | display: inline-block; 336 | text-decoration: none; 337 | color: #47266E; 338 | } 339 | 340 | .methods-inherited a:hover { 341 | text-decoration: underline; 342 | color: #6C518B; 343 | } 344 | 345 | .methods-inherited .tooltip>span { 346 | background: #D5CAE3; 347 | padding: 4px 8px; 348 | border-radius: 3px; 349 | margin: -4px -8px; 350 | } 351 | 352 | .methods-inherited .tooltip * { 353 | color: #47266E; 354 | } 355 | 356 | pre { 357 | padding: 10px 20px; 358 | margin-top: 4px; 359 | border-radius: 3px; 360 | line-height: 1.45; 361 | overflow: auto; 362 | color: #333; 363 | background: #fdfdfd; 364 | font-size: 14px; 365 | border: 1px solid #eee; 366 | } 367 | 368 | code { 369 | font-family: Menlo, Monaco, Consolas, 'Courier New', Courier, monospace; 370 | } 371 | 372 | :not(pre) > code { 373 | background-color: rgba(40,35,30,0.05); 374 | padding: 0.2em 0.4em; 375 | font-size: 85%; 376 | border-radius: 3px; 377 | } 378 | 379 | span.flag { 380 | padding: 2px 4px 1px; 381 | border-radius: 3px; 382 | margin-right: 3px; 383 | font-size: 11px; 384 | border: 1px solid transparent; 385 | } 386 | 387 | span.flag.orange { 388 | background-color: #EE8737; 389 | color: #FCEBDD; 390 | border-color: #EB7317; 391 | } 392 | 393 | span.flag.yellow { 394 | background-color: #E4B91C; 395 | color: #FCF8E8; 396 | border-color: #B69115; 397 | } 398 | 399 | span.flag.green { 400 | background-color: #469C14; 401 | color: #E2F9D3; 402 | border-color: #34700E; 403 | } 404 | 405 | span.flag.red { 406 | background-color: #BF1919; 407 | color: #F9ECEC; 408 | border-color: #822C2C; 409 | } 410 | 411 | span.flag.purple { 412 | background-color: #2E1052; 413 | color: #ECE1F9; 414 | border-color: #1F0B37; 415 | } 416 | 417 | .tooltip>span { 418 | position: absolute; 419 | opacity: 0; 420 | display: none; 421 | pointer-events: none; 422 | } 423 | 424 | .tooltip:hover>span { 425 | display: inline-block; 426 | opacity: 1; 427 | } 428 | 429 | .c { 430 | color: #969896; 431 | } 432 | 433 | .n { 434 | color: #0086b3; 435 | } 436 | 437 | .t { 438 | color: #0086b3; 439 | } 440 | 441 | .s { 442 | color: #183691; 443 | } 444 | 445 | .i { 446 | color: #7f5030; 447 | } 448 | 449 | .k { 450 | color: #a71d5d; 451 | } 452 | 453 | .o { 454 | color: #a71d5d; 455 | } 456 | 457 | .m { 458 | color: #795da3; 459 | } 460 | 461 | .hidden { 462 | display: none; 463 | } 464 | .search-results { 465 | font-size: 90%; 466 | line-height: 1.3; 467 | } 468 | 469 | .search-results mark { 470 | color: inherit; 471 | background: transparent; 472 | font-weight: bold; 473 | } 474 | .search-result { 475 | padding: 5px 8px 5px 5px; 476 | cursor: pointer; 477 | border-left: 5px solid transparent; 478 | transform: translateX(-3px); 479 | transition: all .2s, background-color 0s, border .02s; 480 | min-height: 3.2em; 481 | } 482 | .search-result.current { 483 | border-left-color: #ddd; 484 | background-color: rgba(200,200,200,0.4); 485 | transform: translateX(0); 486 | transition: all .2s, background-color .5s, border 0s; 487 | } 488 | .search-result.current:hover, 489 | .search-result.current:focus { 490 | border-left-color: #866BA6; 491 | } 492 | .search-result:not(.current):nth-child(2n) { 493 | background-color: rgba(255,255,255,.06); 494 | } 495 | .search-result__title { 496 | font-size: 105%; 497 | word-break: break-all; 498 | line-height: 1.1; 499 | padding: 3px 0; 500 | } 501 | .search-result__title strong { 502 | font-weight: normal; 503 | } 504 | .search-results .search-result__title > a { 505 | padding: 0; 506 | display: block; 507 | } 508 | .search-result__title > a > .args { 509 | color: #dddddd; 510 | font-weight: 300; 511 | transition: inherit; 512 | font-size: 88%; 513 | line-height: 1.2; 514 | letter-spacing: -.02em; 515 | } 516 | .search-result__title > a > .args * { 517 | color: inherit; 518 | } 519 | 520 | .search-result a, 521 | .search-result a:hover { 522 | color: inherit; 523 | } 524 | .search-result:not(.current):hover .search-result__title > a, 525 | .search-result:not(.current):focus .search-result__title > a, 526 | .search-result__title > a:focus { 527 | color: #866BA6; 528 | } 529 | .search-result:not(.current):hover .args, 530 | .search-result:not(.current):focus .args { 531 | color: #6a5a7d; 532 | } 533 | 534 | .search-result__type { 535 | color: #e8e8e8; 536 | font-weight: 300; 537 | } 538 | .search-result__doc { 539 | color: #bbbbbb; 540 | font-size: 90%; 541 | } 542 | .search-result__doc p { 543 | margin: 0; 544 | text-overflow: ellipsis; 545 | display: -webkit-box; 546 | -webkit-box-orient: vertical; 547 | -webkit-line-clamp: 2; 548 | overflow: hidden; 549 | line-height: 1.2em; 550 | max-height: 2.4em; 551 | } 552 | 553 | .js-modal-visible .modal-background { 554 | display: flex; 555 | } 556 | .main-content { 557 | position: relative; 558 | } 559 | .modal-background { 560 | position: absolute; 561 | display: none; 562 | height: 100%; 563 | width: 100%; 564 | background: rgba(120,120,120,.4); 565 | z-index: 100; 566 | align-items: center; 567 | justify-content: center; 568 | } 569 | .usage-modal { 570 | max-width: 90%; 571 | background: #fff; 572 | border: 2px solid #ccc; 573 | border-radius: 9px; 574 | padding: 5px 15px 20px; 575 | min-width: 50%; 576 | color: #555; 577 | position: relative; 578 | transform: scale(.5); 579 | transition: transform 200ms; 580 | } 581 | .js-modal-visible .usage-modal { 582 | transform: scale(1); 583 | } 584 | .usage-modal > .close-button { 585 | position: absolute; 586 | right: 15px; 587 | top: 8px; 588 | color: #aaa; 589 | font-size: 27px; 590 | cursor: pointer; 591 | } 592 | .usage-modal > .close-button:hover { 593 | text-shadow: 2px 2px 2px #ccc; 594 | color: #999; 595 | } 596 | .modal-title { 597 | margin: 0; 598 | text-align: center; 599 | font-weight: normal; 600 | color: #666; 601 | border-bottom: 2px solid #ddd; 602 | padding: 10px; 603 | } 604 | .usage-list { 605 | padding: 0; 606 | margin: 13px; 607 | } 608 | .usage-list > li { 609 | padding: 5px 2px; 610 | overflow: auto; 611 | padding-left: 100px; 612 | min-width: 12em; 613 | } 614 | .usage-modal kbd { 615 | background: #eee; 616 | border: 1px solid #ccc; 617 | border-bottom-width: 2px; 618 | border-radius: 3px; 619 | padding: 3px 8px; 620 | font-family: monospace; 621 | margin-right: 2px; 622 | display: inline-block; 623 | } 624 | .usage-key { 625 | float: left; 626 | clear: left; 627 | margin-left: -100px; 628 | margin-right: 12px; 629 | } 630 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | README - github.com/naqvis/crystar 18 | 19 | 20 | 21 | 108 | 109 | 110 |
111 |

Build Status 112 | GitHub release

113 | 114 |

Crystal Tar (Crystar)

115 | 116 |

Shard Crystar implements access to tar archives.

117 | 118 |

No external library needed. This is written in pure Crystal.

119 | 120 |

Tape archives (tar) are a file format for storing a sequence of files that can be read and written in a streaming manner. This shard aims to cover most variations of the format, including those produced by GNU and BSD tar tools.

121 | 122 |

This module is mostly based on Tar package implementation of Golang

123 | 124 |

Format represents the tar archive format.

125 | 126 |

The original tar format was introduced in Unix V7. 127 | Since then, there have been multiple competing formats attempting to 128 | standardize or extend the V7 format to overcome its limitations. 129 | The most common formats are the USTAR, PAX, and GNU formats, 130 | each with their own advantages and limitations.

131 | 132 |

The following table captures the capabilities of each format:

133 | 134 |

| USTAR | PAX | GNU 135 | ------------------+--------+-----------+---------- 136 | Name | 256B | unlimited | unlimited 137 | Linkname | 100B | unlimited | unlimited 138 | Size | uint33 | unlimited | uint89 139 | Mode | uint21 | uint21 | uint57 140 | Uid/Gid | uint21 | unlimited | uint57 141 | Uname/Gname | 32B | unlimited | 32B 142 | ModTime | uint33 | unlimited | int89 143 | AccessTime | n/a | unlimited | int89 144 | ChangeTime | n/a | unlimited | int89 145 | Devmajor/Devminor | uint21 | uint21 | uint57 146 | ------------------+--------+-----------+---------- 147 | string encoding | ASCII | UTF-8 | binary 148 | sub-second times | no | yes | no 149 | sparse files | no | yes | yes

150 | 151 |

The table's upper portion shows the Header fields, where each format reports 152 | the maximum number of bytes allowed for each string field and 153 | the integer type used to store each numeric field 154 | (where timestamps are stored as the number of seconds since the Unix epoch).

155 | 156 |

The table's lower portion shows specialized features of each format, 157 | such as supported string encodings, support for sub-second timestamps, 158 | or support for sparse files.

159 | 160 |

The Writer currently provides no support for sparse files.

161 | 162 |

Installation

163 | 164 |
  1. Add the dependency to your shard.yml:
165 | 166 |

`yaml 167 | dependencies:

168 | 169 |
 crystar:
170 |    github: naqvis/crystar
171 | 172 |

`

173 | 174 |
  1. Run shards install
175 | 176 |

Usage

177 | 178 |
require "crystar"
179 | 180 |

Crystar module contains readers and writers for tar archive. 181 | Tape archives (tar) are a file format for storing a sequence of files that can be read and written in a streaming manner. 182 | This module aims to cover most variations of the format, including those produced by GNU and BSD tar tools.

183 | 184 |

Sample Usage

185 | 186 |
files = [
187 |   {"readme.txt", "This archive contains some text files."},
188 |   {"minerals.txt", "Mineral names:\nalunite\nchromium\nvlasovite"},
189 |   {"todo.txt", "Get crystal mining license."},
190 | ]
191 | 
192 | buf = IO::Memory.new
193 | Crystar::Writer.open(buf) do |tw|
194 |   files.each_with_index do |f, UNDERSCORE|
195 |     hdr = Header.new(
196 |       name: f[0],
197 |       mode: 0o600_i64,
198 |       size: f[1].size.to_i64
199 |     )
200 |     tw.write_header(hdr)
201 |     tw.write(f[1].to_slice)
202 |   end
203 | end
204 | #Open and iterate through the files in the archive
205 | buf.pos = 0
206 | Crystar::Reader.open(buf) do |tar|
207 |   tar.each_entry do |entry|
208 |     p "Contents of #{entry.name}"
209 |     IO.copy entry.io, STDOUT
210 |     p "\n"
211 |   end
212 | end
213 | 214 |

Refer to Crystar::Reader and Crystar::Writer module for documentation on detailed usage.

215 | 216 |

Development

217 | 218 |

To run all tests:

219 | 220 |
crystal spec
221 | 222 |

Contributing

223 | 224 |
  1. Fork it (<https://github.com/naqvis/crystar/fork>)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request
225 | 226 |

Contributors

227 | 228 | 229 |
230 | 231 | 232 | -------------------------------------------------------------------------------- /example/compressed.cr: -------------------------------------------------------------------------------- 1 | require "gzip" 2 | require "../src/crystar" 3 | include Crystar 4 | 5 | File.open("test.tar.gz") do |file| 6 | Gzip::Reader.open(file) do |gzip| 7 | Crystar::Reader.open(gzip) do |tar| 8 | tar.each_entry do |entry| 9 | puts "Contents of #{entry.name}" 10 | IO.copy entry.io, STDOUT 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /example/example.cr: -------------------------------------------------------------------------------- 1 | require "../src/crystar" 2 | include Crystar 3 | 4 | files = [ 5 | {"readme.txt", "This archive contains some text files."}, 6 | {"minerals.txt", "Mineral names:\nalunite\nchromium\nvlasovite"}, 7 | {"todo.txt", "Get crystal mining license."}, 8 | ] 9 | 10 | File.open("test.tar", "w") do |file| 11 | Crystar::Writer.open(file) do |tw| 12 | files.each do |f| 13 | hdr = Header.new( 14 | name: f[0], 15 | mode: 0o600_i64, 16 | size: f[1].size.to_i64 17 | ) 18 | tw.write_header(hdr) 19 | tw.write(f[1].to_slice) 20 | end 21 | end 22 | end 23 | 24 | # Open and iterate through the files in the archive 25 | File.open("test.tar") do |file| 26 | Crystar::Reader.open(file) do |tar| 27 | tar.each_entry do |entry| 28 | puts "Contents of #{entry.name}" 29 | IO.copy entry.io, STDOUT 30 | puts "" 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: crystar 2 | version: 0.4.0 3 | description: | 4 | Shard Crystar implements access to tar archives. This shard aims to cover most variations of the format, including those produced by GNU and BSD tar tools. 5 | authors: 6 | - Ali Naqvi 7 | 8 | crystal: ">=1.0.0" 9 | 10 | license: MIT 11 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/crystar" 3 | -------------------------------------------------------------------------------- /spec/testdata/gnu-incremental.tar: -------------------------------------------------------------------------------- 1 | test2/0040755000175000017500000000001612574542263013224 Dustar rawrdsnet1257454434512574542274YfooYsparsetest2/foo0100644000175000017500000000010012574542163013667 0ustar rawrdsnet1257454434512574542274fewafewa 2 | fewa 3 | feawfehahaha 4 | hahaafwe 5 | hahafawe 6 | hahawafe 7 | a 8 | fwefewa 9 | test2/sparse0100644000175000017500000000000012574542263017530 Sustar rawrdsnet1257460641412574542274040000000000000000000004000000000 -------------------------------------------------------------------------------- /spec/testdata/gnu-long-nul.tar: -------------------------------------------------------------------------------- 1 | ././@LongLink0000644000000000000000000000024100000000000011600 Lustar rootroot01234567891234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890000644000175000017500000000000013044750217022125 0ustar rawrdsnet -------------------------------------------------------------------------------- /spec/testdata/gnu-not-utf8.tar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naqvis/crystar/3975502cbe89bca528e1af4b86fbfc0f0bfacce1/spec/testdata/gnu-not-utf8.tar -------------------------------------------------------------------------------- /spec/testdata/gnu-utf8.tar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naqvis/crystar/3975502cbe89bca528e1af4b86fbfc0f0bfacce1/spec/testdata/gnu-utf8.tar -------------------------------------------------------------------------------- /spec/testdata/gnu.tar: -------------------------------------------------------------------------------- 1 | small.txt0000640021650100116100000000000511213074064012105 0ustar dsymondsengKiltssmall2.txt0000640021650100116100000000001311213113114012154 0ustar dsymondsengGoogle.com 2 | -------------------------------------------------------------------------------- /spec/testdata/neg-size.tar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naqvis/crystar/3975502cbe89bca528e1af4b86fbfc0f0bfacce1/spec/testdata/neg-size.tar -------------------------------------------------------------------------------- /spec/testdata/pax-bad-hdr-file.tar: -------------------------------------------------------------------------------- 1 | path/to/readme/readme/readme/readme/readme/readme/readme/readme/readme/readme/readme/readme/readme/r0000000000000000000000000000004100000000000032025 xustar000000000000000033 path=PAX1/PAX1/long-path-namefoo0000640116074500116100000000125412575676024010640 0ustar00joetsaiengiRFmWghs3CK9/2HSvRja4TzX8HsRwzbVYl+h0HRkH9uPho2BGmrG5a0vpHsPn2W7Pn33Ux/+rkLSA3GUOX/WiPmP+h73T1r0DZIDJXtOgYWIUhsqUE0zUz1LEaO/y2H+WAe/ZlWt90N2KHka0bkXajoEAdOUrN42PKl/3mu7jiCW45hTNBDp3ArJD8QHN7l3JFMfnusPuir9+K8Oh6bEfN2bHhXjZ41ZkweCHZWUKT8NsdHeObQnXAyvkU5q1OhefE0+uvksVba2ZNyhThAAGZgiqEtTOJJLm8zgcI5avXHMVwlR6mt1jepOct4jQNlAdpkmslKW3BuiwLswGAsw7ttr/pRa/oCT4HUoBWcY3w96+TGR6uXtvbDOM9WhPXGo+1bwhAsA/RXPA1ZX+oS6t4rl/ZvkMZZN4VO5OvKph8tthdG3ocpXUw11zv6mQ7n6kyObLDCMFOtkdnhQBU/BGEK6mw4oTRa1Hd91+bUUqQh6hl3JeDk/t2KDWOEehOxgOqfVG72UuMeo2IayNK/pUXrcUXuywq9KT+bWQxdJsXzwkkyT8Ovz4oiIzHAa14e/Ib8Xxz+BHwpN3TtOXsHziuqLGMzqv867CganwsFxNEGRaTQ6C2bRK+OxetaxhQqe1G/UWwfi5a9PuJC3wfITSa0IhBot9hGAG35VVb4LsRE= -------------------------------------------------------------------------------- /spec/testdata/pax-bad-mtime-file.tar: -------------------------------------------------------------------------------- 1 | path/to/readme/readme/readme/readme/readme/readme/readme/readme/readme/readme/readme/readme/readme/r0000000000000000000000000000004100000000000032025 xustar000000000000000033 mtime=999xxx9324.432432444444 2 | foo0000640116074500116100000000125412575676024010640 0ustar00joetsaiengiRFmWghs3CK9/2HSvRja4TzX8HsRwzbVYl+h0HRkH9uPho2BGmrG5a0vpHsPn2W7Pn33Ux/+rkLSA3GUOX/WiPmP+h73T1r0DZIDJXtOgYWIUhsqUE0zUz1LEaO/y2H+WAe/ZlWt90N2KHka0bkXajoEAdOUrN42PKl/3mu7jiCW45hTNBDp3ArJD8QHN7l3JFMfnusPuir9+K8Oh6bEfN2bHhXjZ41ZkweCHZWUKT8NsdHeObQnXAyvkU5q1OhefE0+uvksVba2ZNyhThAAGZgiqEtTOJJLm8zgcI5avXHMVwlR6mt1jepOct4jQNlAdpkmslKW3BuiwLswGAsw7ttr/pRa/oCT4HUoBWcY3w96+TGR6uXtvbDOM9WhPXGo+1bwhAsA/RXPA1ZX+oS6t4rl/ZvkMZZN4VO5OvKph8tthdG3ocpXUw11zv6mQ7n6kyObLDCMFOtkdnhQBU/BGEK6mw4oTRa1Hd91+bUUqQh6hl3JeDk/t2KDWOEehOxgOqfVG72UuMeo2IayNK/pUXrcUXuywq9KT+bWQxdJsXzwkkyT8Ovz4oiIzHAa14e/Ib8Xxz+BHwpN3TtOXsHziuqLGMzqv867CganwsFxNEGRaTQ6C2bRK+OxetaxhQqe1G/UWwfi5a9PuJC3wfITSa0IhBot9hGAG35VVb4LsRE= -------------------------------------------------------------------------------- /spec/testdata/pax-multi-hdrs.tar: -------------------------------------------------------------------------------- 1 | path/to/readme/readme/readme/readme/readme/readme/readme/readme/readme/readme/readme/readme/readme/r0000000000000000000000000000004100000000000032025 xustar000000000000000033 path=PAX1/PAX1/long-path-name 2 | path/to/readme/readme/readme/readme/readme/readme/readme/readme/readme/readme/readme/readme/readme/r0000000000000000000000000000004100000000000032025 xustar000000000000000033 path=PAX2/PAX2/long-path-name 3 | path/to/readme/readme/readme/readme/readme/readme/readme/readme/readme/readme/readme/readme/readme/r0000000000000000000000000000005100000000000032026 xustar000000000000000041 linkpath=PAX3/PAX3/long-linkpath-name 4 | path/to/readme/readme/readme/readme/readme/readme/readme/readme/readme/readme/readme/readme/readme/r0000000000000000000000000000005100000000000032026 xustar000000000000000041 linkpath=PAX4/PAX4/long-linkpath-name 5 | bar0000000000000000000000000000000000000000000007112 2fooustar00 -------------------------------------------------------------------------------- /spec/testdata/pax-nil-sparse-data.tar: -------------------------------------------------------------------------------- 1 | PaxHeaders.0/sparse.db0000000000000000000000000000016200000000000012024 xustar0022 GNU.sparse.major=1 2 | 22 GNU.sparse.minor=0 3 | 29 GNU.sparse.name=sparse.db 4 | 28 GNU.sparse.realsize=1000 5 | 13 size=1512 6 | GNUSparseFile.0/sparse.db0000000000000000000000000000275000000000000013544 0ustar00000000000000001 7 | 0 8 | 1000 9 | 0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 -------------------------------------------------------------------------------- /spec/testdata/pax-nul-path.tar: -------------------------------------------------------------------------------- 1 | PaxHeaders.0/0123456789012345678901234567890123456789012345678901234567890123456789012345678901234560000000000000000000000000000032300000000000022376 xustar0000000000000000211 path=01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 2 | 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890000000000000000000000000000000000000000000021361 0ustar0000000000000000 -------------------------------------------------------------------------------- /spec/testdata/pax-nul-xattrs.tar: -------------------------------------------------------------------------------- 1 | PaxHeaders.0/bad-null.txt0000000000000000000000000000003700000000000013720 xustar000000000000000031 SCHILY.xattr.null=fizzbuzz 2 | bad-null.txt0000000000000000000000000000000000000000000011414 0ustar0000000000000000 -------------------------------------------------------------------------------- /spec/testdata/pax-pos-size-file.tar: -------------------------------------------------------------------------------- 1 | path/to/readme/readme/readme/readme/readme/readme/readme/readme/readme/readme/readme/readme/readme/r0000000000000000000000000000004100000000000032025 xustar000000000000000033 size=000000000000000000000999 2 | foo0000640116074500116100000000125412575676024010640 0ustar00joetsaiengiRFmWghs3CK9/2HSvRja4TzX8HsRwzbVYl+h0HRkH9uPho2BGmrG5a0vpHsPn2W7Pn33Ux/+rkLSA3GUOX/WiPmP+h73T1r0DZIDJXtOgYWIUhsqUE0zUz1LEaO/y2H+WAe/ZlWt90N2KHka0bkXajoEAdOUrN42PKl/3mu7jiCW45hTNBDp3ArJD8QHN7l3JFMfnusPuir9+K8Oh6bEfN2bHhXjZ41ZkweCHZWUKT8NsdHeObQnXAyvkU5q1OhefE0+uvksVba2ZNyhThAAGZgiqEtTOJJLm8zgcI5avXHMVwlR6mt1jepOct4jQNlAdpkmslKW3BuiwLswGAsw7ttr/pRa/oCT4HUoBWcY3w96+TGR6uXtvbDOM9WhPXGo+1bwhAsA/RXPA1ZX+oS6t4rl/ZvkMZZN4VO5OvKph8tthdG3ocpXUw11zv6mQ7n6kyObLDCMFOtkdnhQBU/BGEK6mw4oTRa1Hd91+bUUqQh6hl3JeDk/t2KDWOEehOxgOqfVG72UuMeo2IayNK/pUXrcUXuywq9KT+bWQxdJsXzwkkyT8Ovz4oiIzHAa14e/Ib8Xxz+BHwpN3TtOXsHziuqLGMzqv867CganwsFxNEGRaTQ6C2bRK+OxetaxhQqe1G/UWwfi5a9PuJC3wfITSa0IhBot9hGAG35VVb4LsRE= -------------------------------------------------------------------------------- /spec/testdata/pax-records.tar: -------------------------------------------------------------------------------- 1 | PaxHeaders.0/file0000000000000000000000000000013500000000000011062 xustar0018 GOLANG.pkg=tar 2 | 25 comment=Hello, 世界 3 | 50 uname=longlonglonglonglonglonglonglonglonglong 4 | file0000000000000000000000000000000000000000000016617 0ustar00longlonglonglonglonglonglonglong00000000000000 -------------------------------------------------------------------------------- /spec/testdata/pax.tar: -------------------------------------------------------------------------------- 1 | a/PaxHeaders.6887/12345678910111213141516171819202122232425262728293031323334353637383940414243444540000644000175000017500000000044612036615200022461 xustar0000000000000000204 path=a/123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100 2 | 30 mtime=1350244992.023960108 3 | 30 atime=1350244992.023960108 4 | 30 ctime=1350244992.023960108 5 | a/123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525350000664000175000017500000000000712036615200023454 0ustar00shaneshane00000000000000shaner 6 | a/PaxHeaders.6887/b0000644000175000017500000000045012036666720012440 xustar0000000000000000206 linkpath=123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100 7 | 30 mtime=1350266320.910238425 8 | 30 atime=1350266320.910238425 9 | 30 ctime=1350266320.910238425 10 | a/b0000777000175000017500000000000012036666720024004 21234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545ustar00shaneshane00000000000000 -------------------------------------------------------------------------------- /spec/testdata/small.txt: -------------------------------------------------------------------------------- 1 | Kilts -------------------------------------------------------------------------------- /spec/testdata/star.tar: -------------------------------------------------------------------------------- 1 | small.txt0000640 0216501 0011610 00000000005 11213575217 0016730 0ustar00dsymondseng0000000 0000000 11213575217 11213575217 tarKiltssmall2.txt0000640 0216501 0011610 00000000013 11213575217 0017011 0ustar00dsymondseng0000000 0000000 11213575217 11213575217 tarGoogle.com 2 | -------------------------------------------------------------------------------- /spec/testdata/trailing-slash.tar: -------------------------------------------------------------------------------- 1 | 123456789/123456789/123456789/123456789/123456789/123456789/123456789/123456789/123456789/1234567890000000000000000000000000000046600000000000020160 xustar00310 path=123456789/123456789/123456789/123456789/123456789/123456789/123456789/123456789/123456789/123456789/123456789/123456789/123456789/123456789/123456789/123456789/123456789/123456789/123456789/123456789/123456789/123456789/123456789/123456789/123456789/123456789/123456789/123456789/123456789/123456789/ 2 | 123456789/123456789/123456789/123456789/123456789/123456789/123456789/123456789/123456789/1234567890000000000000000000000000000000000000000000021275 5ustar0000000000000000 -------------------------------------------------------------------------------- /spec/testdata/v7.tar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naqvis/crystar/3975502cbe89bca528e1af4b86fbfc0f0bfacce1/spec/testdata/v7.tar -------------------------------------------------------------------------------- /spec/writer_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | module Crystar 4 | describe "Writer" do 5 | it "Test PAX - Create an archive with a large name" do 6 | buf = IO::Memory.new 7 | begin 8 | f = File.open("spec/testdata/small.txt") 9 | hdr = Crystar.file_info_header(f, "") 10 | f.close 11 | 12 | # Force a PAX long name to be written 13 | contents = " " * hdr.size 14 | long_name = "ab" * 100 15 | hdr.name = long_name 16 | Crystar::Writer.open(buf) do |tw| 17 | tw.write_header(hdr) 18 | tw.write(contents.to_slice) 19 | end 20 | 21 | # Simple test to make sure PAX extension are in effect 22 | buf.pos = 0 23 | buf.to_s.should contain("PaxHeaders.0") 24 | 25 | # Test that we can get a long name back out of the archive. 26 | buf.pos = 0 27 | t = Crystar::Reader.new(buf) 28 | hdr = t.next_entry 29 | if hdr 30 | hdr.name.should eq(long_name) 31 | else 32 | fail "no header" 33 | end 34 | ensure 35 | buf.close 36 | end 37 | end 38 | 39 | it "Test PAX - Create an archive with a large linkname" do 40 | buf = IO::Memory.new 41 | begin 42 | f = File.open("spec/testdata/small.txt") 43 | hdr = Crystar.file_info_header(f, "") 44 | hdr.flag = SYMLINK.ord.to_u8 45 | f.close 46 | 47 | # Force a PAX long linkname to be written 48 | long_linkname = "1234567890/1234567890" * 10 49 | hdr.link_name = long_linkname 50 | Crystar::Writer.open(buf) do |tw| 51 | tw.write_header(hdr) 52 | end 53 | 54 | # Simple test to make sure PAX extension are in effect 55 | buf.pos = 0 56 | buf.to_s.should contain("PaxHeaders.0") 57 | 58 | # Test that we can get a long name back out of the archive. 59 | buf.pos = 0 60 | t = Crystar::Reader.new(buf) 61 | hdr = t.next_entry 62 | if hdr 63 | hdr.link_name.should eq(long_linkname) 64 | else 65 | fail "no header" 66 | end 67 | ensure 68 | buf.close 69 | end 70 | end 71 | 72 | it "Test PAX - Create an archive with non ascii" do 73 | # These should trigger a pax header because pax headers 74 | # have a defined utf-8 encoding. 75 | buf = IO::Memory.new 76 | begin 77 | f = File.open("spec/testdata/small.txt") 78 | hdr = Crystar.file_info_header(f, "") 79 | f.close 80 | 81 | # some sample data 82 | chinese_filename = "文件名" 83 | chinese_groupname = "組" 84 | chinese_username = "用戶名" 85 | contents = " " * hdr.size 86 | 87 | hdr.name = chinese_filename 88 | hdr.gname = chinese_groupname 89 | hdr.uname = chinese_username 90 | 91 | Crystar::Writer.open(buf) do |tw| 92 | tw.write_header(hdr) 93 | tw.write(contents.to_slice) 94 | end 95 | 96 | # Simple test to make sure PAX extension are in effect 97 | buf.pos = 0 98 | buf.to_s.should contain("PaxHeaders.0") 99 | 100 | # Test that we can get a long name back out of the archive. 101 | buf.pos = 0 102 | t = Crystar::Reader.new(buf) 103 | hdr = t.next_entry 104 | if hdr 105 | hdr.name.should eq(chinese_filename) 106 | hdr.gname.should eq(chinese_groupname) 107 | hdr.uname.should eq(chinese_username) 108 | else 109 | fail "no header" 110 | end 111 | ensure 112 | buf.close 113 | end 114 | end 115 | 116 | it "Test PAX - Create an archive with an xattr" do 117 | buf = IO::Memory.new 118 | begin 119 | f = File.open("spec/testdata/small.txt") 120 | hdr = Crystar.file_info_header(f, "") 121 | f.close 122 | 123 | xattr = Hash{"user.key" => "value"} 124 | contents = "Kilts" 125 | hdr.xattr = xattr 126 | Crystar::Writer.open(buf) do |tw| 127 | tw.write_header(hdr) 128 | tw.write(contents.to_slice) 129 | end 130 | 131 | # Test that we can get a xattr back out of the archive. 132 | buf.pos = 0 133 | t = Crystar::Reader.new(buf) 134 | hdr = t.next_entry 135 | if hdr 136 | hdr.xattr.should eq(xattr) 137 | else 138 | fail "no header" 139 | end 140 | ensure 141 | buf.close 142 | end 143 | end 144 | 145 | it "Test PAX headers are sorted" do 146 | buf = IO::Memory.new 147 | begin 148 | f = File.open("spec/testdata/small.txt") 149 | hdr = Crystar.file_info_header(f, "") 150 | f.close 151 | 152 | # Force a PAX long name to be written 153 | contents = " " * hdr.size 154 | hdr.xattr = Hash{ 155 | "foo" => "foo", 156 | "bar" => "bar", 157 | "baz" => "baz", 158 | "qux" => "qux", 159 | } 160 | 161 | Crystar::Writer.open(buf) do |tw| 162 | tw.write_header(hdr) 163 | tw.write(contents.to_slice) 164 | end 165 | 166 | # Simple test to make sure PAX extension are in effect 167 | buf.pos = 0 168 | buf.to_s.should contain("PaxHeaders.0") 169 | 170 | # xattr bar should always appear before others 171 | buf.pos = 0 172 | str = buf.to_s 173 | index = ->(strs : String, s : String) { 174 | a = strs.index(s) 175 | fail "Couldn't find xattr = #{s}" if a.nil? 176 | a 177 | } 178 | indices = [ 179 | index.call(str, "bar=bar"), 180 | index.call(str, "baz=baz"), 181 | index.call(str, "foo=foo"), 182 | index.call(str, "qux=qux"), 183 | ] 184 | indices.should eq(indices.sort) 185 | ensure 186 | buf.close 187 | end 188 | end 189 | 190 | it "Test USTAR - Create an archive with a large name" do 191 | # Create an archive with a path that failed to split with USTAR extension in previous versions. 192 | 193 | buf = IO::Memory.new 194 | begin 195 | f = File.open("spec/testdata/small.txt") 196 | hdr = Crystar.file_info_header(f, "") 197 | hdr.flag = DIR.ord.to_u8 198 | f.close 199 | 200 | # Force a PAX long name to be written. The name was taken from a practical example 201 | # that fails and replaced ever char through numbers to anonymize the sample. 202 | long_name = "/0000_0000000/00000-000000000/0000_0000000/00000-0000000000000/0000_0000000/00000-0000000-00000000/0000_0000000/00000000/0000_0000000/000/0000_0000000/00000000v00/0000_0000000/000000/0000_0000000/0000000/0000_0000000/00000y-00/0000/0000/00000000/0x000000/" 203 | hdr.name = long_name 204 | Crystar::Writer.open(buf) do |tw| 205 | tw.write_header(hdr) 206 | end 207 | 208 | # Test that we can get a long name back out of the archive. 209 | buf.pos = 0 210 | t = Crystar::Reader.new(buf) 211 | hdr = t.next_entry 212 | if hdr 213 | hdr.name.should eq(long_name) 214 | else 215 | fail "no header" 216 | end 217 | ensure 218 | buf.close 219 | end 220 | end 221 | 222 | it "Test PAX - Valid flag with PAX Header" do 223 | buf = IO::Memory.new 224 | begin 225 | file_name = "ab" * 100 226 | hdr = Header.new( 227 | name: file_name, 228 | size: 4_i64, 229 | flag: 0_u8 230 | ) 231 | Crystar::Writer.open(buf) do |tw| 232 | tw.write_header(hdr) 233 | tw.write "fooo".to_slice 234 | end 235 | 236 | Crystar::Reader.open(buf) do |tar| 237 | tar.each_entry do |entry| 238 | entry.flag.should eq(REG.ord.to_u8) 239 | end 240 | end 241 | ensure 242 | buf.close 243 | end 244 | end 245 | 246 | it "Test Prefix field when encoding GNU format" do 247 | # Prefix field is valid in USTAR and PAX, but not GNU 248 | 249 | names = [ 250 | "0/1/2/3/4/5/6/7/8/9/10/11/12/13/14/15/16/17/18/19/20/21/22/23/24/25/26/27/28/29/30/file.txt", 251 | "0/1/2/3/4/5/6/7/8/9/10/11/12/13/14/15/16/17/18/19/20/21/22/23/24/25/26/27/28/29/30/31/32/33/file.txt", 252 | "0/1/2/3/4/5/6/7/8/9/10/11/12/13/14/15/16/17/18/19/20/21/22/23/24/25/26/27/28/29/30/31/32/333/file.txt", 253 | "0/1/2/3/4/5/6/7/8/9/10/11/12/13/14/15/16/17/18/19/20/21/22/23/24/25/26/27/28/29/30/31/32/33/34/35/36/37/38/39/40/file.txt", 254 | "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/file.txt", 255 | "/home/support/.openoffice.org/3/user/uno_packages/cache/registry/com.sun.star.comp.deployment.executable.PackageRegistryBackend", 256 | ] 257 | 258 | names.each_with_index do |name, i| 259 | b = IO::Memory.new 260 | hdr = Header.new( 261 | name: name, 262 | uid: 1 << 25, # Prevent USTAR format 263 | ) 264 | Crystar::Writer.open(b) do |tw| 265 | tw.write_header hdr 266 | end 267 | 268 | # The Prefix field should never appear in the GNU format. 269 | blk = Block.new 270 | blk.to_bytes.copy_from(b.to_slice.to_unsafe, blk.size) 271 | prefix = String.new(blk.ustar.prefix) 272 | if (idx = byte_index(prefix, '\0')) && (i >= 0) 273 | prefix = prefix[...idx] 274 | end 275 | if blk.get_format == Format::GNU && !prefix.blank? && name.starts_with?(prefix) 276 | fail "test #{i}, found prefix in GNU format: #{prefix}" 277 | end 278 | b.pos = 0 279 | Crystar::Reader.open(b) do |tar| 280 | tar.each_entry do |entry| 281 | entry.name.should eq(name) 282 | end 283 | end 284 | end 285 | end 286 | end 287 | 288 | describe "Writer Errors" do 289 | it "Test for WriteTooLong" do 290 | buf = IO::Memory.new 291 | hdr = Header.new(name: "dir/", flag: DIR.ord.to_u8) 292 | Crystar::Writer.open(buf) do |tw| 293 | tw.write_header(hdr) 294 | expect_raises(ErrWriteTooLong) do 295 | tw.write(Bytes.new(1)) 296 | end 297 | end 298 | end 299 | 300 | it "Test for Negative Size" do 301 | buf = IO::Memory.new 302 | hdr = Header.new(name: "small.txt", size: -1_i64) 303 | Crystar::Writer.open(buf) do |tw| 304 | expect_raises(Error, "negative size on header-only type") do 305 | tw.write_header(hdr) 306 | end 307 | end 308 | end 309 | 310 | it "Test write before header" do 311 | buf = IO::Memory.new 312 | 313 | Crystar::Writer.open(buf) do |tw| 314 | expect_raises(ErrWriteTooLong) do 315 | tw.write "Kilts".to_slice 316 | end 317 | end 318 | end 319 | 320 | it "Test After close" do 321 | buf = IO::Memory.new 322 | hdr = Header.new(name: "small.txt") 323 | tw = Crystar::Writer.new(buf) 324 | tw.write_header(hdr) 325 | tw.close 326 | expect_raises(Error, "Can't write to closed writer") do 327 | tw.write "Kilts".to_slice 328 | end 329 | expect_raises(Error, "Can't write to closed writer") do 330 | tw.flush 331 | end 332 | tw.close 333 | end 334 | 335 | it "Test for Premature flush" do 336 | buf = IO::Memory.new 337 | hdr = Header.new(name: "small.txt", size: 5_i64) 338 | expect_raises(Error) do 339 | Crystar::Writer.open(buf) do |tw| 340 | tw.write_header(hdr) 341 | tw.flush 342 | end 343 | end 344 | end 345 | 346 | it "Test for Premature close" do 347 | buf = IO::Memory.new 348 | hdr = Header.new(name: "small.txt", size: 5_i64) 349 | expect_raises(Error) do 350 | Crystar::Writer.open(buf) do |tw| 351 | tw.write_header(hdr) 352 | end 353 | end 354 | end 355 | end 356 | end 357 | -------------------------------------------------------------------------------- /src/crystar.cr: -------------------------------------------------------------------------------- 1 | require "./tar/*" 2 | 3 | # `Crystar` module contains readers and writers for tar archive. 4 | # Tape archives (tar) are a file format for storing a sequence of files that can be read and written in a streaming manner. 5 | # This module aims to cover most variations of the format, including those produced by GNU and BSD tar tools. 6 | # 7 | # ### Example 8 | # ``` 9 | # files = [ 10 | # {"readme.txt", "This archive contains some text files."}, 11 | # {"minerals.txt", "Mineral names:\nalunite\nchromium\nvlasovite"}, 12 | # {"todo.txt", "Get crystal mining license."}, 13 | # ] 14 | # buf = IO::Memory.new 15 | # Crystar::Writer.open(buf) do |tw| 16 | # files.each do |f| 17 | # hdr = Header.new( 18 | # name: f[0], 19 | # mode: 0o600_i64, 20 | # size: f[1].size.to_i64 21 | # ) 22 | # tw.write_header(hdr) 23 | # tw.write(f[1].to_slice) 24 | # end 25 | # end 26 | # 27 | # # Open and iterate through the files in the archive 28 | # buf.pos = 0 29 | # Crystar::Reader.open(buf) do |tar| 30 | # tar.each_entry do |entry| 31 | # p "Contents of #{entry.name}" 32 | # IO.copy entry.io, STDOUT 33 | # p "\n" 34 | # end 35 | # end 36 | # ``` 37 | module Crystar 38 | VERSION = {{ `shards version "#{__DIR__}"`.chomp.stringify }} 39 | 40 | # Common Crystar exceptions 41 | class Error < Exception 42 | end 43 | 44 | class ErrWriteTooLong < Error 45 | end 46 | 47 | # Type flags for Header#flag 48 | REG = '0' # '0' indicated a regular file 49 | @[Deprecated("Use `REG` instead")] 50 | REGA = '\0' 51 | 52 | # '1' to '6' are header-only flags and may not have a data body. 53 | LINK = '1' # Hard link 54 | SYMLINK = '2' # Symbolic link 55 | CHAR = '3' # Character device node 56 | BLOCK = '4' # Block device node 57 | DIR = '5' # Directory 58 | FIFO = '6' # FIFO node 59 | 60 | CONT = '7' # reserved 61 | XHEADER = 'x' # Used by PAX format to store key-value records that are only relevant to the next file. 62 | XGLOBAL_HEADER = 'g' # Used by PAX format to key-value records that are relevant to all subsequent files. 63 | GNU_SPARSE = 'S' # indicated a sparse file in the GNU format 64 | 65 | # 'L' and 'K' are used by teh GNU format for a meta file 66 | # used to store the path or link name for the next file. 67 | GNU_LONGNAME = 'L' 68 | GNU_LONGLINK = 'K' 69 | 70 | # Keywords for PAX extended header records 71 | PAX_NONE = "" # indicates that no PAX key is suitable 72 | PAX_PATH = "path" 73 | PAX_LINK_PATH = "linkpath" 74 | PAX_SIZE = "size" 75 | PAX_UID = "uid" 76 | PAX_GID = "gid" 77 | PAX_UNAME = "uname" 78 | PAX_GNAME = "gname" 79 | PAX_MTIME = "mtime" 80 | PAX_ATIME = "atime" 81 | PAX_CTIME = "ctime" # Removed from later revision of PAX spec, but was valid 82 | PAX_CHARSET = "charset" # Currently unused 83 | PAX_COMMENT = "comment" # Currently unused 84 | 85 | PAX_SCHILY_XATTR = "SCHILY.xattr." 86 | 87 | # Keywords for GNU sparse files in a PAX extended header. 88 | PAX_GNU_SPARSE = "GNU.sparse." 89 | PAX_GNU_SPARSE_NUMBLOCKS = "GNU.sparse.numblocks" 90 | PAX_GNU_SPARSE_OFFSET = "GNU.sparse.offset" 91 | PAX_GNU_SPARSE_NUMBYTES = "GNU.sparse.numbytes" 92 | PAX_GNU_SPARSE_MAP = "GNU.sparse.map" 93 | PAX_GNU_SPARSE_NAME = "GNU.sparse.name" 94 | PAX_GNU_SPARSE_MAJOR = "GNU.sparse.major" 95 | PAX_GNU_SPARSE_MINOR = "GNU.sparse.minor" 96 | PAX_GNU_SPARSE_SIZE = "GNU.sparse.size" 97 | PAX_GNU_SPARSE_REALSIZE = "GNU.sparse.realsize" 98 | 99 | # set of the PAX keys for which we have built-in support. 100 | # This does not contain "charset" or "comment", which are both PAX-specific, 101 | # so adding them as first-class features of Header is unlikely. 102 | # Users can use the PAXRecords field to set it themselves. 103 | 104 | BASIC_KEYS = { 105 | PAX_PATH => true, PAX_LINK_PATH => true, PAX_SIZE => true, 106 | PAX_UID => true, PAX_GID => true, PAX_UNAME => true, 107 | PAX_GNAME => true, PAX_MTIME => true, PAX_ATIME => true, 108 | PAX_CTIME => true, 109 | } 110 | 111 | # Mode constants from USTAR spec: 112 | # See http://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html#tag_20_92_13_06 113 | ISUID = 0o4000 # Set uid 114 | ISGID = 0o2000 # Set gid 115 | ISVTX = 0o1000 # Save text (sticky bit) 116 | # Common Unix mode constants; these are not defined in any common tar standard. 117 | # Header.FileInfo understands these, but FileInfoHeader will never produce these. 118 | ISDIR = 0o40000 # Directory 119 | ISFIFO = 0o10000 # FIFO 120 | ISREG = 0o100000 # Regular file 121 | ISLINK = 0o120000 # Symbolic link 122 | ISBLK = 0o60000 # Block special file 123 | ISCHR = 0o20000 # Character special file 124 | ISSOCK = 0o140000 # Socket 125 | end 126 | -------------------------------------------------------------------------------- /src/tar/format.cr: -------------------------------------------------------------------------------- 1 | module Crystar 2 | extend self 3 | 4 | # Format represents the tar archive format. 5 | # 6 | # The original tar format was introduced in Unix V7. 7 | # Since then, there have been multiple competing formats attempting to 8 | # standardize or extend the V7 format to overcome its limitations. 9 | # The most common formats are the USTAR, PAX, and GNU formats, 10 | # each with their own advantages and limitations. 11 | # 12 | # The following table captures the capabilities of each format: 13 | # 14 | # | USTAR | PAX | GNU 15 | # ------------------+--------+-----------+---------- 16 | # Name | 256B | unlimited | unlimited 17 | # Linkname | 100B | unlimited | unlimited 18 | # Size | uint33 | unlimited | uint89 19 | # Mode | uint21 | uint21 | uint57 20 | # Uid/Gid | uint21 | unlimited | uint57 21 | # Uname/Gname | 32B | unlimited | 32B 22 | # ModTime | uint33 | unlimited | int89 23 | # AccessTime | n/a | unlimited | int89 24 | # ChangeTime | n/a | unlimited | int89 25 | # Devmajor/Devminor | uint21 | uint21 | uint57 26 | # ------------------+--------+-----------+---------- 27 | # string encoding | ASCII | UTF-8 | binary 28 | # sub-second times | no | yes | no 29 | # sparse files | no | yes | yes 30 | # 31 | # The table's upper portion shows the Header fields, where each format reports 32 | # the maximum number of bytes allowed for each string field and 33 | # the integer type used to store each numeric field 34 | # (where timestamps are stored as the number of seconds since the Unix epoch). 35 | # 36 | # The table's lower portion shows specialized features of each format, 37 | # such as supported string encodings, support for sub-second timestamps, 38 | # or support for sparse files. 39 | # 40 | # The Writer currently provides no support for sparse files. 41 | 42 | # Various Crystar formats 43 | @[Flags] 44 | enum Format 45 | # The format of the original Unix V7 tar tool prior to standardization. 46 | V7 47 | # USTAR represents the USTAR header format defined in POSIX.1-1988. 48 | # 49 | # While this format is compatible with most tar readers, 50 | # the format has several limitations making it unsuitable for some usages. 51 | # Most notably, it cannot support sparse files, files larger than 8GiB, 52 | # filenames larger than 256 characters, and non-ASCII filenames. 53 | # 54 | # Reference: 55 | # http:#pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html#tag_20_92_13_06 56 | USTAR 57 | # PAX represents the PAX header format defined in POSIX.1-2001. 58 | # 59 | # PAX extends USTAR by writing a special file with Typeflag TypeXHeader 60 | # preceding the original header. This file contains a set of key-value 61 | # records, which are used to overcome USTAR's shortcomings, in addition to 62 | # providing the ability to have sub-second resolution for timestamps. 63 | # 64 | # Some newer formats add their own extensions to PAX by defining their 65 | # own keys and assigning certain semantic meaning to the associated values. 66 | # For example, sparse file support in PAX is implemented using keys 67 | # defined by the GNU manual (e.g., "GNU.sparse.map"). 68 | # 69 | # Reference: 70 | # http:#pubs.opengroup.org/onlinepubs/009695399/utilities/pax.html 71 | PAX 72 | # GNU represents the GNU header format. 73 | # 74 | # The GNU header format is older than the USTAR and PAX standards and 75 | # is not compatible with them. The GNU format supports 76 | # arbitrary file sizes, filenames of arbitrary encoding and length, 77 | # sparse files, and other features. 78 | # 79 | # It is recommended that PAX be chosen over GNU unless the target 80 | # application can only parse GNU formatted archives. 81 | # 82 | # Reference: 83 | # https:#www.gnu.org/softwarecrystar/manual/html_node/Standard.html 84 | GNU 85 | # Schily's tar format, which is incompatible with USTAR. 86 | # This does not cover STAR extensions to the PAX format; these fall under 87 | # the PAX format. 88 | STAR 89 | 90 | def has(f2 : self) 91 | (self & f2) != None 92 | end 93 | 94 | def maybe(f2 : self) 95 | self.| f2 96 | end 97 | 98 | def may_only_be(f2) 99 | self.& f2 100 | end 101 | 102 | def must_not_be(f2) 103 | self.& ~f2 104 | end 105 | end 106 | 107 | # Magics used to identify various formats. 108 | MAGIC_GNU = "ustar " 109 | VERSION_GNU = " \x00" 110 | MAGIC_USTAR = "ustar\x00" 111 | VERSION_USTAR = "00" 112 | TRAILER_STAR = "tar\x00" 113 | 114 | BLOCK_SIZE = 512 # Size of each block in a tar stream 115 | NAME_SIZE = 100 # Max length of the name in USTAR format 116 | PREFIX_SIZE = 155 # Max length of the prefix field in USTAR format 117 | 118 | # block_padding computes the number of bytes needed to pad offset up to the 119 | # nearest block edge where 0 <= n < blockSize. 120 | def block_padding(offset : Int) 121 | -offset.to_i64 & (BLOCK_SIZE - 1) 122 | end 123 | 124 | private class Block 125 | @@zero_block : self = self.new 126 | forward_missing_to @block 127 | 128 | def initialize 129 | @block = Bytes.new(BLOCK_SIZE) 130 | end 131 | 132 | def initialize(@block : Bytes) 133 | end 134 | 135 | def to_bytes 136 | @block 137 | end 138 | 139 | def self.zero_block 140 | @@zero_block 141 | end 142 | 143 | def v7 144 | HeaderV7.new(@block) 145 | end 146 | 147 | def gnu 148 | HeaderGNU.new(@block) 149 | end 150 | 151 | def star 152 | HeaderSTAR.new(@block) 153 | end 154 | 155 | def ustar 156 | HeaderUSTAR.new(@block) 157 | end 158 | 159 | def sparse 160 | SparseArray.new(@block[...]) 161 | end 162 | 163 | # get_format checks that the block is a valid tar header based on the checksum. 164 | # It then attempts to guess the specific format based on magic values. 165 | # If the checksum fails, then Format::None is returned. 166 | def get_format 167 | # Verify checksum 168 | p = Parser.new 169 | value = p.parse_octal(v7.chksum) 170 | chksum1, chksum2 = compute_checksum 171 | return Format::None if value != chksum1 && value != chksum2 172 | 173 | # Guess the magic values. 174 | magic = String.new(ustar.magic) 175 | version = String.new(ustar.version) 176 | trailer = String.new(star.trailer) 177 | case 178 | when magic == MAGIC_USTAR && trailer == TRAILER_STAR 179 | Format::STAR 180 | when magic == MAGIC_USTAR 181 | Format::USTAR | Format::PAX 182 | when magic == MAGIC_GNU && version == VERSION_GNU 183 | Format::GNU 184 | else 185 | Format::V7 186 | end 187 | end 188 | 189 | # set_format writes the magic values necessary for specified format 190 | # and then updates the checksum accordingly. 191 | 192 | def set_format(format : Format) : Nil 193 | # Set the magic values 194 | case 195 | when format.has(Format::V7) 196 | # Do nothing 197 | when format.has(Format::GNU) 198 | gnu.magic = MAGIC_GNU 199 | gnu.version = VERSION_GNU 200 | when format.has(Format::STAR) 201 | star.magic = MAGIC_USTAR 202 | star.version = VERSION_USTAR 203 | star.trailer = TRAILER_STAR 204 | when format.has(Format::USTAR | Format::PAX) 205 | ustar.magic = MAGIC_USTAR 206 | ustar.version = VERSION_USTAR 207 | else 208 | raise Error.new("invalid format") 209 | end 210 | 211 | # Update checksum 212 | # This field is special in that it is terminated by a NULL then space. 213 | 214 | f = Formatter.new 215 | field = v7.chksum 216 | chksum, _ = compute_checksum # Possible values are 256..128776 217 | f.format_octal(field[...7], chksum) 218 | field[7] = ' '.ord.to_u8 219 | end 220 | 221 | # compute_checksum computes the checksum for the header block. 222 | # POSIX specifies a sum of the unsigned byte values, but the Sun tar used 223 | # signed byte values. 224 | # We compute and return both. 225 | def compute_checksum 226 | u = s = 0_i64 227 | @block.each_with_index do |c, i| 228 | if 148 <= i && i < 156 229 | c = ' '.ord 230 | end 231 | u += c.to_i64 232 | s += c.to_i64 233 | end 234 | {u, s} 235 | end 236 | 237 | # reset clears the block with all zeros 238 | def reset 239 | p = @block.to_unsafe 240 | p.clear 241 | @block = p.to_slice(@block.size) 242 | end 243 | end 244 | 245 | private class HeaderV7 246 | def initialize(@h : Bytes) 247 | end 248 | 249 | def name 250 | @h[0..][...100] 251 | end 252 | 253 | def mode 254 | @h[100..][...8] 255 | end 256 | 257 | def uid 258 | @h[108..][...8] 259 | end 260 | 261 | def gid 262 | @h[116..][...8] 263 | end 264 | 265 | def size 266 | @h[124..][...12] 267 | end 268 | 269 | def mod_time 270 | @h[136..][...12] 271 | end 272 | 273 | def chksum 274 | @h[148..][...8] 275 | end 276 | 277 | def flag 278 | @h[156..][...1] 279 | end 280 | 281 | def link_name 282 | @h[157..][...100] 283 | end 284 | end 285 | 286 | private class HeaderGNU 287 | def initialize(@h : Bytes) 288 | end 289 | 290 | def v7 291 | HeaderV7.new(@h) 292 | end 293 | 294 | def magic 295 | @h[257..][...6] 296 | end 297 | 298 | def magic=(s : String) 299 | set(magic, s) 300 | end 301 | 302 | def version 303 | @h[263..][...2] 304 | end 305 | 306 | def version=(s : String) 307 | set(version, s) 308 | end 309 | 310 | def user_name 311 | @h[265..][...32] 312 | end 313 | 314 | def group_name 315 | @h[297..][...32] 316 | end 317 | 318 | def dev_major 319 | @h[329..][...8] 320 | end 321 | 322 | def dev_minor 323 | @h[337..][...8] 324 | end 325 | 326 | def access_time 327 | @h[345..][...12] 328 | end 329 | 330 | def change_time 331 | @h[357..][...12] 332 | end 333 | 334 | def sparse 335 | SparseArray.new @h[386..][...24*4 + 1] 336 | end 337 | 338 | def real_size 339 | @h[483..][...12] 340 | end 341 | 342 | private def set(h : Bytes, s : String) 343 | h.copy_from(s.to_slice.to_unsafe, h.size) 344 | end 345 | end 346 | 347 | private class HeaderSTAR 348 | def initialize(@h : Bytes) 349 | end 350 | 351 | def v7 352 | HeaderV7.new(@h) 353 | end 354 | 355 | def magic 356 | @h[257..][...6] 357 | end 358 | 359 | def magic=(s : String) 360 | set(magic, s) 361 | end 362 | 363 | def version 364 | @h[263..][...2] 365 | end 366 | 367 | def version=(s : String) 368 | set(version, s) 369 | end 370 | 371 | def user_name 372 | @h[265..][...32] 373 | end 374 | 375 | def group_name 376 | @h[297..][...32] 377 | end 378 | 379 | def dev_major 380 | @h[329..][...8] 381 | end 382 | 383 | def dev_minor 384 | @h[337..][...8] 385 | end 386 | 387 | def prefix 388 | @h[345..][...131] 389 | end 390 | 391 | def access_time 392 | @h[476..][...12] 393 | end 394 | 395 | def change_time 396 | @h[488..][...12] 397 | end 398 | 399 | def trailer 400 | @h[508..][...4] 401 | end 402 | 403 | def trailer=(s : String) 404 | set(trailer, s) 405 | end 406 | 407 | private def set(h : Bytes, s : String) 408 | h.copy_from(s.to_slice.to_unsafe, h.size) 409 | end 410 | end 411 | 412 | private class HeaderUSTAR 413 | def initialize(@h : Bytes) 414 | end 415 | 416 | def v7 417 | HeaderV7.new(@h) 418 | end 419 | 420 | def magic 421 | @h[257..][...6] 422 | end 423 | 424 | def magic=(s : String) 425 | set(magic, s) 426 | end 427 | 428 | def version 429 | @h[263..][...2] 430 | end 431 | 432 | def version=(s : String) 433 | set(version, s) 434 | end 435 | 436 | def user_name 437 | @h[265..][...32] 438 | end 439 | 440 | def group_name 441 | @h[297..][...32] 442 | end 443 | 444 | def dev_major 445 | @h[329..][...8] 446 | end 447 | 448 | def dev_minor 449 | @h[337..][...8] 450 | end 451 | 452 | def prefix 453 | @h[345..][...155] 454 | end 455 | 456 | private def set(h : Bytes, s : String) 457 | h.copy_from(s.to_slice.to_unsafe, h.size) 458 | end 459 | end 460 | 461 | private class SparseArray 462 | def initialize(@s : Bytes) 463 | end 464 | 465 | def entry(i : Int32) 466 | SparseElem.new(@s[i*24..]) 467 | end 468 | 469 | def is_extended 470 | @s[24*max_entries..][...1] 471 | end 472 | 473 | def max_entries 474 | @s.size // 24 475 | end 476 | end 477 | 478 | private class SparseElem 479 | def initialize(@s : Bytes) 480 | end 481 | 482 | def offset 483 | @s[0..][...12] 484 | end 485 | 486 | def length 487 | @s[12..][...12] 488 | end 489 | end 490 | end 491 | -------------------------------------------------------------------------------- /src/tar/helper.cr: -------------------------------------------------------------------------------- 1 | require "time" 2 | 3 | module Crystar 4 | extend self 5 | 6 | MAX_NANO_SECOND_DIGITS = 9 7 | PADDING = 3 # Extra padding for ' ', '=', and '\n' 8 | 9 | # checks whether NUL character exists within s 10 | def has_nul(s : String) 11 | s.byte_index(0) ? true : false 12 | end 13 | 14 | # to_ascii converts the input to an ASCII C-style string. 15 | # This a best effort conversion, so invalid characters are dropped. 16 | def to_ascii(s : String) 17 | return s if s.ascii_only? 18 | b = Bytes.new(s.size) 19 | index = 0 20 | s.bytes.each do |c| 21 | if c < 0x80 && c != 0x00 22 | b[index] = c 23 | index += 1 24 | end 25 | end 26 | String.new(b[...index]) 27 | end 28 | 29 | # split_ustar_path splits a path according to USTAR prefix and suffix rules. 30 | # If the path is not splittable, then it will return ("", "", false). 31 | 32 | def split_ustar_path(name : String) 33 | length = name.size 34 | return "", "", false if length <= NAME_SIZE || !name.ascii_only? 35 | length = PREFIX_SIZE + 1 if length > PREFIX_SIZE + 1 36 | length -= 1 if name.char_at(name.size - 1) == '/' 37 | 38 | i = name[..length].rindex("/") 39 | if !i 40 | return "", "", false 41 | else 42 | nlen = name.size - i - 1 # nlen is length of suffix 43 | plen = i # plen is length of prefix 44 | if i <= 0 || nlen > NAME_SIZE || nlen == 0 || plen > PREFIX_SIZE 45 | return "", "", false 46 | else 47 | return name[..i], name[i + 1..], true 48 | end 49 | end 50 | end 51 | 52 | # fits_in_base256 reports whether x can be encoded into n bytes using base-256 53 | # encoding. Unlike octal encoding, base-256 encoding does not require that the 54 | # string ends with a NUL character. Thus, all n bytes are available for output. 55 | # 56 | # If operating in binary mode, this assumes strict GNU binary mode; which means 57 | # that the first byte can only be either 0x80 or 0xff. Thus, the first byte is 58 | # equivalent to the sign bit in two's complement form. 59 | 60 | def fits_in_base256(n : Int32, x : Int64) 61 | bin_bits = (n - 1).to_u32 * 8 62 | n >= 9 || (x >= -1_i64 << bin_bits && x < 1_i64 << bin_bits) 63 | end 64 | 65 | # fits_in_octal reports whether the integer x fits in a field n-bytes long 66 | # using octal encoding with the appropriate NUL terminator. 67 | def fits_in_octal(n : Int32, x : Int64) 68 | oct_bits = (n - 1).to_u32 * 3 69 | x >= 0 && (n >= 22 || x < 1_i64 << oct_bits) 70 | end 71 | 72 | # parse_pax_time takes a string of the form %d.%d as described in the PAX 73 | # specification. Note that this implementation allows for negative timestamps, 74 | # which is allowed for by the PAX specification, but not always portable. 75 | def parse_pax_time(s : String) 76 | return unix_time(0, 0) unless s.size > 0 77 | ss, sn = s, "" 78 | if (pos = s.index('.')) && (pos >= 0) 79 | ss, sn = s[...pos], s[pos + 1..] 80 | end 81 | 82 | # Parse the seconds 83 | begin 84 | secs = ss.to_i64 85 | rescue 86 | raise Error.new("invalid tar header") 87 | end 88 | 89 | return unix_time(secs, 0) unless sn.size > 0 # No sub-second values 90 | 91 | # Parse the nanoseconds. 92 | raise "invalid tar header" unless sn.strip("0123456789") == "" 93 | 94 | if sn.size < MAX_NANO_SECOND_DIGITS 95 | sn += ("0" * (MAX_NANO_SECOND_DIGITS - sn.size)) 96 | else 97 | sn = sn[...MAX_NANO_SECOND_DIGITS] # Right truncate 98 | end 99 | 100 | nsecs = sn.to_i64 # Must succeed 101 | return unix_time(secs, -1*nsecs) if ss.size > 0 && ss[0] == '-' # negative correction 102 | 103 | unix_time(secs, nsecs) 104 | end 105 | 106 | # format_pax_time converts ts into a time of the form %d.%d as described in the 107 | # PAX specification. This function is capable of negative timestamps. 108 | def format_pax_time(ts : Time) 109 | secs, nsecs = ts.to_unix, ts.nanosecond 110 | return secs.to_s if nsecs == 0 111 | 112 | # If seconds is negative, then perform correction 113 | sign = "" 114 | if secs < 0 115 | sign = "-" # Remember sign 116 | secs = -(secs + 1) # Add a second to secs 117 | nsecs = -(nsecs - 1e9) # Take that second away from nsecs 118 | end 119 | sprintf("%s%d.%09d", [sign, secs, nsecs]).rstrip("0") 120 | end 121 | 122 | # parse_pax_record parses the input PAX record string into a key-value pair. 123 | # If parsing is successful, it will slice off the currently read record and 124 | # return the remainder as r. 125 | def parse_pax_record(s : String) : {String, String, String} 126 | # The size field ends at the first space. 127 | sp = byte_index(s, ' ') 128 | raise Error.new "invalid tar header" unless sp >= 0 129 | 130 | # Parse the first token as a decimal integer 131 | n = s[...sp].to_i { 0 } # Intentionally parse as native int 132 | raise Error.new "invalid tar header" if n < 5 || s.bytesize < n 133 | 134 | # Extract everything between the space and the final newline. 135 | rec = s.byte_slice(sp + 1, n - sp - 2) 136 | nl = s.byte_slice(n - 1, 1) 137 | rem = s.byte_slice(n) 138 | # return {"", "", s} unless nl == "\n" 139 | raise Error.new "invalid tar header" unless nl == "\n" 140 | 141 | # The first equals separates the key from the value 142 | eq = byte_index(rec, '=') 143 | return {"", "", s} unless eq >= 0 144 | k = rec.byte_slice(0, eq) 145 | v = rec.byte_slice(eq + 1) 146 | raise Error.new "invalid tar header" unless valid_pax_record(k, v) 147 | {k, v, rem} 148 | end 149 | 150 | # format_pax_record formats a single PAX record, prefixing it with the 151 | # appropriate length 152 | def format_pax_record(k : String, v : String) 153 | raise Error.new "invalid tar header" unless valid_pax_record(k, v) 154 | size = k.bytesize + v.bytesize + PADDING 155 | size += size.to_s.size 156 | rec = "#{size} #{k}=#{v}\n" 157 | # Final adjustment if adding size field increased the record size. 158 | if rec.bytesize != size 159 | size = rec.bytesize 160 | rec = "#{size} #{k}=#{v}\n" 161 | end 162 | rec 163 | end 164 | 165 | # valid_pax_record reports whether the key-value pair is valid where each 166 | # record is formatted as: 167 | # "%d %s=%s\n" % (size, key, value) 168 | # 169 | # Keys and values should be UTF-8, but the number of bad writers out there 170 | # forces us to be a more liberal. 171 | # Thus, we only reject all keys with NUL, and only reject NULs in values 172 | # for the PAX version of the USTAR string fields. 173 | # The key must not contain an '=' character. 174 | def valid_pax_record(k : String, v : String) 175 | return false if k.blank? || byte_index(k, '=') >= 0 176 | case k 177 | when PAX_PATH, PAX_LINK_PATH, PAX_UNAME, PAX_GNAME 178 | !has_nul(v) 179 | else 180 | !has_nul(k) 181 | end 182 | end 183 | 184 | def byte_index(bytes : Bytes, b : Int) 185 | 0.upto(bytes.size - 1) do |i| 186 | if bytes[i] == b 187 | return i 188 | end 189 | end 190 | -1 191 | end 192 | 193 | def byte_index(s : String, c : Char) 194 | byte_index(s.to_slice, c.ord) 195 | end 196 | 197 | def ltrim(b : Bytes, s : String) 198 | left = 0 199 | b.each do |c| 200 | if s.includes?(c.unsafe_chr) 201 | left += 1 202 | else 203 | break 204 | end 205 | end 206 | b[left..] 207 | end 208 | 209 | def rtrim(b : Bytes, s : String) 210 | a = b.dup.reverse! 211 | right = b.size - 1 212 | a.each do |c| 213 | if s.includes?(c.unsafe_chr) 214 | right -= 1 215 | else 216 | break 217 | end 218 | end 219 | b[..right] 220 | end 221 | 222 | def trim_bytes(b : Bytes, s : String) 223 | rtrim(ltrim(b, s), s) 224 | end 225 | 226 | def unix_time(sec : Int, nsec : Int) 227 | ts = Time.unix(sec) 228 | ts.shift(0, nsec) 229 | end 230 | 231 | def unix_time(sec, nsec) 232 | unix_time(sec.to_i64, nsec.to_i64) 233 | end 234 | 235 | private class Parser 236 | # parse_string parses bytes as a NUL-terminated C-style string. 237 | # If a NUL byte is not found then the whole slice is returned as a string. 238 | def parse_string(b : Bytes) 239 | if (i = Crystar.byte_index(b, 0)) && (i >= 0) 240 | String.new(b[...i]) 241 | else 242 | String.new(b) 243 | end 244 | end 245 | 246 | # parse_numeric parses the input as being encoded in either base-256 or octal. 247 | # This function may return negative numbers. 248 | # If parsing fails or an integer overflow occurs, err will be set. 249 | def parse_numeric(b : Bytes) : Int64 250 | # Check for base-256 (binary) format first. 251 | # If the first bit is set, then all following bits constitue a two's 252 | # complement encoded number in big-endian byte order. 253 | if b.size > 0 && b[0] & 0x80 != 0 254 | # Handling negative numbers relies on the following identity: 255 | # -a-1 == ^a 256 | # 257 | # If the number is negative, we use an inversion mask to invert the 258 | # data bytes and treat the value as an unsigned number. 259 | inv = b[0] & 0x40 != 0 ? 0xff_u8 : 0_u8 260 | x = 0_u64 261 | b.each_with_index do |c, i| 262 | c ^= inv # inverts c only if inv is oxff, otherwise does nothing 263 | c &= 0x7f if i == 0 # Ignore signal bit in first byte 264 | raise Error.new("invalid tar header") if (x >> 56) > 0 265 | x = x << 8 | c.to_u64 266 | end 267 | raise Error.new("invalid tar header") if (x >> 63) > 0 268 | return ~x.to_i64 if inv == 0xff 269 | return x.to_i64 270 | end 271 | parse_octal(b) 272 | end 273 | 274 | def parse_octal(b : Bytes) : Int64 275 | # Because unused fields are filled with NULs, we need 276 | # to skip leading NULs. Fields may also be padded with 277 | # spaces or NULs. 278 | # So we remove leading and trailing NULs and spaces to 279 | # be sure. 280 | b = Crystar.trim_bytes(b, " \x00") 281 | return 0.to_i64 if b.size == 0 282 | begin 283 | (parse_string(b).to_u64(base: 8)).to_i64 284 | rescue exc 285 | raise Error.new "invalid tar header" 286 | end 287 | end 288 | end 289 | 290 | private class Formatter 291 | def initialize(@ignore_errors = false) 292 | end 293 | 294 | # format_string copies s into b, NUL-terminating if possible 295 | def format_string(b : Bytes, s : String) : Nil 296 | raise Error.new("header field too long") if !@ignore_errors && s.size > b.size 297 | size = s.bytesize > b.size ? b.size : s.bytesize 298 | b.copy_from(s.to_slice.to_unsafe, size) 299 | b[s.size] = 0 if s.size < b.size 300 | 301 | # Some buggy readers treat regular files with a trailing slash 302 | # in the V7 path field as a directory even though the full path 303 | # recorded elsewhere (e.g., via PAX record) contains no trailing slash. 304 | if b[b.size - 1] == '/'.ord 305 | n = s[...b.size].rstrip('/').size 306 | b[n] = 0 # Replace trailing slash with NUL terminator 307 | end 308 | end 309 | 310 | # format_numeric encodes x into b using base-8 (octal) encoding if possible. 311 | # Otherwise it will attempt to use base-256 (binary) encoding. 312 | def format_numeric(b : Bytes, x : Int64) : Nil 313 | if Crystar.fits_in_octal(b.size, x) 314 | format_octal(b, x) 315 | return 316 | end 317 | 318 | if Crystar.fits_in_base256(b.size, x) 319 | (b.size - 1).downto 0 do |i| 320 | b[i] = x.to_u8 321 | x >>= 8_i64 322 | end 323 | b[0] |= 0x80 # Highest bit indicates binary format 324 | return 325 | end 326 | 327 | format_octal(b, 0) # Last resort, just write zero 328 | raise Error.new("header field too long") if !@ignore_errors 329 | end 330 | 331 | def format_octal(b : Bytes, x : Int64) : Nil 332 | raise Error.new("header field too long") if !@ignore_errors && !Crystar.fits_in_octal(b.size, x) 333 | 334 | s = x.to_s(8) # .to_i64.to_s 335 | if (n = b.size - s.size - 1) && (n > 0) 336 | s = ("0" * n) + s 337 | end 338 | format_string(b, s) 339 | end 340 | end 341 | end 342 | -------------------------------------------------------------------------------- /src/tar/writer.cr: -------------------------------------------------------------------------------- 1 | require "math" 2 | 3 | module Crystar 4 | # Writer provides sequential writing of a tar archive. 5 | # Writer#write_header begins a new file with the provided Header, 6 | # and then Writer can be treated as an io.Writer to supply that file's data via invoking Writer#write method . 7 | # 8 | # ### Example 9 | # ``` 10 | # require "tar" 11 | # 12 | # File.open("./file.tar", "w") do |file| 13 | # Crystar::Writer.open(file) do |tar| 14 | # # add file to archive 15 | # tar.add File.open("./some_file.txt") 16 | # # Manually create the Header with info per your choice 17 | # hdr = Header.new( 18 | # name: "Your file Name", 19 | # size: 100_i64, # Contents size 20 | # mode: 0o644_i64 # Permission and mode bits 21 | # # ..... Look into `Crystar::Header` 22 | # ) 23 | # tar.write_header hdr 24 | # tar.write "your file contents".to_slice 25 | # 26 | # # Create header from File you have already opened. 27 | # hdr = file_info_header(file, file.path) 28 | # tar.write_header hdr 29 | # tar.write file.gets_to_end.to_slice 30 | # end 31 | # end 32 | # ``` 33 | class Writer 34 | # Whether to close the enclosed `IO` when closing this writer. 35 | property? sync_close = false 36 | 37 | # Returns `true` if this writer is closed. 38 | getter? closed = false 39 | # Amount of padding to write after current file entry 40 | @pad = 0_i64 41 | # Writer for current file entry 42 | @curr : FileWriter 43 | # Copy of Header that is safe for mutations 44 | setter hdr : Header 45 | # Buffer to use as temporary local storage 46 | @block : Block 47 | 48 | # Creates a new writer to the given *io*. 49 | def initialize(@io : IO, @sync_close = false) 50 | @block = Block.new 51 | @curr = RegFileWriter.new(@io, 0_i64) 52 | @hdr = Header.new 53 | end 54 | 55 | # Creates a new writer to the given *filename*. 56 | def self.new(filename : String) 57 | new(::File.new(filename, "w"), sync_close: true) 58 | end 59 | 60 | # Creates a new writer to the given *io*, yields it to the given block, 61 | # and closes it at the end. 62 | def self.open(io : IO, sync_close = false, &) 63 | writer = new(io, sync_close: sync_close) 64 | yield writer ensure writer.close 65 | end 66 | 67 | # Creates a new writer to the given *filename*, yields it to the given block, 68 | # and closes it at the end. 69 | def self.open(filename : String, &) 70 | writer = new(filename) 71 | yield writer ensure writer.close 72 | end 73 | 74 | # Adds an entry that will have its data copied from the given *file*. 75 | # file is automatically closed after data is copied from it. 76 | def add(file : File) 77 | hdr = Crystar.file_info_header(file, file.path) 78 | write_header hdr 79 | IO.copy(file, @curr) 80 | file.close 81 | end 82 | 83 | # Close closes the tar archive by flushing the padding, and writing the footer. 84 | # If the current file (from a prior call to WriteHeader) is not fully written, 85 | # then this returns an error. 86 | def close 87 | return if @closed 88 | # Trailer: two zero blocks. 89 | flush() 90 | 0.upto(1) do |_| 91 | @io.write(Block.zero_block.to_bytes[..]) 92 | end 93 | @io.close if @sync_close 94 | @closed = true 95 | end 96 | 97 | # :nodoc: 98 | def flush 99 | raise Error.new("Can't write to closed writer") if @closed 100 | 101 | if (nb = @curr.logical_remaining) && nb > 0 102 | raise Error.new "tar: missed writing #{nb} bytes" 103 | end 104 | 105 | @io.write(Block.zero_block.to_bytes[...@pad]) 106 | @pad = 0 107 | nil 108 | end 109 | 110 | # write writes to the current file in the tar archive. 111 | # write returns the error ErrWriteTooLong if more than 112 | # Header#size bytes are written after WriteHeader. 113 | # 114 | # Calling write on special types like LINK, SYMLINK, CHAR, 115 | # BLOCK, DIR, and FIFO returns (0, ErrWriteTooLong) regardless 116 | # of what the Header#size claims. 117 | def write(b : Bytes) : Nil 118 | raise Error.new("Can't write to closed writer") if @closed 119 | @curr.write(b) 120 | end 121 | 122 | # write_header writes hdr and prepares to accept the file's contents. 123 | # The Header#size determines how many bytes can be written for the next file. 124 | # If the current file is not fully written, then this returns an error. 125 | # This implicitly flushes any padding necessary before writing the header. 126 | def write_header(hdr : Header) : Nil 127 | flush() 128 | 129 | @hdr = hdr # Shallow copy of header 130 | 131 | # Avoid usage of the legacy REGA flag, and automatically promote 132 | # it to use REG or DIR 133 | if @hdr.flag == REGA 134 | if @hdr.name.ends_with?("/") 135 | @hdr.flag = DIR.ord.to_u8 136 | else 137 | @hdr.flag = REG.ord.to_u8 138 | end 139 | end 140 | 141 | # Round ModTime and ignore AccessTime and ChangeTime unless 142 | # the format is explicitly chosen. 143 | # This ensures nominal usage of WriteHeader (without specifying the format) 144 | # does not always result in the PAX format being chosen, which 145 | # causes a 1KiB increase to every header. 146 | 147 | if @hdr.format.none? 148 | # TO-DO 149 | # Add round time 150 | # hdr.mod_time = round_time(SECOND) 151 | @hdr.access_time = Crystar.unix_time(0, 0) 152 | @hdr.change_time = Crystar.unix_time(0, 0) 153 | end 154 | 155 | allowed_formats, pax_hdrs = @hdr.allowed_formats 156 | case 157 | when allowed_formats.has(Format::USTAR) 158 | write_ustar_header(@hdr) 159 | when allowed_formats.has(Format::PAX) 160 | write_pax_header(@hdr, pax_hdrs) 161 | when allowed_formats.has(Format::GNU) 162 | write_gnu_header(@hdr) 163 | end 164 | end 165 | 166 | # :nodoc: 167 | private def write_ustar_header(hdr : Header) : Nil 168 | # Check if we can use USTAR prefix/suffix splitting. 169 | name_prefix = "" 170 | prefix, suffix, ok = Crystar.split_ustar_path(hdr.name) 171 | if ok 172 | name_prefix, hdr.name = prefix, suffix 173 | end 174 | 175 | # Pack the main header. 176 | f = Formatter.new 177 | blk = template_v7_plus(hdr, ->f.format_string(Bytes, String), ->f.format_octal(Bytes, Int64)) 178 | f.format_string(blk.ustar.prefix, name_prefix) 179 | blk.set_format(Format::USTAR) 180 | 181 | write_raw_header(blk, hdr.size, hdr.flag) 182 | end 183 | 184 | # :nodoc: 185 | private def write_pax_header(hdr : Header, pax_hdrs : Hash(String, String)) : Nil 186 | # real_name, real_size = hdr.name, hdr.size 187 | real_name, _ = hdr.name, hdr.size 188 | # TO-DO 189 | # Add sparse support 190 | 191 | # Write PAX records to the output. 192 | is_global = hdr.flag == XGLOBAL_HEADER 193 | if pax_hdrs.size > 0 || is_global 194 | # Sort keys for deterministic ordering. 195 | keys = pax_hdrs.keys.sort! 196 | 197 | # Write each record to a buffer 198 | data = String.build do |buf| 199 | keys.each do |k| 200 | rec = Crystar.format_pax_record(k, pax_hdrs.fetch(k, "")) 201 | buf << rec 202 | end 203 | end 204 | 205 | # Write the extended header file. 206 | name = "" 207 | flag = 0_u8 208 | if is_global 209 | name = real_name.blank? ? "GlobalHead.0.0" : real_name 210 | flag = XGLOBAL_HEADER.ord.to_u8 211 | else 212 | p = Path[real_name] 213 | if p.dirname == "." 214 | name = Path.new("PaxHeaders.0", p.basename).to_s 215 | else 216 | name = Path.new(p.dirname, "PaxHeaders.0", p.basename).to_s 217 | end 218 | flag = XHEADER.ord.to_u8 219 | end 220 | write_raw_file(name, data, flag, Format::PAX) 221 | end 222 | 223 | # Pack the main header. 224 | f = Formatter.new(true) # Ignore errors since they are expected 225 | fmt_str = ->(b : Bytes, s : String) { 226 | f.format_string(b, Crystar.to_ascii(s)) 227 | } 228 | blk = template_v7_plus(hdr, fmt_str, ->f.format_octal(Bytes, Int64)) 229 | blk.set_format(Format::PAX) 230 | write_raw_header(blk, hdr.size, hdr.flag) 231 | 232 | # TO-DO 233 | # Add sparse support 234 | end 235 | 236 | # :nodoc: 237 | private def write_gnu_header(hdr : Header) : Nil 238 | if hdr.name.size > NAME_SIZE 239 | data = hdr.name + "\x00" 240 | write_raw_file(LONG_NAME, data, GNU_LONGNAME.ord.to_u8, Format::GNU) 241 | end 242 | if hdr.link_name.size > NAME_SIZE 243 | data = hdr.link_name + "\x00" 244 | write_raw_file(LONG_NAME, data, GNU_LONGLINK.ord.to_u8, Format::GNU) 245 | end 246 | 247 | # Pack the main header. 248 | f = Formatter.new(true) # Ignore errors since they are expected 249 | spd = SparseDatas.new 250 | spb = Bytes.new(0) 251 | blk = template_v7_plus(hdr, ->f.format_string(Bytes, String), ->f.format_numeric(Bytes, Int64)) 252 | if hdr.access_time.to_unix != 0 253 | f.format_numeric(blk.gnu.access_time, hdr.access_time.to_unix) 254 | end 255 | if hdr.change_time.to_unix != 0 256 | f.format_numeric(blk.gnu.change_time, hdr.change_time.to_unix) 257 | end 258 | # TO-DO 259 | # Add Sparse support 260 | 261 | blk.set_format(Format::GNU) 262 | write_raw_header(blk, hdr.size, hdr.flag) 263 | 264 | # Write the extended sparse map and setup the sparse writer if necessary. 265 | if spd.size > 0 266 | # Use @io since the sparse map is not accounted for in hdr.size 267 | @io.write(spb) 268 | @curr = SparseFileWriter.new(@curr, spd, 0) 269 | end 270 | end 271 | 272 | # template_v7_plus fills out the V7 fields of a block using values from hdr. 273 | # It also fills out fields (uname, gname, devmajor, devminor) that are 274 | # shared in the USTAR, PAX, and GNU formats using the provided formatters. 275 | # 276 | # The block returned is only valid until the next call to 277 | # templateV7Plus or write_raw_file. 278 | private def template_v7_plus(hdr : Header, fmt_str : StringFormatter, fmt_num : NumberFormatter) 279 | @block.reset 280 | 281 | # mod_time = hdr.mod_time 282 | 283 | v7 = @block.v7 284 | v7.flag[0] = hdr.flag 285 | fmt_str.call(v7.name, hdr.name) 286 | fmt_str.call(v7.link_name, hdr.link_name) 287 | fmt_num.call(v7.mode, hdr.mode) 288 | fmt_num.call(v7.uid, hdr.uid.to_i64) 289 | fmt_num.call(v7.gid, hdr.gid.to_i64) 290 | fmt_num.call(v7.size, hdr.size) 291 | fmt_num.call(v7.mod_time, hdr.mod_time.to_unix) 292 | 293 | ustar = @block.ustar 294 | fmt_str.call(ustar.user_name, hdr.uname) 295 | fmt_str.call(ustar.group_name, hdr.gname) 296 | fmt_num.call(ustar.dev_major, hdr.dev_major) 297 | fmt_num.call(ustar.dev_minor, hdr.dev_minor) 298 | 299 | @block 300 | end 301 | 302 | # write_raw_file writes a minimal file with the given name and flag type. 303 | # It uses format to encode the header format and will write data as the body. 304 | # It uses default values for all of the other fields (as BSD and GNU tar does). 305 | private def write_raw_file(name : String, data : String, flag : UInt8, format : Format) : Nil 306 | @block.reset 307 | 308 | # Best effort for the filename 309 | name = Crystar.to_ascii(name) 310 | name = name[...NAME_SIZE] if name.size > NAME_SIZE 311 | name = name.rstrip("/") 312 | 313 | f = Formatter.new 314 | v7 = @block.v7 315 | v7.flag[0] = flag.to_u8 316 | f.format_string(v7.name, name) 317 | f.format_octal(v7.mode, 0) 318 | f.format_octal(v7.uid, 0) 319 | f.format_octal(v7.gid, 0) 320 | f.format_octal(v7.size, data.bytesize.to_i64) # Must be < 8GiB 321 | f.format_octal(v7.mod_time, 0) 322 | @block.set_format(format) 323 | 324 | # Write the header and data 325 | write_raw_header(@block, data.bytesize.to_i64, flag.to_u8) 326 | @curr.puts data 327 | end 328 | 329 | # write_raw_header writes the value of blk, regardless of its value. 330 | # It sets up the Writer such that it can accept a file of the given size. 331 | # If the flag is a special header-only flag, then the size is treated as zero. 332 | private def write_raw_header(blk : Block, size : Int, flag : UInt8) : Nil 333 | flush 334 | @io.write(blk.to_bytes[..]) 335 | size = 0 if Crystar.header_only_type?(flag.to_u8) 336 | 337 | @curr = RegFileWriter.new(@io, size.to_i64) 338 | @pad = Crystar.block_padding(size.to_i64) 339 | end 340 | 341 | private abstract class FileWriter < IO 342 | include FileState 343 | getter io : IO 344 | 345 | def initialize(@io) 346 | end 347 | 348 | abstract def write(slice : Bytes) : Nil 349 | abstract def read_from(r : IO) : Int 350 | 351 | def read(slice : Bytes) 352 | raise Error.new "Crystar Writer: Can't read" 353 | end 354 | 355 | forward_missing_to @io 356 | end 357 | 358 | private class RegFileWriter < FileWriter 359 | @nb = 0_i64 # Number of remaining bytes to write 360 | 361 | def initialize(@io, nb : Int) 362 | @nb = nb.to_i64 363 | super(@io) 364 | end 365 | 366 | def write(slice : Bytes) : Nil 367 | overwrite = slice.size > @nb 368 | slice = slice[..@nb] if overwrite 369 | if slice.size > 0 370 | @io.write(slice) 371 | @nb -= slice.size 372 | end 373 | raise ErrWriteTooLong.new "tar: write too long" if overwrite 374 | end 375 | 376 | def read_from(r : IO) : Int 377 | IO.copy r, self 378 | end 379 | 380 | def logical_remaining : Int64 381 | @nb 382 | end 383 | 384 | def physical_remaining : Int64 385 | @nb 386 | end 387 | end 388 | 389 | private class SparseFileWriter < FileWriter 390 | def initialize(@fw : FileWriter, @sp : SparseDatas, @pos : Int64) 391 | super(@fw) 392 | end 393 | 394 | # ameba:disable Metrics/CyclomaticComplexity 395 | def write(slice : Bytes) : Nil 396 | overwrite = slice.size > logical_remaining 397 | slice = slice[...logical_remaining] if overwrite 398 | end_pos = @pos + slice.size 399 | too_long = false 400 | while end_pos > @pos && !too_long 401 | nf = 0 # Bytes written in fragment 402 | data_start, data_end = @sp[0].offset, @sp[0].end_of_offset 403 | if @pos < data_start # In a hole fragment 404 | bf = slice[...Math.min(slice.size, data_start - @pos)] 405 | tmp = Bytes.new(bf.size) 406 | bf.copy_from(tmp.to_unsafe, bf.size) 407 | nf = bf.size 408 | else # In a data fragment 409 | bf = slice[...Math.min(slice.size, data_end - @pos)] 410 | begin 411 | @fw.write(bf) 412 | nf = bf.size 413 | rescue Error 414 | too_long = true 415 | end 416 | end 417 | slice = slice[nf..] 418 | @pos += nf 419 | if @pos >= data_end && @sp.size > 1 420 | @sp = @sp[1..] # Ensure last fragment always remains 421 | end 422 | end 423 | 424 | # Not possible; implies bug in validation logic 425 | raise Error.new("sparse file references non-existent data") if too_long 426 | if logical_remaining == 0 && physical_remaining > 0 427 | # Not possible; implies bug in validation logic 428 | raise Error.new("sparse file contains unreferenced data") 429 | end 430 | raise IO::EOFError.new if overwrite 431 | end 432 | 433 | # ameba:disable Metrics/CyclomaticComplexity 434 | def read_from(r : IO) : Int 435 | begin 436 | r.seek(0, IO::Seek::Current) 437 | rescue ex 438 | # not all IO can really seek 439 | return IO.copy r, self 440 | end 441 | read_last_byte = false 442 | too_long = false 443 | eof = false 444 | pos0 = @pos 445 | while logical_remaining > 0 && !read_last_byte && !too_long 446 | nf = 0 # Size of fragment 447 | data_start, data_end = @sp[0].offset, @sp[0].end_of_offset 448 | if @pos < data_start # In a hole fragment 449 | nf = data_start - @pos 450 | if physical_remaining == 0 451 | read_last_byte = true 452 | nf -= 1 453 | end 454 | r.seek(nf, IO::Seek::Current) 455 | else # In a data fragment 456 | nf = data_end - @pos 457 | begin 458 | nf = IO.copy r, @fw, nf 459 | rescue Error 460 | too_long = true 461 | rescue IO::EOFError 462 | eof = true 463 | end 464 | end 465 | @pos += nf 466 | if @pos >= data_end && @sp.size > 1 467 | @sp = @sp[1..] # Ensure last fragment always remains 468 | end 469 | end 470 | 471 | # If the last fragment is a hole, then seek to 1-byte before EOF, and 472 | # read a single byte to ensure the file is the right size. 473 | if read_last_byte && !too_long 474 | r.read_full(Bytes.new(1)) 475 | @pos += 1 476 | end 477 | 478 | n = @pos - pos0 479 | raise Error.new "unexpected EOF" if eof 480 | # Not possible; implies bug in validation logic 481 | raise Error.new("sparse file references non-existent data") if too_long 482 | if logical_remaining == 0 && physical_remaining > 0 483 | # Not possible; implies bug in validation logic 484 | raise Error.new("sparse file contains unreferenced data") 485 | end 486 | ensure_eof r 487 | n 488 | end 489 | 490 | def logical_remaining : Int64 491 | @sp[@sp.size - 1].end_of_offset - @pos 492 | end 493 | 494 | def physical_remaining : Int64 495 | @fw.physical_remaining 496 | end 497 | 498 | private def ensure_eof(r : IO) 499 | begin 500 | n = r.read_full(Bytes.new(1)) 501 | rescue IO::EOFError 502 | end 503 | raise Error.new "tar: write too long" if n > 0 504 | end 505 | end 506 | 507 | # Use long-link files if Name or Linkname exceeds the field size. 508 | LONG_NAME = "././@LongLink" 509 | end 510 | 511 | private alias StringFormatter = Proc(Bytes, String, Nil) 512 | private alias NumberFormatter = Proc(Bytes, Int64, Nil) 513 | end 514 | --------------------------------------------------------------------------------