Skip to content

Commit

Permalink
global refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
Infactum committed Feb 13, 2016
1 parent 5991919 commit c5871a6
Show file tree
Hide file tree
Showing 16 changed files with 1,009 additions and 458 deletions.
29 changes: 18 additions & 11 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
===========
onec_dtools
===========
===============
OneC Data Tools
===============

.. image:: https://img.shields.io/pypi/v/onec_dtools.svg
:target: https://pypi.python.org/pypi/onec_dtools
Expand All @@ -13,7 +13,7 @@ onec_dtools
.. image:: https://img.shields.io/coveralls/Infactum/onec_dtools.svg
:target: https://coveralls.io/github/Infactum/onec_dtools

**onec_dtools** - библиотека для работы с файлами данных 1С:Предприятие 8 (1CD, cf, epf и т.д.) без использования
**onec_dtools** - библиотека для работы с бинарными файлами 1С:Предприятие 8 (1CD, cf, epf и т.д.) без использования
технологической платформы.

Установка
Expand All @@ -26,17 +26,24 @@ onec_dtools
Использование
=============

Полное описание применения библиотеки доступно в документации_.
Полное описание всех возможностей библиотеки доступно в документации_.
.. _документации: http://onec-dtools.readthedocs.org/ru/latest/

Простой пример, демонстрирующий чтение таблицы V8USERS::
Простой пример, демонстрирующий чтение всех данных (включая BLOB) из таблицы V8USERS::

import onec_dtools

with open('1Cv8.1CD', 'rb') as f:
db = onec_dtools.Database(f)
db = onec_dtools.DatabaseReader(f)
if row.is_empty:
continue
for row in db.tables['V8USERS']:
print(row.as_list(True))

table_name = 'V8USERS'
for row in db.read_table(table_name):
print(row)
Распаковка и запаковки CF файла::

import onec_dtools

onec_dtools.extract('D:/sample.cf', 'D:/unpack')
onec_dtools.build('D:/unpack', 'D:/repacked.cf')

.. _документации: http://onec-dtools.readthedocs.org/ru/latest/
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@
# built documents.
#
# The short X.Y version.
version = '0.1'
version = '0.3'
# The full version, including alpha/beta/rc tags.
release = '0.1.1'
release = '0.3.0'

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand Down
13 changes: 3 additions & 10 deletions docs/index.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Документация onec_dtools
========================

**onec_dtools** - это python модуль для работы с файлами данных 1С:Предприятие 8 без использования
**onec_dtools** - это python модуль для работы с бинарными файлами 1С:Предприятие 8 без использования
технологической платформы.

Основные возможности:
Expand All @@ -16,6 +16,7 @@

installation
usage
modules
versions

Сайты onec_dtools
Expand All @@ -27,20 +28,12 @@
* http://infostart.ru/public/418553/
* http://infostart.ru/public/412475/

* Страница проекта в PyPI:
https://pypi.python.org/pypi/onec_dtools/
* Страница проекта в PyPI: https://pypi.python.org/pypi/onec_dtools/

.. Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
.. Autodoc
=======
.. automodule:: onec_dtools
:special-members: __init__
:members:
:undoc-members:
60 changes: 60 additions & 0 deletions docs/modules.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
Описание модулей
================

database_reader
---------------

.. py:currentmodule:: onec_dtools.database_reader
.. autoclass:: DatabaseReader
:members:
.. autoclass:: Table
:members:
:special-members: __len__, __iter__, __getitem__
.. autoclass:: Row
:members:
:special-members: __getitem__
.. autoclass:: Blob
:members:
:special-members: __len__, __iter__
.. autoclass:: DBObject
:members:
:special-members: __len__
.. autoclass:: FieldDescription
.. autofunction:: database_header
.. autofunction:: root_object
.. autofunction:: raw_tables_descriptions
.. autofunction:: calc_field_size
.. autofunction:: numeric_to_int
.. autofunction:: nvc_to_string
.. autofunction:: bytes_to_datetime

container_reader
----------------
.. py:currentmodule:: onec_dtools.container_reader
.. autoclass:: ContainerReader
:members:
.. autofunction:: extract
.. autofunction:: read_header
.. autofunction:: read_block
.. autofunction:: read_document
.. autofunction:: read_full_document
.. autofunction:: parse_datetime
.. autofunction:: read_entries


container_writer
----------------
.. py:currentmodule:: onec_dtools.container_writer
.. autoclass:: ContainerWriter
:members:
:special-members: __enter__, __exit__
.. autofunction:: build
.. autofunction:: add_entries
.. autofunction:: epoch2int
.. autofunction:: int2hex
.. autofunction:: get_size


108 changes: 93 additions & 15 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,106 @@
Работа с файлами БД
-------------------

Рассмотрим работу с файлами базы данных на простом примере, демонстрирующем чтение общей информации о базе и получение
данных о пользователях из таблицы V8USERS. Обратите внимание, что данные полей неограниченной длины не считываются
автоматически при получении данных строки во избежание лишнего расхода памяти. Считывание таких данных необходимо
производить отдельно, используя имеющийся генератор. ::

Для чтения БД используется классс :code:`DatabaseReader`. При инициализации класса считывается основная информация о
БД (версия формата, язык и т.д) и список таблиц. Каждая таблица представляет собой объект класса :code:`Table`.
Обращение к строкам таблицы может выполняться путем итерирования объекта таблицы, либо путем обращения по индексу.
Каждая строка таблицы БД представляет собой объект класса :code:`Row`. Методы работы со строками аналогичны методам
работы с таблицами БД.

Стоит обратить внимание на то, что преобразование значений полей из внутреннего формата 1С происходит при обращении к
полю. В дальнейшем значение кэшируется внутри объект. Таким образом, чтобы не снижать скорость работы, не рекоммендуется
применять методы :code:`Row.as_dict` и :code:`Row.as_list` если не требуются значения всех полей.

Значения полей неограниченной длины представлены объектами класса :code:`Blob`. Значение поля может быть считано в
память целиком путем обращения к свойству :code:`Blob.value`. Если объект слишком большой, чтобы поместиться в памяти
(размер можно получить через :code:`len(Blob)`), то он может быть считан частями по 256 байт путем итерирования.

Следующий пример демонстрирует чтение данных о пользователях (а так же расшифровку хэшей паролей) из таблицы V8USERS
файловой БД. ::

import binascii
import re
import base64
import argparse
import onec_dtools

with open('1Cv8.1CD', 'rb') as f:
db = onec_dtools.Database(f)
print("База данных 1С (вер. {}/{})".format(db.version, db.locale))
print("Всего таблиц: {}".format(len(db.description)))

table_name = 'V8USERS'
for row in db.read_table(table_name):
data = b''.join([chunk for chunk in row['DATA'].data])
print('{0[NAME]:25} {1}'.format(row, data))
def extract_hashes(text):
"""
Получает SHA1 хэши паролей пользователей из расшифрованных данных поля DATA

:param text: расшифрованное поле DATA
:return: кортеж хэшей: (SHA1(pwd), SHA1(TO_UPPER(pwd))
"""
result = re.search('\d+,\d+,"(\S+)","(\S+)",\d+,\d+', text)
if result is None:
return
return tuple([''.join('{:02x}'.format(byte) for byte in base64.decodebytes(x.encode())) for x in result.groups()])


def decode_data_fld(buffer):
"""
Декодирование поля DATA таблицы V8USERS

:param buffer: зашифрованные данные
:return:
"""
# Первый байт содержит длину маски шифрования
# Далее каждая порция байт соответствующей длины поксорена на маску
mask_length = int(buffer[0])
j = 1
decoded = []
for i in buffer[mask_length + 1:]:
decoded.append('{:02X}'.format(int(buffer[j] ^ int(i))))
j += 1
if j > mask_length:
j = 1
decoded_hex_str = ''.join(decoded)
decoded_bin_str = binascii.unhexlify(decoded_hex_str)
return decoded_bin_str.decode("utf-8-sig")


if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('path_to_1CD', type=str)
args = parser.parse_args()
with open(args.path_to_1CD, 'rb') as f:
db = onec_dtools.DatabaseReader(f)

print("+{}+{}+{}+{}+".format(6*'-', 50*'-', 42*'-', 42*'-'))
print("|{:6}|{:50}|{:42}|{:42}|".format('Админ', 'Имя пользователя', 'SHA1', 'SHA1'))
print("+{}+{}+{}+{}+".format(6*'-', 50*'-', 42*'-', 42*'-'))

for row in db.tables['V8USERS']:
if row.is_empty:
continue
hashes = extract_hashes(decode_data_fld(row['DATA'].value))
if hashes is None:
continue
print("|{0[ADMROLE]!r:6}|{0[NAME]:50}|{1[0]:42}|{1[1]:42}|".format(row, hashes))

print("+{}+{}+{}+{}+".format(6*'-', 50*'-', 42*'-', 42*'-'))

Результатом на примере демо базы конфигурации `Управляемое приложение <http://its.1c.ru/db/metod8dev/content/5028/hdoc>`_
будет следующая таблица: ::

+------+--------------------------------------------------+------------------------------------------+------------------------------------------+
|Админ |Имя пользователя |SHA1 |SHA1 |
+------+--------------------------------------------------+------------------------------------------+------------------------------------------+
|True |Администратор |da39a3ee5e6b4b0d3255bfef95601890afd80709 |da39a3ee5e6b4b0d3255bfef95601890afd80709 |
|False |Менеджер по закупкам |da39a3ee5e6b4b0d3255bfef95601890afd80709 |da39a3ee5e6b4b0d3255bfef95601890afd80709 |
|False |Менеджер по продажам |da39a3ee5e6b4b0d3255bfef95601890afd80709 |da39a3ee5e6b4b0d3255bfef95601890afd80709 |
|False |Продавец |da39a3ee5e6b4b0d3255bfef95601890afd80709 |da39a3ee5e6b4b0d3255bfef95601890afd80709 |
+------+--------------------------------------------------+------------------------------------------+------------------------------------------+

Работа с контейнерами
---------------------

Следующий код реализует возможности распаковки и обратной сборки контейнеров по аналогии с тем, как это делает
v8unpack::
Работать с контейнерами можно как используя классы :code:`ContainerReader` и :code:`ContainerWriter` для распаковки/упаковки
контейнеров соответственно, так и применяя синтаксический сахар в виде функций :code:`parse` и :code:`build`.

Следующий код реализует возможности распаковки и обратной сборки контейнеров по аналогии с тем, как это делает C++
версия `v8unpack <https://github.com/dmpas/v8unpack>`_::

import argparse
import sys
Expand Down
15 changes: 15 additions & 0 deletions docs/versions.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
История версий
===============

0.3.0
-----
* Рефакторинг механизмы чтения формата 1CD
* Новый API работы с файловой базой
* Значительно улучшена документация

0.2.0
-----
* Исправлена ошибка преобразования значений типа Numeric
* Исправлена ошибка чтения значений полей, допускающих NULL
* Ускорено чтение информации о страницах размещения объектов БД
* Ускорен разбор описаний таблиц БД
* Ускорено преобразование полей типа DateTime
* Преобразование значение в полях таблиц теперь происходит в момент обращения к ним, а не в момент чтения строки

0.1.1
-----
* Исправление ошибок
Expand Down
69 changes: 69 additions & 0 deletions examples/users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""
Пример чтения данных о пользователях файловой ИБ (с распаковкой хэшей) при помощь onec_dtools
Copyright (c) 2016 infactum
"""

# -*- coding: utf-8 -*-
import binascii
import re
import base64
import argparse
import onec_dtools


def extract_hashes(text):
"""
Получает SHA1 хэши паролей пользователей из расшифрованных данных поля DATA
:param text: расшифрованное поле DATA
:return: кортеж хэшей: (SHA1(pwd), SHA1(TO_UPPER(pwd))
"""
result = re.search('\d+,\d+,"(\S+)","(\S+)",\d+,\d+', text)
if result is None:
return
return tuple([''.join('{:02x}'.format(byte) for byte in base64.decodebytes(x.encode())) for x in result.groups()])


def decode_data_fld(buffer):
"""
Декодирование поля DATA таблицы V8USERS
:param buffer: зашифрованные данные
:return:
"""
# Первый байт содержит длину маски шифрования
# Далее каждая порция байт соответствующей длины поксорена на маску
mask_length = int(buffer[0])
j = 1
decoded = []
for i in buffer[mask_length + 1:]:
decoded.append('{:02X}'.format(int(buffer[j] ^ int(i))))
j += 1
if j > mask_length:
j = 1
decoded_hex_str = ''.join(decoded)
decoded_bin_str = binascii.unhexlify(decoded_hex_str)
return decoded_bin_str.decode("utf-8-sig")


if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('path_to_1CD', type=str)
args = parser.parse_args()
with open(args.path_to_1CD, 'rb') as f:
db = onec_dtools.DatabaseReader(f)

print("+{}+{}+{}+{}+".format(6*'-', 50*'-', 42*'-', 42*'-'))
print("|{:6}|{:50}|{:42}|{:42}|".format('Админ', 'Имя пользователя', 'SHA1', 'SHA1'))
print("+{}+{}+{}+{}+".format(6*'-', 50*'-', 42*'-', 42*'-'))

for row in db.tables['V8USERS']:
if row.is_empty:
continue
hashes = extract_hashes(decode_data_fld(row['DATA'].value))
if hashes is None:
continue
print("|{0[ADMROLE]!r:6}|{0[NAME]:50}|{1[0]:42}|{1[1]:42}|".format(row, hashes))

print("+{}+{}+{}+{}+".format(6*'-', 50*'-', 42*'-', 42*'-'))
Loading

0 comments on commit c5871a6

Please sign in to comment.