kirjava-jvm


Namekirjava-jvm JSON
Version 0.1.5 PyPI version JSON
download
home_pagehttps://github.com/notiska/kirjava
SummaryA Java bytecode library for Python.
upload_time2024-03-06 12:22:41
maintainer
docs_urlNone
authornode3112 (Iska)
requires_python
licenseGPL-3.0
keywords java jvm bytecode assembler disassembler
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # kirjava

![](kirjava.png)
Artwork by [Lou](https://www.instagram.com/devils_destination/).

A pure-Python Java bytecode manipulation library with decent obfuscation resilience.  

Documentation is planned for in the future, but as of right now, a quickstart guide has been provided below.  
For more usage, see [examples](examples/).

Just as a note, this is *very much* a hobby project so my maintenance schedule will fluctuate a lot. If you have any bug fixes, PRs are welcome.

**Active development is mostly done on the `dev` branch, if you're curious about new features.**

## Quickstart

### Installing

`python>=3.10` is required, any other versions are untested.  

You can install this library by either:
 1. Installing via pip: `pip3 install git+https://github.com/node3112/kirjava.git`.  
   **Sidenote:** This library will not be published on [PyPI](https://pypi.org/project/kirjava/) as the name `kirjava` is already taken.
 2. Cloning this repository and installing it manually:  
    - `git clone https://github.com/node3112/kirjava.git kirjava`
    - `cd kirjava`
    - `python3 setup.py install` or, if you lack permissions: `python3 setup.py install --user`

*Additionally, [PyPy](https://www.pypy.org/) does appear to work and can result in significant performance gains.*

### Getting started

Simply import kirjava, no extra steps are required once installed:

```python3
In [1]: import kirjava
```

### Reading classfiles

kirjava contains quite a few shortcuts for various tedious tasks, an example:

```python3
In [2]: cf = kirjava.load("Test.class")

In [3]: cf
Out[3]: <ClassFile(name='Test') at 7fc10a2245c0>
```

This is *roughly equivalent* to:

```python3
In [2]: with open("Test.class", "rb") as stream:
   ...:     cf = kirjava.ClassFile.read(stream)
   ...: 

In [3]: cf
Out[3]: <ClassFile(name='Test') at 7fc10a2245c0>
```

Whatever you choose to use is up to you.  
The latter is likely more performant than the former, but if you just wish to inspect a classfile in an interactive shell, the shortcut is always available for use.

### Inspecting the class

Viewing all the methods in the class can be done via:

```python3
In [4]: cf.methods
Out[4]: 
(<MethodInfo(name='main', argument_types=(java/lang/String[],), return_type=void) at 7fc10a069a80>,
 <MethodInfo(name='<init>', argument_types=(), return_type=void) at 7fc10a0698a0>,
 <MethodInfo(name='test', argument_types=(boolean,), return_type=void) at 7fc10a069ae0>,
 <MethodInfo(name='test2', argument_types=(), return_type=void) at 7fc10a069ba0>)
```

And similarly, the fields:

```python3
In [5]: cf.fields
Out[5]: (<FieldInfo(name='field', type=int) at 7fc10a069b40>,)
```

The same goes for attributes, although this example file does not contain any:

```python3
In [6]: cf.attributes
Out[6]: {}
```

### Editing bytecode

Creating valid bytecode can be quite an annoyance, so kirjava provides functionality that allows you to edit methods with ease.  
The main classes you'll be using for this are `InsnGraph`, `InsnBlock` and `InsnEdge`.

#### Disassembly

To disassemble a method, you can use the shortcut:

```python3
In [7]: graph = kirjava.disassemble(cf.get_method("test"))

In [8]: graph
Out[8]: <InsnGraph(blocks=10, edges=12) at 7fc10abfed50>
```

Or more verbosely:

```python3
In [7]: graph = kirjava.analysis.InsnGraph.disassemble(cf.get_method("test"))

In [8]: graph
Out[8]: <InsnGraph(blocks=10, edges=12) at 7fc10abfed50>
```

You can then view the blocks and edges present in the graph:

```python3
In [9]: graph.blocks
Out[9]: 
(<InsnBlock(label=0, instructions=[iload_1]) at 7fc10a1ed340>,
 <InsnReturnBlock() at 7fc10b60e5d0>,
 <InsnRethrowBlock() at 7fc10ab8f5c0>,
 <InsnBlock(label=1, instructions=[aload_0, iconst_0, putfield Test.field:I]) at 7fc10abc9f80>,
 <InsnBlock(label=2, instructions=[aload_0, getfield Test.field:I]) at 7fc10abcac40>,
 <InsnBlock(label=3, instructions=[iconst_0]) at 7fc10a2138c0>,
 <InsnBlock(label=4, instructions=[]) at 7fc10a211f00>,
 <InsnBlock(label=5, instructions=[iload_1]) at 7fc10a210340>,
 <InsnBlock(label=6, instructions=[]) at 7fc10a2103c0>,
 <InsnBlock(label=7, instructions=[iinc 1 by 1]) at 7fc10a213240>)

In [10]: graph.edges
Out[10]: 
(<FallthroughEdge(from=block 0, to=block 1)>,
 <JumpEdge(from=block 0, to=block 2, instruction=ifne)>,
 <FallthroughEdge(from=block 1, to=block 2)>,
 <FallthroughEdge(from=block 2, to=block 3)>,
 <JumpEdge(from=block 2, to=block 4, instruction=ifgt)>,
 <JumpEdge(from=block 3, to=block 5, instruction=ifeq)>,
 <FallthroughEdge(from=block 3, to=block 4)>,
 <JumpEdge(from=block 4, to=return block, instruction=return)>,
 <FallthroughEdge(from=block 5, to=block 6)>,
 <JumpEdge(from=block 5, to=block 7, instruction=ifeq)>,
 <JumpEdge(from=block 6, to=return block, instruction=return)>,
 <JumpEdge(from=block 7, to=return block, instruction=return)>)
```

#### Editing blocks

Say for example you wanted to change the value of `Test.field` from `0` to `17`, you could do this:

```python3
In [11]: graph[1].remove(kirjava.instructions.iconst_0)
    ...: graph[1].insert(1, kirjava.instructions.bipush(17))
Out[11]: <ConstantInstruction(opcode=0x10, mnemonic=bipush, constant=<Integer(17)>) at 7fc10a213480>
```

And just to check that we have edited the block correctly:

```python3
In [12]: graph[1]
Out[12]: <InsnBlock(label=1, instructions=[aload_0, bipush 17, putfield Test.field:I]) at 7fc10abc9f80>
```

#### Editing edges

Now let's edit an edge. Firstly let's find one that we can edit easily for the sake of tutorial:

```python3
In [13]: graph.out_edges(graph[2])
Out[13]: 
(<FallthroughEdge(from=block 2, to=block 3)>,
 <JumpEdge(from=block 2, to=block 4, instruction=ifgt)>)
```

Let's change the `ifgt` instruction into an `iflt` for this example:

```python3
In [14]: graph.jump(graph[2], graph[4], kirjava.instructions.iflt)
Out[14]: <JumpEdge(from=block 2, to=block 4, instruction=iflt)>
```

And, to check:

```python3
In [15]: graph.out_edges(graph[2])
Out[15]: 
(<FallthroughEdge(from=block 2, to=block 3)>,
 <JumpEdge(from=block 2, to=block 4, instruction=iflt)>)
```

As you can see we've managed to successfully edit the jump condition.  

There's a lot more that can be done than just these simple tutorials though **(have a play around!)**.

### Analysing bytecode

Often editing a method goes hand-in-hand with analysing it, and kirjava provides tools that allow you to statically analyse the data on the stack and in the locals via the use of the class `Trace`.

To create a trace for a method, you'll need to use the graph for said method. In this example, we'll use the graph from the previous examples:

```python3
In [16]: trace = kirjava.trace(graph)

In [17]: trace
Out[17]: <Trace(entries=9, exits=9, conflicts=0, subroutines=0, max_stack=2, max_locals=2) at 7fc10abff4c0>
```

And again, the more verbose method:

```python3
In [16]: trace = kirjava.analysis.Trace.from_graph(graph)

In [17]: trace
Out[17]: <Trace(entries=9, exits=9, conflicts=0, subroutines=0, max_stack=2, max_locals=2) at 7fc10abff4c0>
```

The `Trace` class provides pre/post liveness information (on a per-block basis) as well as information on subroutines, type conflicts and frames at block entries/exits.

For example, we could look at the local pre-liveness for block 3:

```python3
In [18]: trace.pre_liveness[graph[3]]
Out[18]: {1}
```

We could also view the state of the stack at the entry to it:

```python3
In [19]: trace.entries[graph[3]]
Out[19]: [<Frame(stack=[], locals={0=Test, 1=boolean}) at 7fc109ee7f10>]
```

And we can even inspect individual locals further:  

```python3
In [20]: trace.entries[graph[3]][0].locals
Out[20]: 
{0: <Entry(type=Test, constraints={Test, reference, java/lang/Object}) at 7fc109ee69d0>,
 1: <Entry(type=boolean, constraints={int, boolean}) at 7fc109ee7ce0>}

In [21]: trace.entries[graph[3]][0].locals[0].constraints
Out[21]: 
(<Entry.Constraint(type=reference, source=aload_0 @ block 1[0], original=False)>,
 <Entry.Constraint(type=Test, source=getfield Test.field:I @ block 2[1], original=False)>,
 <Entry.Constraint(type=Test, source=putfield Test.field:I @ block 1[2], original=False)>,
 <Entry.Constraint(type=java/lang/Object, source=None, original=True)>,
 <Entry.Constraint(type=Test, source=param 0 of Test#void test(boolean), original=True)>,
 <Entry.Constraint(type=reference, source=aload_0 @ block 2[0], original=False)>)

In [22]: trace.entries[graph[3]][0].locals[1].producers
Out[22]: 
(<InstructionInBlock(index=0, block=block 7, instruction=iinc 1 by 1)>,
 <Frame.Parameter(index=1, type=boolean, method=Test#void test(boolean))>)

In [23]: trace.entries[graph[3]][0].locals[1].consumers
Out[23]: 
(<InstructionInBlock(index=0, block=block 0, instruction=iload_1)>,
 <JumpEdge(from=block 0, to=block 2, instruction=ifne)>,
 <InstructionInBlock(index=0, block=block 5, instruction=iload_1)>,
 <JumpEdge(from=block 5, to=block 7, instruction=ifeq)>,
 <InstructionInBlock(index=0, block=block 7, instruction=iinc 1 by 1)>)
```

#### Assembly

Reassembling the method after editing is as easy as:

```python3
In [24]: kirjava.assemble(graph)
```

Or:

```python3
In [24]: graph.method.code = graph.assemble()
```

### Writing classfiles

Writing classfiles back out is also easy:

```python3
In [25]: kirjava.dump(cf, "Test-edited.class")
```

Or for the more verbose method:

```python3
In [25]: with open("Test-edited.class", "wb") as stream:
    ...:     cf.write(stream)
    ...: 
```

## "Trivia"

It's honestly not super interesting, but if anyone was wondering, it IS named after a certain character from a certain book series.  
The name is not a Java-related pun, but it does help that "java" is in the name.

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/notiska/kirjava",
    "name": "kirjava-jvm",
    "maintainer": "",
    "docs_url": null,
    "requires_python": "",
    "maintainer_email": "",
    "keywords": "java,jvm,bytecode,assembler,disassembler",
    "author": "node3112 (Iska)",
    "author_email": "node3112@protonmail.com",
    "download_url": "https://files.pythonhosted.org/packages/91/e0/bf6264ceb20edf4e2261cbb8d1b048e9f636fb22cba9df2232abb04240e5/kirjava-jvm-0.1.5.tar.gz",
    "platform": null,
    "description": "# kirjava\n\n![](kirjava.png)\nArtwork by [Lou](https://www.instagram.com/devils_destination/).\n\nA pure-Python Java bytecode manipulation library with decent obfuscation resilience.  \n\nDocumentation is planned for in the future, but as of right now, a quickstart guide has been provided below.  \nFor more usage, see [examples](examples/).\n\nJust as a note, this is *very much* a hobby project so my maintenance schedule will fluctuate a lot. If you have any bug fixes, PRs are welcome.\n\n**Active development is mostly done on the `dev` branch, if you're curious about new features.**\n\n## Quickstart\n\n### Installing\n\n`python>=3.10` is required, any other versions are untested.  \n\nYou can install this library by either:\n 1. Installing via pip: `pip3 install git+https://github.com/node3112/kirjava.git`.  \n   **Sidenote:** This library will not be published on [PyPI](https://pypi.org/project/kirjava/) as the name `kirjava` is already taken.\n 2. Cloning this repository and installing it manually:  \n    - `git clone https://github.com/node3112/kirjava.git kirjava`\n    - `cd kirjava`\n    - `python3 setup.py install` or, if you lack permissions: `python3 setup.py install --user`\n\n*Additionally, [PyPy](https://www.pypy.org/) does appear to work and can result in significant performance gains.*\n\n### Getting started\n\nSimply import kirjava, no extra steps are required once installed:\n\n```python3\nIn [1]: import kirjava\n```\n\n### Reading classfiles\n\nkirjava contains quite a few shortcuts for various tedious tasks, an example:\n\n```python3\nIn [2]: cf = kirjava.load(\"Test.class\")\n\nIn [3]: cf\nOut[3]: <ClassFile(name='Test') at 7fc10a2245c0>\n```\n\nThis is *roughly equivalent* to:\n\n```python3\nIn [2]: with open(\"Test.class\", \"rb\") as stream:\n   ...:     cf = kirjava.ClassFile.read(stream)\n   ...: \n\nIn [3]: cf\nOut[3]: <ClassFile(name='Test') at 7fc10a2245c0>\n```\n\nWhatever you choose to use is up to you.  \nThe latter is likely more performant than the former, but if you just wish to inspect a classfile in an interactive shell, the shortcut is always available for use.\n\n### Inspecting the class\n\nViewing all the methods in the class can be done via:\n\n```python3\nIn [4]: cf.methods\nOut[4]: \n(<MethodInfo(name='main', argument_types=(java/lang/String[],), return_type=void) at 7fc10a069a80>,\n <MethodInfo(name='<init>', argument_types=(), return_type=void) at 7fc10a0698a0>,\n <MethodInfo(name='test', argument_types=(boolean,), return_type=void) at 7fc10a069ae0>,\n <MethodInfo(name='test2', argument_types=(), return_type=void) at 7fc10a069ba0>)\n```\n\nAnd similarly, the fields:\n\n```python3\nIn [5]: cf.fields\nOut[5]: (<FieldInfo(name='field', type=int) at 7fc10a069b40>,)\n```\n\nThe same goes for attributes, although this example file does not contain any:\n\n```python3\nIn [6]: cf.attributes\nOut[6]: {}\n```\n\n### Editing bytecode\n\nCreating valid bytecode can be quite an annoyance, so kirjava provides functionality that allows you to edit methods with ease.  \nThe main classes you'll be using for this are `InsnGraph`, `InsnBlock` and `InsnEdge`.\n\n#### Disassembly\n\nTo disassemble a method, you can use the shortcut:\n\n```python3\nIn [7]: graph = kirjava.disassemble(cf.get_method(\"test\"))\n\nIn [8]: graph\nOut[8]: <InsnGraph(blocks=10, edges=12) at 7fc10abfed50>\n```\n\nOr more verbosely:\n\n```python3\nIn [7]: graph = kirjava.analysis.InsnGraph.disassemble(cf.get_method(\"test\"))\n\nIn [8]: graph\nOut[8]: <InsnGraph(blocks=10, edges=12) at 7fc10abfed50>\n```\n\nYou can then view the blocks and edges present in the graph:\n\n```python3\nIn [9]: graph.blocks\nOut[9]: \n(<InsnBlock(label=0, instructions=[iload_1]) at 7fc10a1ed340>,\n <InsnReturnBlock() at 7fc10b60e5d0>,\n <InsnRethrowBlock() at 7fc10ab8f5c0>,\n <InsnBlock(label=1, instructions=[aload_0, iconst_0, putfield Test.field:I]) at 7fc10abc9f80>,\n <InsnBlock(label=2, instructions=[aload_0, getfield Test.field:I]) at 7fc10abcac40>,\n <InsnBlock(label=3, instructions=[iconst_0]) at 7fc10a2138c0>,\n <InsnBlock(label=4, instructions=[]) at 7fc10a211f00>,\n <InsnBlock(label=5, instructions=[iload_1]) at 7fc10a210340>,\n <InsnBlock(label=6, instructions=[]) at 7fc10a2103c0>,\n <InsnBlock(label=7, instructions=[iinc 1 by 1]) at 7fc10a213240>)\n\nIn [10]: graph.edges\nOut[10]: \n(<FallthroughEdge(from=block 0, to=block 1)>,\n <JumpEdge(from=block 0, to=block 2, instruction=ifne)>,\n <FallthroughEdge(from=block 1, to=block 2)>,\n <FallthroughEdge(from=block 2, to=block 3)>,\n <JumpEdge(from=block 2, to=block 4, instruction=ifgt)>,\n <JumpEdge(from=block 3, to=block 5, instruction=ifeq)>,\n <FallthroughEdge(from=block 3, to=block 4)>,\n <JumpEdge(from=block 4, to=return block, instruction=return)>,\n <FallthroughEdge(from=block 5, to=block 6)>,\n <JumpEdge(from=block 5, to=block 7, instruction=ifeq)>,\n <JumpEdge(from=block 6, to=return block, instruction=return)>,\n <JumpEdge(from=block 7, to=return block, instruction=return)>)\n```\n\n#### Editing blocks\n\nSay for example you wanted to change the value of `Test.field` from `0` to `17`, you could do this:\n\n```python3\nIn [11]: graph[1].remove(kirjava.instructions.iconst_0)\n    ...: graph[1].insert(1, kirjava.instructions.bipush(17))\nOut[11]: <ConstantInstruction(opcode=0x10, mnemonic=bipush, constant=<Integer(17)>) at 7fc10a213480>\n```\n\nAnd just to check that we have edited the block correctly:\n\n```python3\nIn [12]: graph[1]\nOut[12]: <InsnBlock(label=1, instructions=[aload_0, bipush 17, putfield Test.field:I]) at 7fc10abc9f80>\n```\n\n#### Editing edges\n\nNow let's edit an edge. Firstly let's find one that we can edit easily for the sake of tutorial:\n\n```python3\nIn [13]: graph.out_edges(graph[2])\nOut[13]: \n(<FallthroughEdge(from=block 2, to=block 3)>,\n <JumpEdge(from=block 2, to=block 4, instruction=ifgt)>)\n```\n\nLet's change the `ifgt` instruction into an `iflt` for this example:\n\n```python3\nIn [14]: graph.jump(graph[2], graph[4], kirjava.instructions.iflt)\nOut[14]: <JumpEdge(from=block 2, to=block 4, instruction=iflt)>\n```\n\nAnd, to check:\n\n```python3\nIn [15]: graph.out_edges(graph[2])\nOut[15]: \n(<FallthroughEdge(from=block 2, to=block 3)>,\n <JumpEdge(from=block 2, to=block 4, instruction=iflt)>)\n```\n\nAs you can see we've managed to successfully edit the jump condition.  \n\nThere's a lot more that can be done than just these simple tutorials though **(have a play around!)**.\n\n### Analysing bytecode\n\nOften editing a method goes hand-in-hand with analysing it, and kirjava provides tools that allow you to statically analyse the data on the stack and in the locals via the use of the class `Trace`.\n\nTo create a trace for a method, you'll need to use the graph for said method. In this example, we'll use the graph from the previous examples:\n\n```python3\nIn [16]: trace = kirjava.trace(graph)\n\nIn [17]: trace\nOut[17]: <Trace(entries=9, exits=9, conflicts=0, subroutines=0, max_stack=2, max_locals=2) at 7fc10abff4c0>\n```\n\nAnd again, the more verbose method:\n\n```python3\nIn [16]: trace = kirjava.analysis.Trace.from_graph(graph)\n\nIn [17]: trace\nOut[17]: <Trace(entries=9, exits=9, conflicts=0, subroutines=0, max_stack=2, max_locals=2) at 7fc10abff4c0>\n```\n\nThe `Trace` class provides pre/post liveness information (on a per-block basis) as well as information on subroutines, type conflicts and frames at block entries/exits.\n\nFor example, we could look at the local pre-liveness for block 3:\n\n```python3\nIn [18]: trace.pre_liveness[graph[3]]\nOut[18]: {1}\n```\n\nWe could also view the state of the stack at the entry to it:\n\n```python3\nIn [19]: trace.entries[graph[3]]\nOut[19]: [<Frame(stack=[], locals={0=Test, 1=boolean}) at 7fc109ee7f10>]\n```\n\nAnd we can even inspect individual locals further:  \n\n```python3\nIn [20]: trace.entries[graph[3]][0].locals\nOut[20]: \n{0: <Entry(type=Test, constraints={Test, reference, java/lang/Object}) at 7fc109ee69d0>,\n 1: <Entry(type=boolean, constraints={int, boolean}) at 7fc109ee7ce0>}\n\nIn [21]: trace.entries[graph[3]][0].locals[0].constraints\nOut[21]: \n(<Entry.Constraint(type=reference, source=aload_0 @ block 1[0], original=False)>,\n <Entry.Constraint(type=Test, source=getfield Test.field:I @ block 2[1], original=False)>,\n <Entry.Constraint(type=Test, source=putfield Test.field:I @ block 1[2], original=False)>,\n <Entry.Constraint(type=java/lang/Object, source=None, original=True)>,\n <Entry.Constraint(type=Test, source=param 0 of Test#void test(boolean), original=True)>,\n <Entry.Constraint(type=reference, source=aload_0 @ block 2[0], original=False)>)\n\nIn [22]: trace.entries[graph[3]][0].locals[1].producers\nOut[22]: \n(<InstructionInBlock(index=0, block=block 7, instruction=iinc 1 by 1)>,\n <Frame.Parameter(index=1, type=boolean, method=Test#void test(boolean))>)\n\nIn [23]: trace.entries[graph[3]][0].locals[1].consumers\nOut[23]: \n(<InstructionInBlock(index=0, block=block 0, instruction=iload_1)>,\n <JumpEdge(from=block 0, to=block 2, instruction=ifne)>,\n <InstructionInBlock(index=0, block=block 5, instruction=iload_1)>,\n <JumpEdge(from=block 5, to=block 7, instruction=ifeq)>,\n <InstructionInBlock(index=0, block=block 7, instruction=iinc 1 by 1)>)\n```\n\n#### Assembly\n\nReassembling the method after editing is as easy as:\n\n```python3\nIn [24]: kirjava.assemble(graph)\n```\n\nOr:\n\n```python3\nIn [24]: graph.method.code = graph.assemble()\n```\n\n### Writing classfiles\n\nWriting classfiles back out is also easy:\n\n```python3\nIn [25]: kirjava.dump(cf, \"Test-edited.class\")\n```\n\nOr for the more verbose method:\n\n```python3\nIn [25]: with open(\"Test-edited.class\", \"wb\") as stream:\n    ...:     cf.write(stream)\n    ...: \n```\n\n## \"Trivia\"\n\nIt's honestly not super interesting, but if anyone was wondering, it IS named after a certain character from a certain book series.  \nThe name is not a Java-related pun, but it does help that \"java\" is in the name.\n",
    "bugtrack_url": null,
    "license": "GPL-3.0",
    "summary": "A Java bytecode library for Python.",
    "version": "0.1.5",
    "project_urls": {
        "Homepage": "https://github.com/notiska/kirjava"
    },
    "split_keywords": [
        "java",
        "jvm",
        "bytecode",
        "assembler",
        "disassembler"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "91e0bf6264ceb20edf4e2261cbb8d1b048e9f636fb22cba9df2232abb04240e5",
                "md5": "5481232963992e5307339bf6d83993f4",
                "sha256": "a8855b40f65cd4d8b4eaa13dd02facf2da92723e4a14b5daeba12bb1459d30c5"
            },
            "downloads": -1,
            "filename": "kirjava-jvm-0.1.5.tar.gz",
            "has_sig": false,
            "md5_digest": "5481232963992e5307339bf6d83993f4",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": null,
            "size": 119980,
            "upload_time": "2024-03-06T12:22:41",
            "upload_time_iso_8601": "2024-03-06T12:22:41.021496Z",
            "url": "https://files.pythonhosted.org/packages/91/e0/bf6264ceb20edf4e2261cbb8d1b048e9f636fb22cba9df2232abb04240e5/kirjava-jvm-0.1.5.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-03-06 12:22:41",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "notiska",
    "github_project": "kirjava",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "lcname": "kirjava-jvm"
}
        
Elapsed time: 0.22377s