1   /**
2    * Distribution License:
3    * JSword is free software; you can redistribute it and/or modify it under
4    * the terms of the GNU Lesser General Public License, version 2.1 or later
5    * as published by the Free Software Foundation. This program is distributed
6    * in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
7    * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
8    * See the GNU Lesser General Public License for more details.
9    *
10   * The License is available on the internet at:
11   *      http://www.gnu.org/copyleft/lgpl.html
12   * or by writing to:
13   *      Free Software Foundation, Inc.
14   *      59 Temple Place - Suite 330
15   *      Boston, MA 02111-1307, USA
16   *
17   * © CrossWire Bible Society, 2008 - 2016
18   *
19   */
20  package org.crosswire.jsword.book.sword;
21  
22  import org.crosswire.common.crypt.Sapphire;
23  
24  /**
25   * Data entry represents an entry in a Data file. The entry consists of a key
26   * and an optional payload.
27   * <p>The payload may be:</p>
28   * <ul>
29   * <li>the content, that is raw text</li>
30   * <li>an alias (@LINK) for another entry</li>
31   * <li>a block locator</li>
32   * </ul>
33   * 
34   * @see gnu.lgpl.License The GNU Lesser General Public License for details.
35   * @author DM Smith
36   */
37  public class DataEntry {
38      /**
39       * Construct a data entry.
40       * 
41       * @param name
42       *            A name used for diagnostics.
43       * @param data
44       *            The data for this entry.
45       * @param charset
46       *            The character encoding for this entry.
47       */
48      public DataEntry(String name, byte[] data, String charset) {
49          this.name = name;
50          this.data = data.clone();
51          this.charset = charset;
52          // The key always ends with \n, typically \r\n
53          this.keyEnd = SwordUtil.findByte(this.data, SEP_NL);
54      }
55  
56      /**
57       * Get the name, that is, the diagnostic label, for this DataEntry.
58       * 
59       * @return the diagnostic name.
60       */
61      public String getName() {
62          return name;
63      }
64  
65      /**
66       * Get the charset in which the data is encoded.
67       * @return this entry's charset
68       */
69      public String getCharset() {
70          return charset;
71      }
72  
73      /**
74       * Get the key from this DataEntry.
75       * 
76       * @return the key
77       */
78      public String getKey() {
79          if (key == null) {
80              // Some entries are empty
81              if (data.length == 0) {
82                  key = "";
83                  return key;
84              }
85  
86              if (keyEnd < 0) {
87                  key = "";
88                  return key;
89              }
90  
91              int end = keyEnd;
92              // remove trailing \r if present
93              if (end > 0 && data[end - 1] == SEP_CR) {
94                  --end;
95              }
96  
97              // for some weird reason plain text dictionaries
98              // all get \ added to the ends of the index entries.
99              if (end > 0 && data[end - 1] == SEP_BSLASH) {
100                 --end;
101             }
102 
103             // If the end is 0 then we have an empty key.
104             if (end == 0) {
105                 key = "";
106                 return key;
107             }
108 
109             // The key may have whitespace, including \r on the end,
110             // that is not actually part of the key.
111             key = SwordUtil.decode(name, data, end, charset);
112         }
113 
114         return key;
115     }
116 
117     /**
118      * Determine whether this entry is an alias for another.
119      * 
120      * @return whether this is an alias entry
121      */
122     public boolean isLinkEntry() {
123         // 6 represents the length of "@LINK" when keyEnd is -1
124         return keyEnd + 6 < data.length
125                 && data[keyEnd + 1] == '@'
126                 && data[keyEnd + 2] == 'L'
127                 && data[keyEnd + 3] == 'I'
128                 && data[keyEnd + 4] == 'N'
129                 && data[keyEnd + 5] == 'K';
130     }
131 
132     /**
133      * Get the link target for this entry. One entry can be chained to another.
134      * If the entry is not linked then it is an error to call this method.
135      * 
136      * @return the key to look up
137      * @see #isLinkEntry()
138      */
139     public String getLinkTarget() {
140         // 6 represents the length of "@LINK" + 1 to skip the last separator.
141         int linkStart = keyEnd + 6;
142         int len = getLinkEnd() - linkStart + 1;
143         return SwordUtil.decode(name, data, linkStart, len, charset).trim();
144     }
145 
146     /**
147      * Get the raw text from this entry.
148      * 
149      * @param cipherKey
150      *            the key, if any, to (un)lock the text
151      * @return the raw text
152      */
153     public String getRawText(byte[] cipherKey) {
154         int textStart = keyEnd + 1;
155         cipher(cipherKey, textStart);
156         return SwordUtil.decode(name, data, textStart, data.length - textStart, charset).trim();
157     }
158 
159     /**
160      * Get the block start and entry position.
161      * 
162      * @return the index of the block
163      */
164     public DataIndex getBlockIndex() {
165         int start = keyEnd + 1;
166         return new DataIndex(SwordUtil.decodeLittleEndian32(data, start), SwordUtil.decodeLittleEndian32(data, start + 4));
167     }
168 
169     /**
170      * Get the position of the second \n in the data. This represents the end of
171      * the link and the start of the rest of the data.
172      * 
173      * @return the end of the link or -1 if not found.
174      */
175     private int getLinkEnd() {
176         if (linkEnd == 0) {
177             linkEnd = SwordUtil.findByte(data, keyEnd + 1, SEP_NL);
178             if (linkEnd == -1) {
179                 linkEnd = data.length - 1;
180             }
181         }
182         return linkEnd;
183     }
184 
185     /**
186      * Decipher/Encipher the data in place, if there is a cipher key.
187      * 
188      * @param cipherKey
189      *            the key to the cipher
190      * @param offset
191      *            the start of the cipher data
192      */
193     public void cipher(byte[] cipherKey, int offset) {
194         if (cipherKey != null && cipherKey.length > 0) {
195             Sapphire cipherEngine = new Sapphire(cipherKey);
196             for (int i = offset; i < data.length; i++) {
197                 data[i] = cipherEngine.cipher(data[i]);
198             }
199             // destroy any evidence!
200             cipherEngine.burn();
201         }
202     }
203 
204     /**
205      * Used to separate the key name from the key value Note: it may be \r\n or
206      * just \n, so only need \n. ^M=CR=13=0x0d=\r ^J=LF=10=0x0a=\n
207      */
208     private static final byte SEP_NL = (byte) '\n'; // 10;
209     private static final byte SEP_CR = (byte) '\r'; // 13;
210     private static final byte SEP_BSLASH = (byte) '\\'; // 92;
211     /**
212      * A diagnostic name.
213      */
214     private String name;
215 
216     /**
217      * The data entry as it comes out of the data file.
218      */
219     private byte[] data;
220 
221     /**
222      * The character set of the data entry.
223      */
224     private String charset;
225 
226     /**
227      * The key in the data entry.
228      */
229     private String key;
230 
231     /**
232      * The index of the separator between the key and the payload.
233      */
234     private int keyEnd;
235 
236     /**
237      * The index of the separator between the link and the payload.
238      */
239     private int linkEnd;
240 }
241