001package jmri.jmrix.loconet.spjfile; 002 003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 004import java.io.File; 005import java.io.FileOutputStream; 006import java.io.IOException; 007import java.io.InputStream; 008import java.io.OutputStream; 009import java.util.Arrays; 010import jmri.jmrix.loconet.sdf.SdfBuffer; 011 012/** 013 * Provide tools for reading, writing and accessing Digitrax SPJ files. 014 * <p> 015 * Four-byte quantities in SPJ files are little-endian. 016 * 017 * @author Bob Jacobsen Copyright (C) 2006, 2009 018 */ 019public class SpjFile { 020 021 public SpjFile(File file) { 022 this.file = file; 023 } 024 025 /** 026 * Number of headers present in the file. 027 * 028 * @return -1 if error 029 */ 030 public int numHeaders() { 031 if (headers != null && h0 != null) { 032 return h0.numHeaders(); 033 } else { 034 return -1; 035 } 036 } 037 038 public String getComment() { 039 return h0.getComment(); 040 } 041 042 public Header getHeader(int index) { 043 return headers[index]; 044 } 045 046 public Header findSdfHeader() { 047 int n = numHeaders(); 048 for (int i = 1; i < n; i++) { 049 if (headers[i].isSDF()) { 050 return headers[i]; 051 } 052 } 053 return null; 054 } 055 056 /** 057 * Find the map entry (character string) that corresponds to a particular 058 * handle number. 059 * @param i handle index. 060 * @return string of map entry. 061 */ 062 public String getMapEntry(int i) { 063 log.debug("getMapEntry({})", i); 064 loadMapCache(); 065 String wanted = "" + i + " "; 066 for (int j = 0; j < mapCache.length; j++) { 067 if (mapCache[j].startsWith(wanted)) { 068 return mapCache[j].substring(wanted.length()); 069 } 070 } 071 return null; 072 } 073 074 String[] mapCache = null; 075 076 void loadMapCache() { 077 if (mapCache != null) { 078 return; 079 } 080 081 // find the map entries 082 log.debug("loading map cache"); 083 int map; 084 for (map = 1; map < numHeaders(); map++) { 085 if (headers[map].isMap()) { 086 break; 087 } 088 } 089 // map holds the map index, hopefully 090 if (map > numHeaders()) { 091 log.error("Did not find map data"); 092 return; 093 } 094 095 // here found it, count lines 096 byte[] buffer = headers[map].getByteArray(); 097 log.debug("map buffer length {}", buffer.length); 098 int count = 0; 099 for (int i = 0; i < buffer.length; i++) { 100 if (buffer[i] == 0x0D) { 101 count++; 102 } 103 } 104 105 mapCache = new String[count]; 106 107 log.debug("found {} map entries", count); 108 109 int start = 0; 110 int end = 0; 111 int index = 0; 112 113 // loop through the string, look for each line 114 log.debug("start loop over map with buffer length = {}", buffer.length); 115 while ((++end) < buffer.length) { 116 if (buffer[end] == 0x0D || buffer[end] == 0x0A) { 117 // sound end; make string 118 String next = new String(buffer, start, end - start); 119 // increment pointers 120 start = ++end; 121 log.debug("new start value is {}", start); 122 log.debug("new end value is {}", end); 123 124 // if another linefeed or newline is present, skip it too 125 if ((buffer[end - 1] == 0x0D || ((end < buffer.length) && buffer[end] == 0x0A)) 126 || (buffer[end - 1] == 0x0A || ((end < buffer.length) && buffer[end] == 0x0D))) { 127 start++; 128 end++; 129 } 130 // store entry 131 log.debug(" store entry {}", index); 132 mapCache[index++] = next; 133 } 134 } 135 } 136 137 /** 138 * Save this file. 139 * <p> 140 * It lays the file out again, changing the record start 141 * addresses into a sequential series. 142 * 143 * @param name file name. 144 * @throws java.io.IOException if anything goes wrong 145 */ 146 public void save(String name) throws java.io.IOException { 147 if (name == null) { 148 throw new java.io.IOException("Null name during write"); // NOI18N 149 } 150 try (OutputStream s = new java.io.BufferedOutputStream( 151 new java.io.FileOutputStream(new java.io.File(name)))) { 152 153 // find size of output file 154 int length = Header.HEADERSIZE * h0.numHeaders(); // allow header space at start 155 for (int i = 1; i < h0.numHeaders(); i++) { 156 length += headers[i].getRecordLength(); 157 } 158 byte[] buffer = new byte[length]; 159 for (int i = 0; i < length; i++) { 160 buffer[i] = 0; 161 } 162 163 // start with first header 164 int index = 0; 165 index = h0.store(buffer, index); 166 167 if (index != Header.HEADERSIZE) { 168 log.error("Unexpected 1st header length: {}", index); 169 } 170 171 int datastart = index * h0.numHeaders(); //index is the length of the 1st header 172 173 // rest of the headers 174 for (int i = 1; i < h0.numHeaders(); i++) { // header 0 already done 175 // Update header pointers. 176 headers[i].updateStart(datastart); 177 datastart += headers[i].getRecordLength(); 178 179 // copy contents into output buffer 180 index = headers[i].store(buffer, index); 181 } 182 183 // copy the chunks; skip the first header, with no data 184 for (int i = 1; i < h0.numHeaders(); i++) { 185 int start = headers[i].getRecordStart(); 186 int count = headers[i].getRecordLength(); // stored one long 187 188 byte[] content = headers[i].getByteArray(); 189 if (count != content.length) { 190 log.error("header count {} != content length {}", count, content.length); 191 } 192 for (int j = 0; j < count; j++) { 193 buffer[start + j] = content[j]; 194 } 195 } 196 197 // write out the buffer 198 s.write(buffer); 199 200 // purge buffers 201 s.close(); 202 } 203 } 204 205 /** 206 * Read the file whose name was provided earlier. 207 * @throws java.io.IOException on file error. 208 */ 209 public void read() throws java.io.IOException { 210 if (file == null) { 211 throw new java.io.IOException("Null file during read"); // NOI18N 212 } 213 int n; 214 try (InputStream s = new java.io.BufferedInputStream(new java.io.FileInputStream(file))) { 215 216 // get first header record 217 h0 = new FirstHeader(); 218 h0.load(s); 219 log.debug("FirstHeader: {}", h0); 220 n = h0.numHeaders(); 221 headers = new Header[n]; 222 headers[0] = h0; 223 224 for (int i = 1; i < n; i++) { // header 0 already read 225 headers[i] = new Header(); 226 headers[i].load(s); 227 log.debug("Header {} {}", i, headers[i].toString()); 228 } 229 230 // now read the rest of the file, loading bytes 231 // first, scan for things we can't handle 232 for (int i = 1; i < n; i++) { 233 if (log.isDebugEnabled()) { 234 log.debug("Header {} length {} type {}", i, headers[i].getDataLength(), headers[i].getType()); // NOI18N 235 } 236 if (headers[i].getDataLength() > headers[i].getRecordLength()) { 237 log.error("header {} has data length {} greater than record length {}", 238 i, headers[i].getDataLength(), headers[i].getRecordLength()); // NOI18N 239 } 240 241 for (int j = 1; j < i; j++) { 242 if (headers[i].getHandle() == headers[j].getHandle() 243 && headers[i].getType() == 1 244 && headers[j].getType() == 1) { 245 log.error("Duplicate handle number in records {}({}) and {}({})", i, headers[i].getHandle(), j, headers[j].getHandle()); 246 } 247 } 248 if (headers[i].getType() > 6) { 249 log.error("Type field unexpected value: {}", headers[i].getType()); 250 } 251 if (headers[i].getType() == 0) { 252 log.error("Type field unexpected value: {}", headers[i].getType()); 253 } 254 if (headers[i].getType() < -1) { 255 log.error("Type field unexpected value: {}", headers[i].getType()); 256 } 257 } 258 259 // find end of last part 260 int length = 0; 261 for (int i = 1; i < n; i++) { 262 if (length < headers[i].getRecordStart() + headers[i].getRecordLength()) { 263 length = headers[i].getRecordStart() + headers[i].getRecordLength(); 264 } 265 } 266 267 log.debug("Last byte at {}", length); 268 s.close(); 269 } 270 271 // inefficient way to read, hecause of all the skips (instead 272 // of seeks) But it handles non-consecutive and overlapping definitions. 273 for (int i = 1; i < n; i++) { 274 try (InputStream s = new java.io.BufferedInputStream(new java.io.FileInputStream(file))) { 275 long count = s.skip(headers[i].getRecordStart()); 276 if (count != headers[i].getRecordStart()) { 277 log.warn("Only skipped {} characters, should have skipped {}", count, headers[i].getRecordStart()); 278 } 279 byte[] array = new byte[headers[i].getRecordLength()]; 280 int read = s.read(array); 281 if (read != headers[i].getRecordLength()) { 282 log.error("header {} read {}, expected {}", i, read, headers[i].getRecordLength()); 283 } 284 285 headers[i].setByteArray(array); 286 s.close(); 287 } 288 } 289 } 290 291 /** 292 * Write data from headers into separate files. 293 * <p> 294 * Normally, we just work with the data within this file. 295 * This method allows us to extract the contents of the file for external use. 296 * @throws java.io.IOException on file error. 297 */ 298 public void writeSubFiles() throws IOException { 299 // write data from WAV headers into separate files 300 int n = numHeaders(); 301 for (int i = 1; i < n; i++) { 302 if (headers[i].isWAV()) { 303 writeSubFile(i, "" + i + ".wav"); // NOI18N 304 } else if (headers[i].isSDF()) { 305 writeSubFile(i, "" + i + ".sdf"); // NOI18N 306 } else if (headers[i].getType() == 3) { 307 writeSubFile(i, "" + i + ".cv"); // NOI18N 308 } else if (headers[i].getType() == 4) { 309 writeSubFile(i, "" + i + ".txt"); // NOI18N 310 } else if (headers[i].isMap()) { 311 writeSubFile(i, "" + i + ".map"); // NOI18N 312 } else if (headers[i].getType() == 6) { 313 writeSubFile(i, "" + i + ".uwav"); // NOI18N 314 } 315 } 316 } 317 318 /** 319 * Write the content from a specific header as a new "subfile". 320 * 321 * @param i index of the specific header 322 * @param name filename 323 * @throws IOException based on underlying activity 324 */ 325 void writeSubFile(int i, String name) throws IOException { 326 File outfile = new File(name); 327 OutputStream ostream = new FileOutputStream(outfile); 328 try { 329 ostream.write(headers[i].getByteArray()); 330 } finally { 331 ostream.close(); 332 } 333 } 334 335 public void dispose() { 336 } 337 338 File file; 339 FirstHeader h0; 340 Header[] headers; 341 342 /** 343 * Class representing a header record. 344 */ 345 public class Header { 346 347 final static int HEADERSIZE = 128; // bytes 348 349 int type; 350 int handle; 351 352 // Offset in overall buffer where the complete record 353 // associated with this header is found 354 int recordStart; 355 356 // Offset in overall buffer where the data part of the 357 // record associated with this header is found 358 int dataStart; 359 360 // Length of the data in the associated record 361 int dataLength; 362 // Length of the associated record 363 int recordLength; 364 365 int time; 366 367 @SuppressFBWarnings(value = "URF_UNREAD_FIELD") // we maintain this, but don't use it for anything yet 368 int spare1; 369 370 @SuppressFBWarnings(value = "URF_UNREAD_FIELD") // we maintain this, but don't use it for anything yet 371 int spare2; 372 373 @SuppressFBWarnings(value = "URF_UNREAD_FIELD") // we maintain this, but don't use it for anything yet 374 int spare3; 375 376 @SuppressFBWarnings(value = "URF_UNREAD_FIELD") // we maintain this, but don't use it for anything yet 377 int spare4; 378 379 @SuppressFBWarnings(value = "URF_UNREAD_FIELD") // we maintain this, but don't use it for anything yet 380 int spare5; 381 382 @SuppressFBWarnings(value = "URF_UNREAD_FIELD") // we maintain this, but don't use it for anything yet 383 int spare6; 384 385 @SuppressFBWarnings(value = "URF_UNREAD_FIELD") // we maintain this, but don't use it for anything yet 386 int spare7; 387 388 String filename = ""; 389 390 public int getType() { 391 return type; 392 } 393 394 public int getHandle() { 395 return handle; 396 } 397 398 public int getDataStart() { 399 return dataStart; 400 } 401 402 public void setDataStart(int i) { 403 dataStart = i; 404 } 405 406 public int getDataLength() { 407 return dataLength; 408 } 409 410 private void setDataLength(int i) { 411 dataLength = i; 412 } 413 414 public int getRecordStart() { 415 return recordStart; 416 } 417 418 public void setRecordStart(int i) { 419 recordStart = i; 420 } 421 422 /** 423 * Get Record Length. 424 * <p> 425 * This method, in addition to returning the needed record size, will 426 * also pull a SdfBuffer back into the record if one exists. 427 * @return record length. 428 */ 429 public int getRecordLength() { 430 if (sdfBuffer != null) { 431 sdfBuffer.loadByteArray(); 432 byte[] a = sdfBuffer.getByteArray(); 433 setByteArray(a); 434 dataLength = bytes.length; 435 recordLength = bytes.length; 436 } 437 return recordLength; 438 } 439 440 public void setRecordLength(int i) { 441 recordLength = i; 442 } 443 444 public String getName() { 445 return filename; 446 } 447 448 public void setName(String name) { 449 if (name.length() > 72) { 450 log.error("new filename \"{}\" too long: {}", name, name.length()); 451 } 452 filename = name; 453 } 454 455 byte[] bytes; 456 457 /** 458 * Copy new data into the local byte array. 459 */ 460 private void setByteArray(byte[] a) { 461 bytes = new byte[a.length]; 462 for (int i = 0; i < a.length; i++) { 463 bytes[i] = a[i]; 464 } 465 } 466 467 public byte[] getByteArray() { 468 return Arrays.copyOf(bytes, bytes.length); 469 } 470 471 /** 472 * Get as a SDF buffer. 473 * This buffer then becomes associated, and a later write will use 474 * the buffer's contents. 475 * @return the byte array as SDF buffer. 476 */ 477 public SdfBuffer getSdfBuffer() { 478 sdfBuffer = new SdfBuffer(getByteArray()); 479 return sdfBuffer; 480 } 481 482 SdfBuffer sdfBuffer = null; 483 484 /** 485 * Data record associated with this header is being being repositioned. 486 * @param newRecordStart identify the new start record 487 */ 488 void updateStart(int newRecordStart) { 489 //int oldRecordStart = getRecordStart(); 490 int dataStartOffset = getDataStart() - getRecordStart(); 491 setRecordStart(newRecordStart); 492 setDataStart(newRecordStart + dataStartOffset); 493 } 494 495 /** 496 * Provide new content. The data start and data length values are 497 * computed from the arguments, and stored relative to the length. 498 * 499 * @param array New byte array; copied into header 500 * @param start data start location within array 501 * @param length data length in bytes (not record length) 502 */ 503 public void setContent(byte[] array, int start, int length) { 504 log.debug("setContent length = 0x{}", Integer.toHexString(length)); 505 setByteArray(array); 506 setDataStart(getRecordStart() + start); 507 setDataLength(length); 508 setRecordLength(array.length); 509 } 510 511 int store(byte[] buffer, int index) { 512 index = copyInt4(buffer, index, type); 513 index = copyInt4(buffer, index, handle); 514 index = copyInt4(buffer, index, recordStart); 515 index = copyInt4(buffer, index, dataStart); 516 index = copyInt4(buffer, index, dataLength); 517 index = copyInt4(buffer, index, recordLength); 518 index = copyInt4(buffer, index, time); 519 520 index = copyInt4(buffer, index, 0); // spare 1 521 index = copyInt4(buffer, index, 0); // spare 2 522 index = copyInt4(buffer, index, 0); // spare 3 523 index = copyInt4(buffer, index, 0); // spare 4 524 index = copyInt4(buffer, index, 0); // spare 5 525 index = copyInt4(buffer, index, 0); // spare 6 526 index = copyInt4(buffer, index, 0); // spare 7 527 528 // name is written in zero-filled array 529 byte[] name = filename.getBytes(); 530 if (name.length > 72) { 531 log.error("Name too long: {}", name.length); 532 } 533 for (int i = 0; i < name.length; i++) { 534 buffer[index + i] = name[i]; 535 } 536 537 return index + 72; 538 } 539 540 void store(OutputStream s) throws java.io.IOException { 541 writeInt4(s, type); 542 writeInt4(s, handle); 543 writeInt4(s, recordStart); 544 writeInt4(s, dataStart); 545 writeInt4(s, dataLength); 546 writeInt4(s, recordLength); 547 writeInt4(s, time); 548 549 writeInt4(s, 0); // spare 1 550 writeInt4(s, 0); // spare 2 551 writeInt4(s, 0); // spare 3 552 writeInt4(s, 0); // spare 4 553 writeInt4(s, 0); // spare 5 554 writeInt4(s, 0); // spare 6 555 writeInt4(s, 0); // spare 7 556 557 // name is written in zero-filled array 558 byte[] name = filename.getBytes(); 559 if (name.length > 72) { 560 log.error("Name too long: {}", name.length); 561 } 562 byte[] buffer = new byte[72]; 563 for (int i = 0; i < 72; i++) { 564 buffer[i] = 0; 565 } 566 for (int i = 0; i < name.length; i++) { 567 buffer[i] = name[i]; 568 } 569 s.write(buffer); 570 } 571 572 void load(InputStream s) throws java.io.IOException { 573 type = readInt4(s); 574 handle = readInt4(s); 575 recordStart = readInt4(s); 576 dataStart = readInt4(s); 577 dataLength = readInt4(s); 578 recordLength = readInt4(s); 579 time = readInt4(s); 580 581 spare1 = readInt4(s); 582 spare2 = readInt4(s); 583 spare3 = readInt4(s); 584 spare4 = readInt4(s); 585 spare5 = readInt4(s); 586 spare6 = readInt4(s); 587 spare7 = readInt4(s); 588 589 byte[] name = new byte[72]; 590 int readLength = s.read(name); 591 // name is zero-terminated, so we have to truncate that array 592 int len = 0; 593 for (len = 0; len < readLength; len++) { 594 if (name[len] == 0) { 595 break; 596 } 597 } 598 byte[] shortname = new byte[len]; 599 for (int i = 0; i < len; i++) { 600 shortname[i] = name[i]; 601 } 602 filename = new String(shortname); 603 } 604 605 @Override 606 public String toString() { 607 return "type= " + typeAsString() + ", handle= " + handle + ", rs= " + recordStart + ", ds= " + dataStart // NOI18N 608 + ", ds-rs = " + (dataStart - recordStart) // NOI18N 609 + ", dl = " + dataLength + ", rl= " + recordLength // NOI18N 610 + ", rl-dl = " + (recordLength - dataLength) // NOI18N 611 + ", filename= " + filename; // NOI18N 612 } 613 614 public boolean isWAV() { 615 return (getType() == 1); 616 } 617 618 public boolean isSDF() { 619 return (getType() == 2); 620 } 621 622 public boolean isMap() { 623 return (getType() == 5); 624 } 625 626 public boolean isTxt() { 627 return (getType() == 4); 628 } 629 630 /** 631 * Read a 4-byte integer, handling endian-ness of SPJ files. 632 */ 633 private int readInt4(InputStream s) throws java.io.IOException { 634 int i1 = s.read() & 0xFF; 635 int i2 = s.read() & 0xFF; 636 int i3 = s.read() & 0xFF; 637 int i4 = s.read() & 0xFF; 638 return i1 + (i2 << 8) + (i3 << 16) + (i4 << 24); 639 } 640 641 /** 642 * Write a 4-byte integer, handling endian-ness of SPJ files. 643 */ 644 private void writeInt4(OutputStream s, int i) throws java.io.IOException { 645 byte i1 = (byte) (i & 0xFF); 646 byte i2 = (byte) ((i >> 8) & 0xFF); 647 byte i3 = (byte) ((i >> 16) & 0xFF); 648 byte i4 = (byte) ((i >> 24) & 0xFF); 649 650 s.write(i1); 651 s.write(i2); 652 s.write(i3); 653 s.write(i4); 654 } 655 656 /** 657 * Copy a 4-byte integer to byte buffer, handling little-endian-ness of 658 * SPJ files. 659 */ 660 private int copyInt4(byte[] buffer, int index, int i) { 661 buffer[index++] = (byte) (i & 0xFF); 662 buffer[index++] = (byte) ((i >> 8) & 0xFF); 663 buffer[index++] = (byte) ((i >> 16) & 0xFF); 664 buffer[index++] = (byte) ((i >> 24) & 0xFF); 665 return index; 666 } 667 668 public String typeAsString() { 669 if (type == -1) { 670 return " initial "; // NOI18N 671 } 672 if ((type >= 0) && (type < 7)) { 673 String[] names = {"(unused) ", // 0 // NOI18N 674 "WAV ", // 1 // NOI18N 675 "SDF ", // 2 // NOI18N 676 " CV data ", // 3 // NOI18N 677 " comment ", // 4 // NOI18N 678 ".map file", // 5 // NOI18N 679 "WAV (mty)"}; // 6 // NOI18N 680 return names[type]; 681 } 682 // unexpected answer 683 log.warn("Unexpected type = {}", type); // NOI18N 684 return "Unknown " + type; // NOI18N 685 } 686 } 687 688 /** 689 * Class representing first header 690 */ 691 class FirstHeader extends Header { 692 693 /** 694 * @return number of headers, including the initial system header. 695 */ 696 int numHeaders() { 697 return (dataStart / 128); 698 } 699 700 float version() { 701 return recordStart / 100.f; 702 } 703 704 String getComment() { 705 return filename; 706 } 707 708 @Override 709 public String toString() { 710 return "initial record, version=" + version() + " num headers = " + numHeaders() // NOI18N 711 + ", comment= " + filename; // NOI18N 712 } 713 } 714 715 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SpjFile.class); 716 717}