BinaryDictOffdeviceUtils.java revision e9a10ff0f026b5ec458f116afc7a75806574cbcd
177fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalard/* 277fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalard * Copyright (C) 2012 The Android Open Source Project 377fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalard * 477fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalard * Licensed under the Apache License, Version 2.0 (the "License"); you may not 577fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalard * use this file except in compliance with the License. You may obtain a copy of 677fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalard * the License at 777fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalard * 877fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalard * http://www.apache.org/licenses/LICENSE-2.0 977fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalard * 1077fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalard * Unless required by applicable law or agreed to in writing, software 1177fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalard * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 1277fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalard * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 1377fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalard * License for the specific language governing permissions and limitations under 1477fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalard * the License. 1577fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalard */ 1677fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalard 1777fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalardpackage com.android.inputmethod.latin.dicttool; 1877fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalard 1977bce05e6f6e3a988253f9305ae22e51f56f5b1aYuichiro Hanadaimport com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils; 20e9a10ff0f026b5ec458f116afc7a75806574cbcdYuichiro Hanadaimport com.android.inputmethod.latin.makedict.DictDecoder; 216ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalardimport com.android.inputmethod.latin.makedict.FusionDictionary; 226ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalardimport com.android.inputmethod.latin.makedict.UnsupportedFormatException; 23112257e40f6f6d914fac1c3a45f39a770693b386Yuichiro Hanadaimport com.android.inputmethod.latin.makedict.Ver3DictDecoder; 246ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard 256ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalardimport org.xml.sax.SAXException; 26b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard 27b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalardimport java.io.File; 28b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalardimport java.io.BufferedInputStream; 29b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalardimport java.io.BufferedOutputStream; 30b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalardimport java.io.FileInputStream; 31b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalardimport java.io.FileOutputStream; 3277fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalardimport java.io.IOException; 3377fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalardimport java.io.InputStream; 3477fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalardimport java.io.OutputStream; 35b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalardimport java.util.ArrayList; 3677fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalard 376ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalardimport javax.xml.parsers.ParserConfigurationException; 386ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard 3977fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalard/** 40b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard * Class grouping utilities for offline dictionary making. 41b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard * 42b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard * Those should not be used on-device, essentially because they are quite 43b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard * liberal about I/O and performance. 44b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard */ 45b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalardpublic final class BinaryDictOffdeviceUtils { 46b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard // Prefix and suffix are arbitrary, the values do not really matter 47b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard private final static String PREFIX = "dicttool"; 48b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard private final static String SUFFIX = ".tmp"; 49b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard 50f1d35ac5dc0cca2b357940cab1001cadca37bcb4Jean Chalard public final static String COMPRESSION = "compressed"; 51f1d35ac5dc0cca2b357940cab1001cadca37bcb4Jean Chalard public final static String ENCRYPTION = "encrypted"; 52b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard 53a8058d169dad450eca428ca76c5a0f44e45f41a7Jean Chalard private final static int MAX_DECODE_DEPTH = 8; 54a8058d169dad450eca428ca76c5a0f44e45f41a7Jean Chalard 55b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard public static class DecoderChainSpec { 56b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard ArrayList<String> mDecoderSpec = new ArrayList<String>(); 57b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard File mFile; 58b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard public DecoderChainSpec addStep(final String stepDescription) { 59b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard mDecoderSpec.add(stepDescription); 60b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard return this; 61b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard } 62f1d35ac5dc0cca2b357940cab1001cadca37bcb4Jean Chalard public String describeChain() { 63f1d35ac5dc0cca2b357940cab1001cadca37bcb4Jean Chalard final StringBuilder s = new StringBuilder("raw"); 64f1d35ac5dc0cca2b357940cab1001cadca37bcb4Jean Chalard for (final String step : mDecoderSpec) { 65f1d35ac5dc0cca2b357940cab1001cadca37bcb4Jean Chalard s.append(" > "); 66f1d35ac5dc0cca2b357940cab1001cadca37bcb4Jean Chalard s.append(step); 67f1d35ac5dc0cca2b357940cab1001cadca37bcb4Jean Chalard } 68f1d35ac5dc0cca2b357940cab1001cadca37bcb4Jean Chalard return s.toString(); 69f1d35ac5dc0cca2b357940cab1001cadca37bcb4Jean Chalard } 70b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard } 71b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard 7277fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalard public static void copy(final InputStream input, final OutputStream output) throws IOException { 7377fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalard final byte[] buffer = new byte[1000]; 7477fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalard final BufferedInputStream in = new BufferedInputStream(input); 7577fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalard final BufferedOutputStream out = new BufferedOutputStream(output); 7677fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalard for (int readBytes = in.read(buffer); readBytes >= 0; readBytes = in.read(buffer)) 7777fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalard output.write(buffer, 0, readBytes); 7877fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalard in.close(); 7977fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalard out.close(); 8077fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalard } 81b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard 82b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard /** 83b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard * Returns a decrypted/uncompressed binary dictionary. 84b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard * 85b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard * This will decrypt/uncompress any number of times as necessary until it finds the binary 86b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard * dictionary signature, and copy the decoded file to a temporary place. 87b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard * If this is not a binary dictionary, the method returns null. 88b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard */ 89b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard public static DecoderChainSpec getRawBinaryDictionaryOrNull(final File src) { 90a8058d169dad450eca428ca76c5a0f44e45f41a7Jean Chalard return getRawBinaryDictionaryOrNullInternal(new DecoderChainSpec(), src, 0); 91b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard } 92b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard 93b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard private static DecoderChainSpec getRawBinaryDictionaryOrNullInternal( 94a8058d169dad450eca428ca76c5a0f44e45f41a7Jean Chalard final DecoderChainSpec spec, final File src, final int depth) { 95a8058d169dad450eca428ca76c5a0f44e45f41a7Jean Chalard // Unfortunately the decoding scheme we use can consider any data to be encrypted 96a8058d169dad450eca428ca76c5a0f44e45f41a7Jean Chalard // and will product some output, meaning it's not possible to reliably detect encrypted 97a8058d169dad450eca428ca76c5a0f44e45f41a7Jean Chalard // data. Thus, some non-dictionary files (especially small) ones may successfully decrypt 98a8058d169dad450eca428ca76c5a0f44e45f41a7Jean Chalard // over and over, ending in a stack overflow. Hence we limit the depth at which we try 99a8058d169dad450eca428ca76c5a0f44e45f41a7Jean Chalard // decoding the file. 100a8058d169dad450eca428ca76c5a0f44e45f41a7Jean Chalard if (depth > MAX_DECODE_DEPTH) return null; 10177bce05e6f6e3a988253f9305ae22e51f56f5b1aYuichiro Hanada if (BinaryDictDecoderUtils.isBinaryDictionary(src)) { 102b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard spec.mFile = src; 103b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard return spec; 104b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard } 105b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard // It's not a raw dictionary - try to see if it's compressed. 106b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard final File uncompressedFile = tryGetUncompressedFile(src); 107b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard if (null != uncompressedFile) { 108b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard final DecoderChainSpec newSpec = 109a8058d169dad450eca428ca76c5a0f44e45f41a7Jean Chalard getRawBinaryDictionaryOrNullInternal(spec, uncompressedFile, depth + 1); 110b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard if (null == newSpec) return null; 111b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard return newSpec.addStep(COMPRESSION); 112b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard } 1130044df6cf2f4ef00d78e530220565b8272187446Jean Chalard // It's not a compressed either - try to see if it's crypted. 1140044df6cf2f4ef00d78e530220565b8272187446Jean Chalard final File decryptedFile = tryGetDecryptedFile(src); 1150044df6cf2f4ef00d78e530220565b8272187446Jean Chalard if (null != decryptedFile) { 1160044df6cf2f4ef00d78e530220565b8272187446Jean Chalard final DecoderChainSpec newSpec = 117a8058d169dad450eca428ca76c5a0f44e45f41a7Jean Chalard getRawBinaryDictionaryOrNullInternal(spec, decryptedFile, depth + 1); 1180044df6cf2f4ef00d78e530220565b8272187446Jean Chalard if (null == newSpec) return null; 1190044df6cf2f4ef00d78e530220565b8272187446Jean Chalard return newSpec.addStep(ENCRYPTION); 1200044df6cf2f4ef00d78e530220565b8272187446Jean Chalard } 121b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard return null; 122b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard } 123b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard 124b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard /* Try to uncompress the file passed as an argument. 125b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard * 126b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard * If the file can be uncompressed, the uncompressed version is returned. Otherwise, null 127b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard * is returned. 128b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard */ 129b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard private static File tryGetUncompressedFile(final File src) { 130b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard try { 131b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard final File dst = File.createTempFile(PREFIX, SUFFIX); 132a8058d169dad450eca428ca76c5a0f44e45f41a7Jean Chalard dst.deleteOnExit(); 133b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard final FileOutputStream dstStream = new FileOutputStream(dst); 134b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard copy(Compress.getUncompressedStream(new BufferedInputStream(new FileInputStream(src))), 135b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard new BufferedOutputStream(dstStream)); // #copy() closes the streams 136b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard return dst; 137b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard } catch (IOException e) { 138b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard // Could not uncompress the file: presumably the file is simply not a compressed file 139b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard return null; 140b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard } 141b3c98901c5fc1460b54cdf27d74405f27c88e74bJean Chalard } 1420044df6cf2f4ef00d78e530220565b8272187446Jean Chalard 1430044df6cf2f4ef00d78e530220565b8272187446Jean Chalard /* Try to decrypt the file passed as an argument. 1440044df6cf2f4ef00d78e530220565b8272187446Jean Chalard * 1450044df6cf2f4ef00d78e530220565b8272187446Jean Chalard * If the file can be decrypted, the decrypted version is returned. Otherwise, null 1460044df6cf2f4ef00d78e530220565b8272187446Jean Chalard * is returned. 1470044df6cf2f4ef00d78e530220565b8272187446Jean Chalard */ 1480044df6cf2f4ef00d78e530220565b8272187446Jean Chalard private static File tryGetDecryptedFile(final File src) { 1490044df6cf2f4ef00d78e530220565b8272187446Jean Chalard try { 1500044df6cf2f4ef00d78e530220565b8272187446Jean Chalard final File dst = File.createTempFile(PREFIX, SUFFIX); 151a8058d169dad450eca428ca76c5a0f44e45f41a7Jean Chalard dst.deleteOnExit(); 1520044df6cf2f4ef00d78e530220565b8272187446Jean Chalard final FileOutputStream dstStream = new FileOutputStream(dst); 1530044df6cf2f4ef00d78e530220565b8272187446Jean Chalard copy(Crypt.getDecryptedStream(new BufferedInputStream(new FileInputStream(src))), 1540044df6cf2f4ef00d78e530220565b8272187446Jean Chalard dstStream); // #copy() closes the streams 1550044df6cf2f4ef00d78e530220565b8272187446Jean Chalard return dst; 1560044df6cf2f4ef00d78e530220565b8272187446Jean Chalard } catch (IOException e) { 157a8058d169dad450eca428ca76c5a0f44e45f41a7Jean Chalard // Could not decrypt the file: presumably the file is simply not a crypted file 1580044df6cf2f4ef00d78e530220565b8272187446Jean Chalard return null; 1590044df6cf2f4ef00d78e530220565b8272187446Jean Chalard } 1600044df6cf2f4ef00d78e530220565b8272187446Jean Chalard } 1616ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard 1626ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard static void crash(final String filename, final Exception e) { 1636ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard throw new RuntimeException("Can't read file " + filename, e); 1646ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard } 1656ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard 1666ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard static FusionDictionary getDictionary(final String filename, final boolean report) { 1676ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard final File file = new File(filename); 1686ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard if (report) { 1696ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard System.out.println("Dictionary : " + file.getAbsolutePath()); 1706ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard System.out.println("Size : " + file.length() + " bytes"); 1716ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard } 1726ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard try { 1736ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard if (XmlDictInputOutput.isXmlUnigramDictionary(filename)) { 1746ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard if (report) System.out.println("Format : XML unigram list"); 1756ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard return XmlDictInputOutput.readDictionaryXml( 1766ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard new BufferedInputStream(new FileInputStream(file)), 1776ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard null /* shortcuts */, null /* bigrams */); 1786ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard } else if (CombinedInputOutput.isCombinedDictionary(filename)) { 1796ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard if (report) System.out.println("Format : Combined format"); 1806ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard return CombinedInputOutput.readDictionaryCombined( 1816ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard new BufferedInputStream(new FileInputStream(file))); 1826ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard } else { 1836ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard final DecoderChainSpec decodedSpec = getRawBinaryDictionaryOrNull(file); 1846ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard if (null == decodedSpec) { 1856ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard crash(filename, new RuntimeException( 1866ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard filename + " does not seem to be a dictionary file")); 1876ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard } else { 188e9a10ff0f026b5ec458f116afc7a75806574cbcdYuichiro Hanada final DictDecoder dictDecoder = new Ver3DictDecoder(decodedSpec.mFile, 189e9a10ff0f026b5ec458f116afc7a75806574cbcdYuichiro Hanada DictDecoder.USE_BYTEARRAY); 1906ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard if (report) { 1916ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard System.out.println("Format : Binary dictionary format"); 1926ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard System.out.println("Packaging : " + decodedSpec.describeChain()); 1936ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard System.out.println("Uncompressed size : " + decodedSpec.mFile.length()); 1946ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard } 195e9a10ff0f026b5ec458f116afc7a75806574cbcdYuichiro Hanada return dictDecoder.readDictionaryBinary(null); 1966ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard } 1976ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard } 1986ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard } catch (IOException e) { 1996ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard crash(filename, e); 2006ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard } catch (SAXException e) { 2016ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard crash(filename, e); 2026ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard } catch (ParserConfigurationException e) { 2036ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard crash(filename, e); 2046ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard } catch (UnsupportedFormatException e) { 2056ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard crash(filename, e); 2066ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard } 2076ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard return null; 2086ecc50a867dc09eb1d9dafe62f40e73de01b30cbJean Chalard } 20977fe603a3d82f5fc28816520bac479ff48bf15e5Jean Chalard} 210