Skip to content

Commit

Permalink
introduce tarjan's algorithm (#103)
Browse files Browse the repository at this point in the history
* implement tarjan's algorithm to find sccs for directed graphs
* implement tarjan's algorithm to find cut vertices/bridges/vdcc/edcc
  for undirected graphs
* formatting
  • Loading branch information
suncanghuai committed Jul 2, 2023
1 parent 98432ff commit facb4eb
Show file tree
Hide file tree
Showing 57 changed files with 225 additions and 4 deletions.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added .cache/clangd/index/DFS_BM.cpp.40AAB3F7C1259F0B.idx
Binary file not shown.
Binary file added .cache/clangd/index/Dial_BM.cpp.F3D5FBFDB3E5F774.idx
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added .cache/clangd/index/EBV.hpp.9F21401B505F63A9.idx
Binary file not shown.
Binary file added .cache/clangd/index/Edge.hpp.36CB9FCC5873EF5D.idx
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added .cache/clangd/index/HDRF.hpp.17F661AEF332D941.idx
Binary file not shown.
Binary file not shown.
Binary file added .cache/clangd/index/Node.hpp.1B18E7AA69513C9A.idx
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added .cache/clangd/index/Prim_BM.cpp.6A59BB33690C7587.idx
Binary file not shown.
Binary file added .cache/clangd/index/Reader.hpp.65EB828154860F82.idx
Binary file not shown.
Binary file added .cache/clangd/index/Record.hpp.875077E2C2839158.idx
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added .cache/clangd/index/Writer.hpp.A8B46E75D2EE1A12.idx
Binary file not shown.
Binary file added .cache/clangd/index/main.cpp.32179A7A9938FDB5.idx
Binary file not shown.
182 changes: 181 additions & 1 deletion include/Graph/Graph.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,21 @@ class Graph {
*/
virtual const DijkstraResult dijkstra(const Node<T> &source,
const Node<T> &target) const;
/**
* @brief This function runs the tarjan algorithm and returns different types
* of results depending on the input parameter typeMask.
*
* @param typeMask each bit of typeMask within valid range represents a kind
* of results should be returned.
*
* Note: No Thread Safe
*
* @return The types of return include strongly connected components
* (only for directed graphs) and cut vertices、 bridges、edge
* biconnected components and vertice biconnected components
* (only for undirected graphs).
*/
virtual const TarjanResult<T> tarjan(const unsigned int typeMask) const;
/**
* @brief Function runs the bellman-ford algorithm for some source node and
* target node in the graph and returns the shortest distance of target
Expand Down Expand Up @@ -2540,7 +2555,7 @@ bool Graph<T>::isUndirectedGraph() const {
}

template <typename T>
void Graph<T>::reverseDirectedGraph(){
void Graph<T>::reverseDirectedGraph() {
if (!isDirectedGraph()) {
throw std::runtime_error(ERR_UNDIR_GRAPH);
}
Expand Down Expand Up @@ -2636,6 +2651,171 @@ bool Graph<T>::isStronglyConnectedGraph() const {
}
}

template <typename T>
const TarjanResult<T> Graph<T>::tarjan(const unsigned int typeMask) const {
TarjanResult<T> result;
result.success = false;
bool isDirected = this->isDirectedGraph();

Check warning

Code scanning / Cppcheck (reported by Codacy)

misra violation 508 with no text in the supplied rule-texts-file Warning

misra violation 508 with no text in the supplied rule-texts-file
if (isDirected) {

Check warning on line 2659 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2655-L2659

Added lines #L2655 - L2659 were not covered by tests
// check whether targetMask is a subset of the mask for directed graph
unsigned int directedMask = TARJAN_FIND_SCC;
if ((typeMask | directedMask) != directedMask) {
result.errorMessage = ERR_DIR_GRAPH;
return result;

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 15.5 rule Note

MISRA 15.5 rule

Check warning on line 2664 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2661-L2664

Added lines #L2661 - L2664 were not covered by tests
}
} else {
// check whether targetMask is a subset of the mask for undirected graph
unsigned int undirectedMask = (TARJAN_FIND_CUTV | TARJAN_FIND_BRIDGE |

Check warning on line 2668 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2668

Added line #L2668 was not covered by tests
TARJAN_FIND_VBCC | TARJAN_FIND_EBCC);
if ((typeMask | undirectedMask) != undirectedMask) {
result.errorMessage = ERR_UNDIR_GRAPH;
return result;

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 15.5 rule Note

MISRA 15.5 rule

Check warning on line 2672 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2670-L2672

Added lines #L2670 - L2672 were not covered by tests
}
}

const auto &adjMatrix = getAdjMatrix();
const auto &nodeSet = getNodeSet();

Check warning

Code scanning / Cppcheck (reported by Codacy)

Local variable 'nodeSet' shadows outer function Warning

Local variable 'nodeSet' shadows outer function

Check warning on line 2677 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2676-L2677

Added lines #L2676 - L2677 were not covered by tests
std::unordered_map<size_t, int>

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 12.3 rule Note

MISRA 12.3 rule
discoveryTime; // the timestamp when a node is visited

Check warning on line 2679 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2679

Added line #L2679 was not covered by tests
std::unordered_map<size_t, int>

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 12.3 rule Note

MISRA 12.3 rule
lowestDisc; // the lowest discory time of all

Check warning on line 2681 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2681

Added line #L2681 was not covered by tests
// reachable nodes from current node
int timestamp = 0;
size_t rootId = 0;
std::stack<Node<T>> sccNodeStack;
std::stack<Node<T>> ebccNodeStack;
std::stack<Node<T>> vbccNodeStack;
std::unordered_set<size_t> inStack;

Check warning on line 2688 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2683-L2688

Added lines #L2683 - L2688 were not covered by tests
std::function<void(const shared<const Node<T>>, const shared<const Edge<T>>)>
dfs_helper = [this, typeMask, isDirected, &dfs_helper, &adjMatrix,

Check warning

Code scanning / Cppcheck (reported by Codacy)

misra violation 806 with no text in the supplied rule-texts-file Warning

misra violation 806 with no text in the supplied rule-texts-file

Check warning on line 2690 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2690

Added line #L2690 was not covered by tests
&discoveryTime, &lowestDisc, &timestamp, &rootId,
&sccNodeStack, &ebccNodeStack, &vbccNodeStack, &inStack,
&result](const shared<const Node<T>> curNode,
const shared<const Edge<T>> prevEdge) {

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 13.1 rule Note

MISRA 13.1 rule
// record the visited time of current node
discoveryTime[curNode->getId()] = timestamp;
lowestDisc[curNode->getId()] = timestamp;
timestamp++;
if (typeMask & TARJAN_FIND_SCC) {

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 14.4 rule Note

MISRA 14.4 rule
sccNodeStack.emplace(*curNode);

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 17.7 rule Note

MISRA 17.7 rule
inStack.emplace(curNode->getId());

Check warning on line 2701 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2696-L2701

Added lines #L2696 - L2701 were not covered by tests
}
if (typeMask & TARJAN_FIND_EBCC) {

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 14.4 rule Note

MISRA 14.4 rule
ebccNodeStack.emplace(*curNode);

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 17.7 rule Note

MISRA 17.7 rule

Check warning on line 2704 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2703-L2704

Added lines #L2703 - L2704 were not covered by tests
}
if (typeMask & TARJAN_FIND_VBCC) {

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 14.4 rule Note

MISRA 14.4 rule
vbccNodeStack.emplace(*curNode);

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 17.7 rule Note

MISRA 17.7 rule

Check warning on line 2707 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2706-L2707

Added lines #L2706 - L2707 were not covered by tests
}
// travel the neighbors
int numSon = 0;

Check warning

Code scanning / Cppcheck (reported by Codacy)

The scope of the variable 'numSon' can be reduced. Warning

The scope of the variable 'numSon' can be reduced.
bool nodeIsAdded =

Check warning

Code scanning / Cppcheck (reported by Codacy)

The scope of the variable 'nodeIsAdded' can be reduced. Warning

The scope of the variable 'nodeIsAdded' can be reduced.

Check warning on line 2711 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2710-L2711

Added lines #L2710 - L2711 were not covered by tests
false; // whether a node has been marked as a cut vertice
if (adjMatrix->find(curNode) != adjMatrix->end()) {
for (const auto &[neighborNode, edge] : adjMatrix->at(curNode)) {
if (!discoveryTime.count(neighborNode->getId())) {
dfs_helper(neighborNode, edge);
lowestDisc[curNode->getId()] =
std::min(lowestDisc[curNode->getId()],
lowestDisc[neighborNode->getId()]);

Check warning on line 2719 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2713-L2719

Added lines #L2713 - L2719 were not covered by tests

if (typeMask & TARJAN_FIND_BRIDGE) {

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 14.4 rule Note

MISRA 14.4 rule

Check warning on line 2721 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2721

Added line #L2721 was not covered by tests
// lowestDisc of neighbor node is larger than that of current
// node means we can travel back to a visited node only through
// this edge
if (discoveryTime[curNode->getId()] <
lowestDisc[neighborNode->getId()]) {
result.bridges.emplace_back(*edge);

Check warning on line 2727 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2725-L2727

Added lines #L2725 - L2727 were not covered by tests
}
}

if ((typeMask & TARJAN_FIND_CUTV) && (nodeIsAdded == false)) {
if (curNode->getId() == rootId) {
numSon++;

Check warning on line 2733 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2731-L2733

Added lines #L2731 - L2733 were not covered by tests
// a root node is a cut vertices only when it connects at
// least two connected components
if (numSon == 2) {
nodeIsAdded = true;
result.cutVertices.emplace_back(*curNode);

Check warning on line 2738 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2736-L2738

Added lines #L2736 - L2738 were not covered by tests
}
} else {
if (discoveryTime[curNode->getId()] <=
lowestDisc[neighborNode->getId()]) {
nodeIsAdded = true;
result.cutVertices.emplace_back(*curNode);

Check warning on line 2744 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2741-L2744

Added lines #L2741 - L2744 were not covered by tests
}
}
}

if (typeMask & TARJAN_FIND_VBCC) {

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 14.4 rule Note

MISRA 14.4 rule
if (discoveryTime[curNode->getId()] <=
lowestDisc[neighborNode->getId()]) {

Check warning on line 2751 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2749-L2751

Added lines #L2749 - L2751 were not covered by tests
// if current node is a cut vertice or the root node, the vbcc
// a vertice-biconnect-component which contains the neighbor
// node
std::vector<Node<T>> vbcc;
while (true) {

Check warning on line 2756 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2755-L2756

Added lines #L2755 - L2756 were not covered by tests
// pop a top node out of stack until
// the neighbor node has been poped out
Node<T> nodeAtTop = sccNodeStack.top();
vbccNodeStack.pop();
vbcc.emplace_back(nodeAtTop);

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 17.7 rule Note

MISRA 17.7 rule
if (nodeAtTop == *neighborNode) {
break;

Check warning on line 2763 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2759-L2763

Added lines #L2759 - L2763 were not covered by tests
}
}
vbcc.emplace_back(*curNode);

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 17.7 rule Note

MISRA 17.7 rule
result.verticeBiconnectedComps.emplace_back(std::move(vbcc));
}

Check warning on line 2768 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2766-L2768

Added lines #L2766 - L2768 were not covered by tests
}
} else if ((edge != prevEdge) &&
((isDirected == false) ||
(inStack.count(neighborNode->getId())))) {

Check warning on line 2772 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2770-L2772

Added lines #L2770 - L2772 were not covered by tests
// it's not allowed to go through the previous edge back
// for a directed graph, it's also not allowed to visit
// a node that is not in stack
lowestDisc[curNode->getId()] =
std::min(lowestDisc[curNode->getId()],
lowestDisc[neighborNode->getId()]);

Check warning on line 2778 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2776-L2778

Added lines #L2776 - L2778 were not covered by tests
}

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 15.7 rule Note

MISRA 15.7 rule
}
}
// find sccs for a undirected graph is very similar with
// find ebccs for a directed graph
if ((typeMask & TARJAN_FIND_SCC) || (typeMask & TARJAN_FIND_EBCC)) {
std::stack<Node<T>> &nodeStack =
(typeMask & TARJAN_FIND_SCC) ? sccNodeStack : ebccNodeStack;
if (discoveryTime[curNode->getId()] == lowestDisc[curNode->getId()]) {
std::vector<Node<T>> connectedComp;
while (true) {

Check warning on line 2789 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2784-L2789

Added lines #L2784 - L2789 were not covered by tests
// pop a top node out of stack until
// the current node has been poped out
Node<T> nodeAtTop = nodeStack.top();
nodeStack.pop();
connectedComp.emplace_back(nodeAtTop);

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 17.7 rule Note

MISRA 17.7 rule
if (nodeAtTop == *curNode) {
break;

Check warning on line 2796 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2792-L2796

Added lines #L2792 - L2796 were not covered by tests
}
}
// store this component in result
(typeMask & TARJAN_FIND_SCC)
? result.stronglyConnectedComps.emplace_back(
std::move(connectedComp))
: result.edgeBiconnectedComps.emplace_back(
std::move(connectedComp));

Check notice

Code scanning / Cppcheck (reported by Codacy)

Access of moved variable 'connectedComp'. Note

Access of moved variable 'connectedComp'.
}

Check warning on line 2805 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2800-L2805

Added lines #L2800 - L2805 were not covered by tests
}
};

for (const auto &node : nodeSet) {
if (!discoveryTime.count(node->getId())) {
rootId = node->getId();
dfs_helper(node, nullptr);

Check warning on line 2812 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2809-L2812

Added lines #L2809 - L2812 were not covered by tests
}
}

return result;
}

Check warning on line 2817 in include/Graph/Graph.hpp

View check run for this annotation

Codecov / codecov/patch

include/Graph/Graph.hpp#L2816-L2817

Added lines #L2816 - L2817 were not covered by tests

template <typename T>

Check notice on line 2819 in include/Graph/Graph.hpp

View check run for this annotation

codefactor.io / CodeFactor

include/Graph/Graph.hpp#L2655-L2819

Complex Method
TopoSortResult<T> Graph<T>::topologicalSort() const {
TopoSortResult<T> result;
Expand Down
47 changes: 44 additions & 3 deletions include/Utility/Typedef.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
#include <map>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>

#include "ConstValue.hpp"
#include "PointerHash.hpp"
Expand All @@ -35,10 +37,10 @@ namespace CXXGraph {
template <typename T>
using unique = std::unique_ptr<T>;
template <typename T>
using shared= std::shared_ptr<T>;
using shared = std::shared_ptr<T>;

using std::make_unique;
using std::make_shared;
using std::make_unique;

Check warning

Code scanning / Cppcheck (reported by Codacy)

misra violation 506 with no text in the supplied rule-texts-file Warning

misra violation 506 with no text in the supplied rule-texts-file

template <typename T>
class Node;
Expand All @@ -62,6 +64,14 @@ enum E_InputOutputFormat {

typedef E_InputOutputFormat InputOutputFormat;

/// specify the type of results returnde by tarjan's algorithm
enum TarjanAlgorithmTypes {

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 2.4 rule Note

MISRA 2.4 rule
TARJAN_FIND_SCC = (1 << 0),

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 10.1 rule Note

MISRA 10.1 rule
TARJAN_FIND_CUTV = (1 << 1),

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 10.1 rule Note

MISRA 10.1 rule
TARJAN_FIND_BRIDGE = (1 << 2),

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 10.1 rule Note

MISRA 10.1 rule
TARJAN_FIND_VBCC = (1 << 3),

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 10.1 rule Note

MISRA 10.1 rule
TARJAN_FIND_EBCC = (1 << 4)

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 10.1 rule Note

MISRA 10.1 rule
};
/////////////////////////////////////////////////////
// Structures ///////////////////////////////////////

Expand Down Expand Up @@ -190,6 +200,35 @@ struct SCCResult_struct {
template <typename T>
using SCCResult = SCCResult_struct<T>;

/// Struct that contains the information about TopologicalSort's Algorithm
/// results
template <typename T>
struct TarjanResult_struct {
bool success =
false; // TRUE if the function does not return error, FALSE otherwise
std::string errorMessage = ""; // message of error

Check warning

Code scanning / Cppcheck (reported by Codacy)

misra violation 704 with no text in the supplied rule-texts-file Warning

misra violation 704 with no text in the supplied rule-texts-file
Components<T>
stronglyConnectedComps; // vectors that store nodes belong to same SCC
// (valid only if a graph is directed and flag
// TARJAN_FIND_SCC is set)
Components<T>
verticeBiconnectedComps; // vectors that store nodes belong to same v-bcc
// (valid only if a graph is undirected and flag
// TARJAN_FIND_VBCC is set)
Components<T>
edgeBiconnectedComps; // vectors that store nodes belong to same e-bcc
// (valid only if a graph is undirected and flag
// TARJAN_FIND_EBCC is set)
std::vector<Node<T>> cutVertices; // a vector that stores cut vertices

Check warning

Code scanning / Cppcheck (reported by Codacy)

struct member 'TarjanResult_struct::cutVertices' is never used. Warning

struct member 'TarjanResult_struct::cutVertices' is never used.
// (valid only is a graph is undirected and
// flag TARJAN_FIND_CUTV is set)
std::vector<Edge<T>> bridges; // a vector that stores bridges

Check warning

Code scanning / Cppcheck (reported by Codacy)

struct member 'TarjanResult_struct::bridges' is never used. Warning

struct member 'TarjanResult_struct::bridges' is never used.
// (valid only is a graph is undirected and
// flag TRAJAN_FIND_BRIDGES is set)
};
template <typename T>
using TarjanResult = TarjanResult_struct<T>;

/// Struct that contains the information about Best First Search Algorithm
/// results
template <typename T>
Expand All @@ -209,7 +248,9 @@ using BestFirstSearchResult = BestFirstSearchResult_struct<T>;

template <typename T>
using AdjacencyMatrix = std::unordered_map<
shared<const Node<T>>, std::vector<std::pair<shared<const Node<T>>, shared<const Edge<T>>>>, nodeHash<T>>;
shared<const Node<T>>,

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 12.3 rule Note

MISRA 12.3 rule
std::vector<std::pair<shared<const Node<T>>, shared<const Edge<T>>>>,

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 12.3 rule Note

MISRA 12.3 rule
nodeHash<T>>;

template <typename T>
using PartitionMap =
Expand Down

0 comments on commit facb4eb

Please sign in to comment.